├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── images.d.ts
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── screenshots
├── ui_solarized_dark.png
└── ui_white.png
├── scripts
├── operations.ts
└── publish_demo.js
├── src
├── Root.tsx
├── api.ts
├── configureStore.ts
├── edikit
│ ├── builder
│ │ ├── components
│ │ │ ├── Block.tsx
│ │ │ ├── Builder.tsx
│ │ │ └── BuilderLabel.tsx
│ │ └── index.ts
│ ├── button
│ │ ├── Button.tsx
│ │ ├── Button_styled.ts
│ │ └── index.ts
│ ├── form
│ │ ├── components
│ │ │ ├── Input.tsx
│ │ │ └── Select.tsx
│ │ └── index.ts
│ ├── index.ts
│ ├── notifications
│ │ ├── components
│ │ │ ├── Notifications.tsx
│ │ │ └── NotificationsItem.tsx
│ │ ├── containers
│ │ │ └── NotificationsContainer.ts
│ │ ├── index.ts
│ │ ├── store
│ │ │ ├── actions.spec.ts
│ │ │ ├── actions.ts
│ │ │ ├── epics.ts
│ │ │ ├── index.ts
│ │ │ ├── middlewares.ts
│ │ │ ├── reducers.ts
│ │ │ └── types.ts
│ │ └── types.ts
│ ├── panes
│ │ ├── components
│ │ │ ├── EmptyPane.tsx
│ │ │ ├── EmptyPane_styled.ts
│ │ │ ├── Pane.tsx
│ │ │ ├── PaneHeader.tsx
│ │ │ ├── PaneHeaderButton.tsx
│ │ │ ├── PaneHeaderButton_styled.ts
│ │ │ ├── PaneHeader_styled.ts
│ │ │ ├── PaneManager.tsx
│ │ │ ├── PaneManager_styled.ts
│ │ │ ├── Pane_styled.ts
│ │ │ ├── SplitPaneIcon.tsx
│ │ │ └── SplitPaneIcon_styled.ts
│ │ ├── createPaneManager.ts
│ │ ├── index.ts
│ │ ├── operations.spec.ts
│ │ ├── operations.ts
│ │ ├── store
│ │ │ ├── actions.spec.ts
│ │ │ ├── actions.ts
│ │ │ ├── index.ts
│ │ │ ├── reducers.ts
│ │ │ ├── selectors.ts
│ │ │ └── types.ts
│ │ └── types.ts
│ ├── theming
│ │ ├── index.ts
│ │ ├── themes
│ │ │ ├── black.ts
│ │ │ ├── index.js
│ │ │ ├── solarizedDark.ts
│ │ │ └── white.ts
│ │ └── types.ts
│ ├── tree
│ │ ├── components
│ │ │ ├── Tree.tsx
│ │ │ ├── TreeNode.tsx
│ │ │ ├── TreeNode_styled.ts
│ │ │ └── Tree_styled.tsx
│ │ ├── index.ts
│ │ ├── lib.ts
│ │ └── types.ts
│ └── util
│ │ └── uuid.ts
├── editorConfig.ts
├── globalStyles.ts
├── index.tsx
├── lib
│ └── status.ts
├── modules
│ ├── core
│ │ ├── components
│ │ │ ├── App.tsx
│ │ │ ├── AppBar.tsx
│ │ │ ├── AppBar_styled.ts
│ │ │ ├── App_styled.ts
│ │ │ ├── Explorer.tsx
│ │ │ ├── StatusIcon.tsx
│ │ │ └── Tree.ts
│ │ ├── containers
│ │ │ ├── AppContainer.tsx
│ │ │ └── ExplorerContainer.ts
│ │ ├── index.ts
│ │ └── store
│ │ │ ├── actions.ts
│ │ │ ├── epics.ts
│ │ │ ├── index.ts
│ │ │ ├── reducers.ts
│ │ │ └── types.ts
│ ├── mappings
│ │ ├── components
│ │ │ ├── CreateMapping.tsx
│ │ │ ├── Mapping.tsx
│ │ │ ├── MappingBar.tsx
│ │ │ ├── MappingBuilder.tsx
│ │ │ ├── MappingIcon.tsx
│ │ │ ├── MappingJsonEditor.tsx
│ │ │ ├── Mapping_styled.ts
│ │ │ └── builder
│ │ │ │ ├── BuilderRequest.tsx
│ │ │ │ ├── BuilderResponse.tsx
│ │ │ │ ├── BuilderSectionLabel.tsx
│ │ │ │ ├── Builder_styled.ts
│ │ │ │ ├── RequestParams.tsx
│ │ │ │ ├── RequestParamsSwitcher.tsx
│ │ │ │ ├── RequestUrl.tsx
│ │ │ │ ├── RequestUrlDetails.tsx
│ │ │ │ └── ResponseBase.tsx
│ │ ├── containers
│ │ │ ├── CreateMappingContainer.ts
│ │ │ └── MappingContainer.ts
│ │ ├── contentTypes.tsx
│ │ ├── dto.ts
│ │ ├── index.ts
│ │ ├── store
│ │ │ ├── actions.spec.ts
│ │ │ ├── actions.tsx
│ │ │ ├── epics.ts
│ │ │ ├── index.ts
│ │ │ ├── reducers.spec.ts
│ │ │ ├── reducers.ts
│ │ │ └── types.ts
│ │ ├── types.ts
│ │ └── validation.ts
│ ├── servers
│ │ ├── components
│ │ │ ├── CreateServer.tsx
│ │ │ └── CreateServer_styled.ts
│ │ ├── containers
│ │ │ └── CreateServerContainer.ts
│ │ ├── contentTypes.tsx
│ │ ├── index.ts
│ │ ├── store
│ │ │ ├── actions.tsx
│ │ │ ├── epics.ts
│ │ │ ├── index.ts
│ │ │ ├── reducer.ts
│ │ │ └── types.ts
│ │ └── types.ts
│ └── settings
│ │ ├── components
│ │ ├── Settings.tsx
│ │ ├── SettingsIcon.tsx
│ │ └── Settings_styled.ts
│ │ ├── containers
│ │ └── SettingsContainer.ts
│ │ ├── contentTypes.tsx
│ │ ├── index.ts
│ │ ├── store
│ │ ├── actions.ts
│ │ ├── index.ts
│ │ ├── reducers.ts
│ │ └── types.ts
│ │ └── types.ts
├── store.ts
├── themes.ts
└── types.ts
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # testing
5 | coverage
6 |
7 | # production
8 | build
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | lerna-debug.log*
15 |
16 | # IDE
17 | .idea
18 |
19 | # misc
20 | .DS_Store
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | - '10'
5 | before_script:
6 | - yarn install
7 | script:
8 | - yarn lint
9 | - yarn test
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine
2 |
3 | WORKDIR /app
4 | RUN apk add --no-cache \
5 | git \
6 | bash
7 |
8 | COPY *.json yarn.lock ./
9 | RUN yarn install
10 |
11 | COPY /src /app/src
12 | COPY /public /app/public
13 |
14 | EXPOSE 3000
15 |
16 | CMD ["yarn", "start"]
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Raphaël Benitte
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WireMock UI
2 |
3 | [](https://github.com/plouc/wiremock-ui/blob/master/LICENSE)
4 | [](https://github.com/plouc/wiremock-ui/issues)
5 | [](https://travis-ci.org/plouc/wiremock-ui)
6 |
7 | An unofficial UI for [WireMock](http://wiremock.org/).
8 |
9 | [Features](#features) | [Project structure](#project-structure) | [How to start the UI](#start-ui)
10 |
11 | 
12 |
13 | ## Features
14 |
15 | - supports multi wiremock servers
16 | - create/edit/delete wiremock stubs
17 | - json or visual mode
18 | - theming
19 | - support multiple panes
20 |
21 | 
22 |
23 | ## Project structure
24 |
25 | The project was bootstrapped using [create-react-app](https://github.com/facebook/create-react-app)
26 | using custom scripts [react-scripts-ts](https://github.com/wmonk/create-react-app-typescript)
27 | for typescript support.
28 |
29 | ## How to start the UI
30 |
31 | 1. Use a shell and enter the wiremock-ui directory.
32 | 2. Type: yarn install
33 | 3. Type: yarn start
34 |
35 | 4. The first thing you do is: Add a server (use the adress of a running WireMock Server)
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | wiremock-ui:
5 | image: wiremock-ui:latest
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | ports:
10 | - "3000:3000"
11 | environment:
12 | NODE_ENV: production
13 | command: yarn start
14 | restart: always
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/inline-style-prefixer": "^3.0.1",
7 | "@types/lodash": "^4.14.116",
8 | "@types/react-redux": "^6.0.6",
9 | "@types/yup": "^0.24.7",
10 | "brace": "^0.11.1",
11 | "edikit": "1.0.0-alpha.4",
12 | "formik": "^1.0.2",
13 | "lodash": "^4.17.10",
14 | "react": "^16.4.2",
15 | "react-ace": "^6.1.4",
16 | "react-dom": "^16.4.2",
17 | "react-feather": "^1.1.1",
18 | "react-redux": "^5.0.7",
19 | "react-scripts-ts": "2.17.0",
20 | "react-split-pane": "^0.1.82",
21 | "redux": "^4.0.0",
22 | "redux-observable": "^1.0.0",
23 | "rxjs": "^6.2.2",
24 | "styled-components": "^3.4.2",
25 | "typesafe-actions": "^2.0.4",
26 | "yup": "^0.26.2"
27 | },
28 | "devDependencies": {
29 | "@types/jest": "^23.3.1",
30 | "@types/node": "^10.5.7",
31 | "@types/react": "^16.4.8",
32 | "@types/react-dom": "^16.0.7",
33 | "gh-pages": "^1.2.0",
34 | "redux-devtools-extension": "^2.13.5",
35 | "typescript": "^3.0.1"
36 | },
37 | "scripts": {
38 | "start": "react-scripts-ts start",
39 | "build": "react-scripts-ts build",
40 | "test": "react-scripts-ts test --env=jsdom",
41 | "lint": "tslint -c tslint.json 'src/**/*.ts'",
42 | "demo:publish": "PUBLIC_URL=https://plouc.github.io/wiremock-ui yarn run build && node scripts/publish_demo.js",
43 | "demo:pub": "yarn run demo:publish"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/wiremock-ui/af76868257ec7e3bd77770f614f6d5ed09052c55/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | wiremock-ui | non official
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "WireMock UI",
3 | "name": "WireMock UI",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/screenshots/ui_solarized_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/wiremock-ui/af76868257ec7e3bd77770f614f6d5ed09052c55/screenshots/ui_solarized_dark.png
--------------------------------------------------------------------------------
/screenshots/ui_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/wiremock-ui/af76868257ec7e3bd77770f614f6d5ed09052c55/screenshots/ui_white.png
--------------------------------------------------------------------------------
/scripts/operations.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IPane,
3 | IPaneContent,
4 | PaneSplitAxis,
5 | IPanesOpertations,
6 | } from './types'
7 | import uuid from '../util/uuid'
8 |
9 | export const paneOperationsFactory = (): IPanesOpertations => {
10 | const flattenPane = (
11 | pane: IPane,
12 | acc: Array> = []
13 | ): Array> => {
14 | const reacc = [...acc, pane]
15 | if (pane.children === undefined) {
16 | return reacc
17 | }
18 |
19 | return pane.children.reduce((a, child) => flattenPane(child, a), reacc)
20 | }
21 |
22 | return {
23 | addContentToCurrentPane: (rootPane: IPane, content: IPaneContent): IPane => {
24 | flattenPane(rootPane).forEach(pane => {
25 | if (pane.isCurrent) {
26 | pane.contents = [
27 | ...pane.contents.map(c => ({
28 | ...c,
29 | isCurrent: false
30 | })),
31 | {
32 | ...content,
33 | isCurrent: true,
34 | }
35 | ]
36 | }
37 | })
38 |
39 | return { ...rootPane }
40 | },
41 | setPaneCurrentContent: (rootPane: IPane, paneId: string, contentId: string): IPane => {
42 | const allPanes = flattenPane(rootPane)
43 | allPanes.forEach(pane => {
44 | if (pane.id === paneId) {
45 | pane.isCurrent = true
46 | pane.contents = pane.contents.map(c => ({
47 | ...c,
48 | isCurrent: c.id === contentId,
49 | }))
50 | } else {
51 | pane.isCurrent = false
52 | }
53 | })
54 |
55 | return { ...rootPane }
56 | },
57 | removePaneContent: (rootPane: IPane, paneId: string, contentId: string): IPane => {
58 | const allPanes = flattenPane(rootPane)
59 | let removeId: string = ''
60 | allPanes.forEach(pane => {
61 | if (pane.id === paneId) {
62 | const contents = pane.contents.filter(c => c.id !== contentId)
63 | const shouldRemove = contents.length === 0 && pane.id !== rootPane.id
64 | if (shouldRemove) {
65 | removeId = pane.id
66 | } else {
67 | pane.isCurrent = true
68 | pane.contents = contents
69 |
70 | // set last remaining content as active if there isn't any
71 | const hasCurrent = pane.contents.some(c => c.isCurrent)
72 | if (!hasCurrent && pane.contents.length > 0) {
73 | pane.contents[pane.contents.length - 1].isCurrent = true
74 | }
75 | }
76 | } else {
77 | pane.isCurrent = false
78 | }
79 | })
80 |
81 | if (removeId !== '') {
82 | allPanes.forEach(pane => {
83 | if (pane.children === undefined || pane.children.length === 0) return
84 |
85 | const childToRemove = pane.children.find(c => c.id === removeId)
86 | if (childToRemove !== undefined) {
87 | // merge panel and replace parent if one of its children has been removed
88 | const remainingPane = pane.children.find(c => c.id !== removeId)
89 | if (remainingPane !== undefined) {
90 | pane.split = remainingPane.split
91 | pane.splitAxis = remainingPane.splitAxis
92 | pane.children = remainingPane.children
93 | pane.contents = remainingPane.contents
94 | pane.isCurrent = true
95 | }
96 | }
97 | })
98 | }
99 |
100 | return { ...rootPane }
101 | },
102 | splitPane: (rootPane: IPane, paneId: string, axis: PaneSplitAxis): IPane => {
103 | flattenPane(rootPane).forEach(pane => {
104 | if (pane.id === paneId) {
105 | const { contents } = pane
106 |
107 | const content = contents.find(c => c.isCurrent)
108 | if (content === undefined) return
109 |
110 | const childPaneAId = uuid()
111 | const childPaneBId = uuid()
112 |
113 | pane.isCurrent = false
114 | pane.contents = []
115 | pane.split = true
116 | pane.splitAxis = axis
117 | pane.children = [
118 | {
119 | id: childPaneAId,
120 | contents,
121 | split: false,
122 | children: [],
123 | isCurrent: false,
124 | },
125 | {
126 | id: childPaneBId,
127 | contents: [content],
128 | split: false,
129 | children: [],
130 | isCurrent: true,
131 | },
132 | ]
133 | } else {
134 | pane.isCurrent = false
135 | }
136 | })
137 |
138 | return { ...rootPane }
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/scripts/publish_demo.js:
--------------------------------------------------------------------------------
1 | const ghpages = require('gh-pages')
2 |
3 | ghpages.publish('build', (err) => {
4 | if (err !== undefined) {
5 | console.error(err)
6 | process.exit(1)
7 | }
8 |
9 | console.log('demo successfully published!')
10 | })
--------------------------------------------------------------------------------
/src/Root.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Store } from 'redux'
3 | import { Provider } from 'react-redux'
4 | import { IApplicationState } from './store'
5 | import AppContainer from './modules/core/containers/AppContainer'
6 |
7 | interface IRootProps {
8 | store: Store
9 | }
10 |
11 | class Root extends React.Component {
12 | render() {
13 | const { store } = this.props
14 |
15 | return (
16 |
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 | export default Root
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import { ajax } from 'rxjs/ajax'
2 | import { IServer } from './modules/servers'
3 | import { IMapping } from './modules/mappings'
4 |
5 | const buildApiUrl = (server: IServer, path: string): string =>
6 | `${server.url}${server.port ? `:${server.port}` : ''}/__admin${path}`
7 |
8 | export const getMappings = (server: IServer) =>
9 | ajax.getJSON(buildApiUrl(server, '/mappings'))
10 |
11 | export const getMapping = (server: IServer, mappingId: string) =>
12 | ajax.getJSON(buildApiUrl(server, `/mappings/${mappingId}`))
13 |
14 | export const createMapping = (server: IServer, mapping: Partial) =>
15 | ajax.post(
16 | buildApiUrl(server, '/mappings'),
17 | mapping,
18 | { 'Content-Type': 'application/json' }
19 | )
20 |
21 | export const updateMapping = (server: IServer, mapping: IMapping) =>
22 | ajax.put(
23 | buildApiUrl(server, `/mappings/${mapping.id}`),
24 | mapping,
25 | { 'Content-Type': 'application/json' }
26 | )
27 |
28 | export const deleteMapping = (server: IServer, mappingId: string) =>
29 | ajax.delete(buildApiUrl(server, `/mappings/${mappingId}`))
30 |
--------------------------------------------------------------------------------
/src/configureStore.ts:
--------------------------------------------------------------------------------
1 | import { Store, createStore, applyMiddleware } from 'redux'
2 | import { composeWithDevTools } from 'redux-devtools-extension'
3 | import { createEpicMiddleware } from 'redux-observable'
4 | import { notificationsMiddleware } from 'edikit'
5 | import { IApplicationState, rootReducer, rootEpic } from './store'
6 |
7 | export default function configureStore(): Store {
8 | const epicMiddleware = createEpicMiddleware()
9 |
10 | const middlewares = [
11 | epicMiddleware,
12 | notificationsMiddleware,
13 | ]
14 |
15 | const store = createStore(
16 | rootReducer,
17 | composeWithDevTools(
18 | applyMiddleware(...middlewares)
19 | )
20 | )
21 |
22 | epicMiddleware.run(rootEpic)
23 |
24 | return store
25 | }
26 |
--------------------------------------------------------------------------------
/src/edikit/builder/components/Block.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled, { withTheme } from 'styled-components'
3 | import { ITheme } from 'edikit'
4 |
5 | interface IContainerProps {
6 | withLink: boolean
7 | }
8 |
9 | const Container = styled.div`
10 | position: relative;
11 | padding: calc(${props => props.theme.builder.spacing} / 2) 0;
12 |
13 | &:first-child {
14 | padding-top: ${props => props.theme.builder.spacing};
15 | }
16 | &:last-child {
17 | padding-bottom: 0;
18 | }
19 |
20 | ${props => {
21 | if (props.withLink === false) return ''
22 | return `
23 | &:before {
24 | content: "";
25 | position: absolute;
26 | width: 2px;
27 | top: 0;
28 | left: 22px;
29 | bottom: 0;
30 | background: ${props.theme.builder.link.color};
31 | }
32 | `
33 | }}
34 | `
35 |
36 | const Header = styled.header`
37 | `
38 |
39 | const Title = styled.div`
40 | font-weight: 600;
41 | `
42 |
43 | interface IContentProps {
44 | withMarker: boolean
45 | markerColor: string
46 | }
47 |
48 | const Content = styled.div`
49 | padding: 9px 16px;
50 | border-radius: 2px;
51 | position: relative;
52 | background: ${props => props.theme.builder.block.background};
53 | ${props => props.theme.builder.block.css}
54 |
55 | &:before {
56 | content: "";
57 | position: absolute;
58 | width: 4px;
59 | top: 0;
60 | left: 0;
61 | bottom: 0;
62 | background: ${props => props.markerColor};
63 | }
64 | `
65 |
66 | interface IBlockProps {
67 | withLink?: boolean
68 | withMarker?: boolean
69 | markerColor?: string
70 | title?: React.ReactNode
71 | icon?: React.ReactNode
72 | children?: React.ReactNode
73 | theme: ITheme
74 | }
75 |
76 | class Block extends React.Component {
77 | static defaultProps = {
78 | withMarker: true,
79 | markerColor: 'accent',
80 | withLink: false
81 | }
82 |
83 | render() {
84 | const {
85 | title,
86 | children,
87 | withLink,
88 | withMarker,
89 | markerColor: markerColorKey,
90 | theme,
91 | } = this.props
92 |
93 | if (title === undefined && children === undefined) {
94 | return null
95 | }
96 |
97 | let markerColor = theme.colors[markerColorKey!]
98 | if (markerColor === undefined) {
99 | markerColor = markerColorKey
100 | }
101 |
102 | return (
103 |
104 |
108 | {title && (
109 |
112 | )}
113 |
114 | {children}
115 |
116 |
117 |
118 | )
119 | }
120 | }
121 |
122 | export default withTheme(Block)
123 |
--------------------------------------------------------------------------------
/src/edikit/builder/components/Builder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled, { withTheme } from 'styled-components'
3 | import { ITheme } from 'edikit'
4 |
5 | const Container = styled.div`
6 | position: relative;
7 | width: 100%;
8 | height: 100%;
9 | overflow-x: hidden;
10 | overflow-y: auto;
11 | background: ${props => props.theme.builder.background};
12 | `
13 |
14 | const Header = styled.header`
15 | padding: 9px 16px;
16 | background: ${props => props.theme.builder.header.background};
17 | `
18 |
19 | const Title = styled.div`
20 | font-weight: 600;
21 | font-size: 14px;
22 | white-space: nowrap;
23 | overflow: hidden;
24 | text-overflow: ellipsis;
25 | color: ${props => props.theme.builder.header.title.color};
26 | `
27 |
28 | const Subtitle = styled.div`
29 | font-size: 13px;
30 | color: ${props => props.theme.builder.header.subtitle.color};
31 | `
32 |
33 | const Content = styled.div`
34 | padding: 0 16px 18px;
35 | overflow: hidden;
36 | `
37 |
38 | interface IBuilderProps {
39 | title?: React.ReactNode
40 | subtitle?: React.ReactNode
41 | icon?: React.ReactNode
42 | children?: React.ReactNode
43 | theme: ITheme
44 | }
45 |
46 | class Builder extends React.Component {
47 | render() {
48 | const {
49 | title,
50 | subtitle,
51 | children,
52 | } = this.props
53 |
54 | return (
55 |
56 | {title && (
57 |
58 | {title}
59 | {subtitle && {subtitle}}
60 |
61 | )}
62 | {children && {children}}
63 |
64 | )
65 | }
66 | }
67 |
68 | export default withTheme(Builder)
69 |
--------------------------------------------------------------------------------
/src/edikit/builder/components/BuilderLabel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const Label = styled.div`
5 | position: absolute;
6 | z-index: 10;
7 | top: calc(${props => props.theme.builder.spacing} / 2);
8 | left: 0;
9 | height: 28px;
10 | padding: 0 13px;
11 | display: flex;
12 | align-items: center;
13 | border-radius: 14px;
14 | overflow: hidden;
15 | font-weight: bold;
16 | font-size: 12px;
17 | text-overflow: ellipsis;
18 | white-space: nowrap;
19 | background: ${props => props.theme.builder.label.background};
20 | color: ${props => props.theme.builder.label.color};
21 | ${props => props.theme.builder.label.css}
22 | `
23 |
24 | interface IContainerProps {
25 | withLink: boolean
26 | }
27 |
28 | const Container = styled.div`
29 | position: relative;
30 | height: calc(${props => props.theme.builder.spacing} + 28px);
31 | display: flex;
32 |
33 | &:first-child {
34 | padding-top: calc(${props => props.theme.builder.spacing} * 1.5 + 28px);
35 | ${Label} {
36 | top: ${props => props.theme.builder.spacing};
37 | }
38 | }
39 |
40 | &:last-child {
41 | height: calc(${props => props.theme.builder.spacing} / 2 + 28px);
42 | }
43 |
44 | ${props => {
45 | if (props.withLink === false) return ''
46 | return `
47 | &:before {
48 | z-index: 0;
49 | content: "";
50 | position: absolute;
51 | width: 2px;
52 | top: 0;
53 | left: 22px;
54 | bottom: 0;
55 | background: ${props.theme.builder.link.color};
56 | }
57 | `
58 | }}
59 | `
60 |
61 | interface IBuilderLabelProps {
62 | label?: React.ReactNode
63 | children?: React.ReactNode
64 | withLink: boolean
65 | style: any
66 | onClick?: React.MouseEventHandler
67 | }
68 |
69 | export default class BuilderLabel extends React.Component {
70 | static defaultProps = {
71 | withLink: false,
72 | style: {}
73 | }
74 |
75 | render() {
76 | const { label, children, withLink, style, onClick } = this.props
77 |
78 | if (label === undefined && children === undefined) {
79 | return null
80 | }
81 |
82 | return (
83 |
84 |
87 |
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/edikit/builder/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Block } from './components/Block'
2 | export { default as Builder } from './components/Builder'
3 | export { default as BuilderLabel } from './components/BuilderLabel'
4 |
--------------------------------------------------------------------------------
/src/edikit/button/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Container } from './Button_styled'
3 |
4 | export type ButtonSize = 'normal' | 'large'
5 |
6 | export type ButtonVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger'
7 |
8 | export type ButtonIconPlacement = 'prepend' | 'append'
9 |
10 | export interface IButtonProps {
11 | label?: React.ReactNode,
12 | children?: React.ReactNode,
13 | variant: ButtonVariant
14 | size: ButtonSize
15 | icon?: React.ReactNode
16 | iconPlacement: ButtonIconPlacement
17 | style?: React.CSSProperties
18 | onClick?: any
19 | }
20 |
21 | export default class Button extends React.Component {
22 | static defaultProps = {
23 | variant: 'default',
24 | size: 'normal',
25 | iconPlacement: 'prepend',
26 | }
27 |
28 | render() {
29 | const { label, children, variant, size, icon, iconPlacement, ...rest } = this.props
30 |
31 | let content = null
32 | if (children) {
33 | content = children
34 | } else if (label) {
35 | content = label
36 | }
37 |
38 | return (
39 |
47 | {icon && iconPlacement === 'prepend' && icon}
48 | {label || children}
49 | {icon && iconPlacement === 'append' && icon}
50 |
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/edikit/button/Button_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export interface IContainerProps {
4 | hasIcon: boolean
5 | hasContent: boolean
6 | size: 'normal' | 'large'
7 | variant: 'default' | 'primary' | 'success' | 'warning' | 'danger'
8 | }
9 |
10 | export const Container = styled.span`
11 | display: inline-flex;
12 | align-items: center;
13 | white-space: nowrap;
14 | user-select: none;
15 | cursor: pointer;
16 | border: 1px solid transparent;
17 | border-radius: 2px;
18 | font-weight: 500;
19 | transition: background 200ms;
20 | justify-content: ${props => (props.hasIcon && props.hasContent ? 'space-between' : 'center')};
21 | font-size: ${props => {
22 | if (props.size === 'large') return '16px'
23 | return '13px'
24 | }};
25 | padding: ${props => {
26 | if (props.size === 'large') return '3px 12px'
27 | return '2px 11px'
28 | }};
29 | color: ${props => {
30 | if (props.variant === 'primary') {
31 | return props.theme.colors.overAccent
32 | }
33 | if (props.variant === 'success') {
34 | return props.theme.colors.overSuccess
35 | }
36 | if (props.variant === 'warning') {
37 | return props.theme.colors.overWarning
38 | }
39 | if (props.variant === 'danger') {
40 | return props.theme.colors.overDanger
41 | }
42 |
43 | return props.theme.colors.text
44 | }};
45 | background: ${props => {
46 | if (props.variant === 'primary') {
47 | return props.theme.colors.accent
48 | }
49 | if (props.variant === 'success') {
50 | return props.theme.colors.success
51 | }
52 | if (props.variant === 'warning') {
53 | return props.theme.colors.warning
54 | }
55 | if (props.variant === 'danger') {
56 | return props.theme.colors.danger
57 | }
58 |
59 | return props.theme.colors.background
60 | }};
61 | &:focus {
62 | outline: 0;
63 | }
64 | `
65 |
--------------------------------------------------------------------------------
/src/edikit/button/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Button } from './Button'
--------------------------------------------------------------------------------
/src/edikit/form/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const Node = styled.input`
5 | width: 100%;
6 | display: inline-block;
7 | padding: 2px 12px;
8 | height: 30px;
9 | font-size: 13px;
10 | border: none;
11 | cursor: pointer;
12 | transition: all 200ms ease-in;
13 | background: ${props => props.theme.form.input.background};
14 | color: ${props => props.theme.form.input.color};
15 | border: ${props => props.theme.form.input.border};
16 | ${props => props.theme.form.input.css}
17 |
18 | &::-webkit-input-placeholder {
19 | font-size: 13px;
20 | color: ${props => props.theme.form.input.placeholder.color};
21 | }
22 |
23 | &:hover {
24 | background: ${props => props.theme.form.input.hover.background};
25 | color: ${props => props.theme.form.input.hover.color};
26 | border: ${props => props.theme.form.input.hover.border};
27 | ${props => props.theme.form.input.hover.css}
28 | }
29 |
30 | &:focus {
31 | cursor: auto;
32 | outline: transparent;
33 | background: ${props => props.theme.form.input.focus.background};
34 | color: ${props => props.theme.form.input.focus.color};
35 | border: ${props => props.theme.form.input.focus.border};
36 | ${props => props.theme.form.input.focus.css}
37 | }
38 | `
39 |
40 | interface InputProps extends React.InputHTMLAttributes {
41 |
42 | }
43 |
44 | export default class Input extends React.Component {
45 | render() {
46 | return (
47 |
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/edikit/form/components/Select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { ChevronUp, ChevronDown } from 'react-feather'
4 |
5 | const Container = styled.div`
6 | height: 30px;
7 | border: 1px solid ${props => props.theme.form.select.borderColor || props.theme.form.select.background || 'transparent'};
8 | border-radius: 3px;
9 | display: inline-flex;
10 | position: relative;
11 | transition: all 200ms ease-in;
12 | background: ${props => props.theme.form.select.background || 'transparent'};
13 | color: ${props => props.theme.form.select.color || 'inherit'};
14 | ${props => props.theme.form.select.css};
15 |
16 | &:focus-within {
17 | ${props => props.theme.form.select.focus.css};
18 | }
19 | `
20 |
21 | const Node = styled.select`
22 | flex: 1;
23 | background: transparent;
24 | border: none;
25 | font-size: 13px;
26 | height: 28px;
27 | padding: 0 29px 0 9px;
28 | color: inherit;
29 | -webkit-appearance: none;
30 | -moz-appearance: none;
31 |
32 | &:focus {
33 | outline: transparent;
34 | }
35 | `
36 |
37 | const Addon = styled.div`
38 | width: 20px;
39 | pointer-events: none;
40 | position: absolute;
41 | top: 0;
42 | bottom: 0;
43 | right: 0;
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | justify-content: center;
48 | border-radius: 0 2px 2px 0;
49 | color: ${props => props.theme.form.select.arrowsColor || 'inherit'};
50 | background: ${props => props.theme.form.select.addonBackground || 'transparent'};
51 | `
52 |
53 | interface InputProps extends React.InputHTMLAttributes {}
54 |
55 | export default class Select extends React.Component {
56 | render() {
57 | const { style, ...selectProps } = this.props
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/edikit/form/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Input } from './components/Input'
2 | export { default as Select } from './components/Select'
3 |
4 |
--------------------------------------------------------------------------------
/src/edikit/index.ts:
--------------------------------------------------------------------------------
1 | export * from './builder'
2 | export * from './button'
3 | export * from './form'
4 | export * from './panes'
5 | export * from './theming'
6 | export * from './tree'
7 | export * from './notifications'
8 | export { default as uuid } from './util/uuid'
--------------------------------------------------------------------------------
/src/edikit/notifications/components/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { INotification } from '../types'
4 | import NotificationsItem from './NotificationsItem'
5 |
6 | const Wrapper = styled.div`
7 | position: absolute;
8 | z-index: 100000;
9 | width: 360px;
10 | bottom: 32px;
11 | left: 32px;
12 | `
13 |
14 | interface INotificationsProps {
15 | notifications: INotification[]
16 | }
17 |
18 | export default class Notifications extends React.Component {
19 | render() {
20 | const { notifications } = this.props
21 |
22 | return (
23 |
24 | {notifications.map(notification => (
25 |
29 | ))}
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/edikit/notifications/components/NotificationsItem.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { IThemeColors } from '../../theming/types'
4 | import { INotification, NotificationType } from '../types'
5 |
6 | const colorByNotificationType = (colors: IThemeColors, type: NotificationType): string => {
7 | return colors[type] || colors.accent
8 | }
9 |
10 | interface IItemProps {
11 | type: NotificationType
12 | }
13 |
14 | const Item = styled.div`
15 | position: relative;
16 | margin-top: 9px;
17 | font-size: 13px;
18 | border-radius: 1px;
19 | padding: 7px 12px 7px 15px;
20 | background: ${props => props.theme.notifications.item.background || 'transparent'};
21 | color: ${props => props.theme.notifications.item.color || 'inherit'};
22 | ${props => props.theme.notifications.item.css}
23 |
24 | &:before {
25 | content: "";
26 | position: absolute;
27 | width: 3px;
28 | top: 0;
29 | left: 0;
30 | bottom: 0;
31 | background: ${props => colorByNotificationType(props.theme.colors, props.type)};
32 | }
33 | `
34 |
35 | interface INotificationsItemProps {
36 | notification: INotification
37 | }
38 |
39 | export default class NotificationsItem extends React.Component {
40 | render() {
41 | const { notification } = this.props
42 |
43 | return (
44 | -
45 | {notification.content}
46 |
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/edikit/notifications/containers/NotificationsContainer.ts:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import Notifications from '../components/Notifications'
3 | import { INotification } from '../types'
4 | import { INotificationsState } from '../store'
5 |
6 | interface IPropsFromState {
7 | notifications: INotification[]
8 | }
9 |
10 | const mapStateToProps = (
11 | { notifications }: {
12 | notifications: INotificationsState
13 | } & any
14 | ): IPropsFromState => {
15 | return { notifications }
16 | }
17 |
18 | export default connect(
19 | mapStateToProps
20 | )(Notifications)
21 |
--------------------------------------------------------------------------------
/src/edikit/notifications/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Notifications } from './components/Notifications'
2 | export { default as NotificationsContainer } from './containers/NotificationsContainer'
3 | export * from './types'
4 | export * from './store'
--------------------------------------------------------------------------------
/src/edikit/notifications/store/actions.spec.ts:
--------------------------------------------------------------------------------
1 | import { INotification } from '../types'
2 | import { NotificationsActionTypes } from './types'
3 | import {
4 | triggerNotification,
5 | closeNotification,
6 | } from './actions'
7 |
8 | describe('triggerNotification', () => {
9 | it('should return corresponding action', () => {
10 | const notification: Pick = {
11 | type: 'default',
12 | content: 'test notification'
13 | }
14 | const action = triggerNotification(notification)
15 | expect(action.type).toBe(NotificationsActionTypes.TRIGGER_NOTIFICATION)
16 | expect(action.payload.notification.id).toBeDefined()
17 | })
18 | })
19 |
20 | describe('closeNotification', () => {
21 | it('should return corresponding action', () => {
22 | expect(closeNotification('notification_id')).toEqual({
23 | type: NotificationsActionTypes.CLOSE_NOTIFICATION,
24 | payload: {
25 | notificationId: 'notification_id',
26 | }
27 | })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/edikit/notifications/store/actions.ts:
--------------------------------------------------------------------------------
1 | import { action, createAction } from 'typesafe-actions'
2 | import uuid from '../../util/uuid'
3 | import { INotification } from '../types'
4 | import { NotificationsActionTypes } from './types'
5 |
6 | export interface ITriggerNotification extends Pick {
7 | id?: string
8 | type?: 'default' | 'success' | 'warning' | 'danger'
9 | }
10 |
11 | export interface ITriggerNotificationAction {
12 | type: NotificationsActionTypes.TRIGGER_NOTIFICATION
13 | payload: {
14 | notification: INotification
15 | }
16 | }
17 |
18 | export const triggerNotification = createAction(
19 | NotificationsActionTypes.TRIGGER_NOTIFICATION,
20 | resolve => (
21 | notification: ITriggerNotification
22 | ) => resolve({
23 | notification: {
24 | id: uuid(),
25 | type: 'default',
26 | ...notification,
27 | },
28 | })
29 | )
30 |
31 | export interface ICloseNotificationAction {
32 | type: NotificationsActionTypes.CLOSE_NOTIFICATION
33 | payload: {
34 | notificationId: string
35 | }
36 | }
37 |
38 | export const closeNotification = (
39 | notificationId: string
40 | ) => action(
41 | NotificationsActionTypes.CLOSE_NOTIFICATION,
42 | {
43 | notificationId,
44 | }
45 | )
46 |
47 | export type NotificationsAction =
48 | | ITriggerNotificationAction
49 | | ICloseNotificationAction
50 |
--------------------------------------------------------------------------------
/src/edikit/notifications/store/epics.ts:
--------------------------------------------------------------------------------
1 | import { Epic, combineEpics } from 'redux-observable'
2 | import { of, EMPTY, from } from 'rxjs'
3 | import { mergeMap, map, delay } from 'rxjs/operators'
4 | import {
5 | closeNotification,
6 | ITriggerNotificationAction,
7 | NotificationsAction,
8 | } from './actions'
9 | import { NotificationsActionTypes } from './types'
10 |
11 | export const triggerNotificationEpic: Epic = action$ =>
12 | action$.ofType(NotificationsActionTypes.TRIGGER_NOTIFICATION)
13 | .pipe(
14 | mergeMap(({ payload: { notification } }: ITriggerNotificationAction) => {
15 | if (notification.ttl === undefined) return EMPTY
16 |
17 | return of(closeNotification(notification.id)).pipe(
18 | delay(notification.ttl)
19 | )
20 | })
21 | )
22 |
23 | export const notificationsEpic = combineEpics(
24 | triggerNotificationEpic
25 | )
26 |
--------------------------------------------------------------------------------
/src/edikit/notifications/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducers'
3 | export * from './epics'
4 | export * from './middlewares'
--------------------------------------------------------------------------------
/src/edikit/notifications/store/middlewares.ts:
--------------------------------------------------------------------------------
1 | import { Store } from 'redux'
2 | import { ITriggerNotification, triggerNotification } from './actions'
3 |
4 | interface IAction {
5 | type: string
6 | meta?: {
7 | notification?: ITriggerNotification
8 | }
9 | }
10 |
11 | export const notificationsMiddleware = (store: Store) => (next: any) => (action: IAction) => {
12 | const result = next(action)
13 | if (action.meta !== undefined && action.meta.notification !== undefined) {
14 | store.dispatch(triggerNotification(action.meta.notification))
15 | }
16 |
17 | return result
18 | }
--------------------------------------------------------------------------------
/src/edikit/notifications/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { INotification } from '../types'
2 | import { NotificationsActionTypes } from './types'
3 | import { NotificationsAction } from './actions'
4 |
5 | export type INotificationsState = INotification[]
6 |
7 | export const notificationsReducer = (
8 | state: INotificationsState = [],
9 | action: NotificationsAction
10 | ): INotificationsState => {
11 | switch (action.type) {
12 | case NotificationsActionTypes.TRIGGER_NOTIFICATION:
13 | return [
14 | ...state,
15 | action.payload.notification,
16 | ]
17 |
18 | case NotificationsActionTypes.CLOSE_NOTIFICATION:
19 | return state.filter(notification =>
20 | notification.id !== action.payload.notificationId
21 | )
22 |
23 | default:
24 | return state
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/src/edikit/notifications/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum NotificationsActionTypes {
2 | TRIGGER_NOTIFICATION = '@@edikit/notifications/TRIGGER_NOTIFICATION',
3 | CLOSE_NOTIFICATION = '@@edikit/notifications/CLOSE_NOTIFICATION',
4 | }
5 |
--------------------------------------------------------------------------------
/src/edikit/notifications/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 |
3 | export type NotificationType = 'default' | 'success' | 'warning' | 'danger'
4 |
5 | export interface INotification {
6 | id: string
7 | type: NotificationType
8 | content: ReactNode
9 | ttl?: number
10 | timer?: NodeJS.Timer
11 | }
12 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/EmptyPane.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Container, Block } from './EmptyPane_styled'
4 | import { ITheme } from '../../theming'
5 |
6 | export interface IEmptyPaneProps {
7 | theme: ITheme
8 | }
9 |
10 | class EmptyPane extends React.Component {
11 | render() {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | export default withTheme(EmptyPane)
21 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/EmptyPane_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | width: 100%;
5 | height: 100%;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | `
10 |
11 | export const Block = styled.div`
12 | display: flex;
13 | align-items: center;
14 | margin-bottom: 120px;
15 | opacity: 0.2;
16 | `
17 |
18 | export const Brand = styled.span`
19 | color: ${props => props.theme.colors.accent};
20 | font-weight: 800;
21 | font-size: 32px;
22 | margin-left: 16px;
23 | `
24 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/Pane.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import SplitPane from 'react-split-pane'
3 | import PaneHeader from './PaneHeader'
4 | import EmptyPane from './EmptyPane'
5 | import { Container, Content } from './Pane_styled'
6 | import { IPane, IPaneContent, IContentType, PaneSplitAxis } from '../types'
7 |
8 | export interface IPaneProps {
9 | pane: IPane
10 | panes: Array>
11 | contentTypes: Array>
12 | setCurrentPane: (
13 | paneId: string
14 | ) => void
15 | setPaneCurrentContent: (
16 | paneId: string,
17 | contentId: string
18 | ) => void
19 | removePaneContent: (
20 | paneId: string,
21 | contentId: string
22 | ) => void
23 | splitPane: (
24 | paneId: string,
25 | axis: PaneSplitAxis
26 | ) => void
27 | }
28 |
29 | export default class Pane extends React.Component> {
30 | setCurrentPane = () => {
31 | const { pane, setCurrentPane } = this.props
32 | setCurrentPane(pane.id)
33 | }
34 |
35 | setCurrentContent = (contentId: string) => {
36 | const { pane, setPaneCurrentContent } = this.props
37 | setPaneCurrentContent(pane.id, contentId)
38 | }
39 |
40 | removeContent = (contentId: string) => {
41 | const { pane, removePaneContent } = this.props
42 | removePaneContent(pane.id, contentId)
43 | }
44 |
45 | closeContent = (contentId: string) => () => {
46 | this.removeContent(contentId)
47 | }
48 |
49 | splitPane = (axis: PaneSplitAxis) => {
50 | const { pane, splitPane } = this.props
51 | splitPane(pane.id, axis)
52 | }
53 |
54 | render() {
55 | const {
56 | pane,
57 | panes,
58 | contentTypes,
59 | setCurrentPane,
60 | setPaneCurrentContent,
61 | removePaneContent,
62 | splitPane,
63 | } = this.props
64 |
65 | if (pane.split === true) {
66 | return (
67 |
68 | {pane.children.map(childPaneId => {
69 | const childPane = panes.find(p => p.id === childPaneId)
70 | if (childPane === undefined) {
71 | throw new Error(`no pane found for id: ${childPaneId}\n${JSON.stringify(pane)}`)
72 | }
73 |
74 | return (
75 |
85 | )
86 | })}
87 |
88 | )
89 | }
90 |
91 | let contents: Array> = []
92 | let content
93 | if (pane !== undefined) {
94 | contents = pane.contents
95 | content = contents.find(c => c.isCurrent)
96 | }
97 |
98 | if (content === undefined) return
99 |
100 | const foundContent = content as IPaneContent
101 | const contentType = contentTypes.find(ct => ct.id === foundContent.type)
102 | if (contentType === undefined) {
103 | throw new Error(`unsupported content type: ${foundContent.type}\n${JSON.stringify(foundContent)}`)
104 | }
105 |
106 | return (
107 |
111 |
118 |
119 | {contentType.renderPane({
120 | content: foundContent,
121 | pane,
122 | extra: {
123 | close: this.closeContent(content.id)
124 | }
125 | })}
126 |
127 |
128 | )
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneHeader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import PaneHeaderButton from './PaneHeaderButton'
3 | import SplitPaneIcon from './SplitPaneIcon'
4 | import { Container, Buttons, SplitButtons } from './PaneHeader_styled'
5 | import { IPane, IContentType, PaneSplitAxis } from '../types'
6 |
7 | export interface IPaneHeaderProps {
8 | contentTypes: Array>
9 | pane: IPane
10 | setCurrentContent: (contentId: string) => void
11 | removeContent: (contentId: string) => void
12 | splitPane: (axis: PaneSplitAxis) => void
13 | }
14 |
15 | export default class PaneHeader extends React.Component> {
16 | splitHorizontally = (e: React.SyntheticEvent) => {
17 | e.stopPropagation()
18 | this.props.splitPane(PaneSplitAxis.Horizontal)
19 | }
20 |
21 | splitVertically = (e: React.SyntheticEvent) => {
22 | e.stopPropagation()
23 | this.props.splitPane(PaneSplitAxis.Vertical)
24 | }
25 |
26 | render() {
27 | const {
28 | pane,
29 | contentTypes,
30 | setCurrentContent,
31 | removeContent,
32 | } = this.props
33 |
34 | return (
35 |
36 |
37 | {pane.contents.map(content => (
38 |
46 | ))}
47 |
48 | {pane.contents.length > 0 && (
49 |
50 |
51 |
52 |
53 | )}
54 |
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneHeaderButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { X as RemoveIcon } from 'react-feather'
3 | import { IContentType, IPane, IPaneContent } from '../types'
4 | import { Container, Label, RemoveButton } from './PaneHeaderButton_styled'
5 |
6 | export interface IPaneHeaderButtonProps {
7 | pane: IPane
8 | contentTypes: Array>
9 | content: IPaneContent
10 | setCurrentContent: (contentId: string) => void
11 | removeContent: (contentId: string) => void
12 | }
13 |
14 | class PaneHeaderButton extends React.Component> {
15 | handleOpen = (e: React.SyntheticEvent) => {
16 | // prevents setCurrentPane
17 | e.stopPropagation()
18 |
19 | const { content, setCurrentContent } = this.props
20 | setCurrentContent(content.id)
21 | }
22 |
23 | handleRemove = (e: React.SyntheticEvent) => {
24 | // prevents setCurrentPane
25 | e.stopPropagation()
26 |
27 | const { content, removeContent } = this.props
28 | removeContent(content.id)
29 | }
30 |
31 | render() {
32 | const { pane, content, contentTypes, removeContent } = this.props
33 |
34 | const contentType = contentTypes.find(ct => ct.id === content.type)
35 | if (contentType === undefined) {
36 | throw new Error(`unsupported content type: ${content.type}\n${JSON.stringify(content)}`)
37 | }
38 |
39 | const renderContext = { content, pane, extra: {
40 | close: () => { removeContent(content.id) }
41 | }}
42 |
43 | const label = contentType.renderButton(renderContext)
44 | const icon = contentType.renderIcon(renderContext)
45 |
46 | return (
47 |
48 | {icon}
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | export default PaneHeaderButton
59 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneHeaderButton_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export interface IContainerProps {
4 | isCurrent: boolean
5 | }
6 |
7 | export const Container = styled.span`
8 | position: relative;
9 | display: flex;
10 | overflow: hidden;
11 | align-items: center;
12 | padding: 0 3px 0 12px;
13 | height: ${props => props.theme.pane.header.height};
14 | cursor: pointer;
15 | user-select: none;
16 | background: ${props =>
17 | props.isCurrent
18 | ? props.theme.pane.header.button.current.background
19 | : props.theme.pane.header.button.background};
20 |
21 | &:after {
22 | content: "";
23 | position: absolute;
24 | height: 2px;
25 | left: 0;
26 | right: 0;
27 | bottom: 0;
28 | background: ${props => props.theme.colors.accent};
29 | opacity: ${props => props.isCurrent ? 1 : 0};
30 | transform: ${props => props.isCurrent ? 'scale3d(1, 1, 1)' : 'scale3d(0, 1, 1)'};
31 | transition: transform 300ms ease-out, opacity 300ms ease-in;
32 | }
33 | `
34 |
35 | export const Label = styled.span`
36 | overflow: hidden;
37 | text-overflow: ellipsis;
38 | white-space: nowrap;
39 | margin-left: 6px;
40 | `
41 |
42 | export const RemoveButton = styled.span`
43 | height: 100%;
44 | display: flex;
45 | align-items: center;
46 | padding: 0 7px;
47 | color: ${props => props.theme.colors.muted};
48 | &:hover {
49 | color: ${props => props.theme.colors.accent};
50 | }
51 | `
52 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneHeader_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.header`
4 | position: absolute;
5 | z-index: 20;
6 | top: 0;
7 | right: 0;
8 | left: 0;
9 | height: ${props => props.theme.pane.header.height};
10 | display: flex;
11 | font-size: 13px;
12 | justify-content: space-between;
13 | background: ${props => props.theme.pane.header.background};
14 | overflow: hidden;
15 | `
16 |
17 | export const Buttons = styled.div`
18 | display: flex;
19 | overflow: hidden;
20 | `
21 |
22 | export const SplitButtons = styled.div`
23 | flex-shrink: 0;
24 | display: flex;
25 | width: calc(${props => props.theme.pane.header.height} * 2);
26 | `
27 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneManager.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | IPane,
4 | IPaneContent,
5 | IContentType,
6 | PaneSplitAxis
7 | } from '../types'
8 | import Pane from './Pane'
9 | import { Container } from './PaneManager_styled'
10 |
11 | export interface IPaneManagerProps {
12 | namespace: string
13 | root?: IPane
14 | panes: Array>
15 | contentTypes: Array>
16 | init: () => void
17 | setCurrentPane: (
18 | paneId: string
19 | ) => void
20 | addContentToCurrentPane: (
21 | content: IPaneContent
22 | ) => void
23 | setPaneCurrentContent: (
24 | paneId: string,
25 | contentId: string
26 | ) => void
27 | removePaneContent: (
28 | paneId: string,
29 | contentId: string
30 | ) => void
31 | splitPane: (
32 | paneId: string,
33 | axis: PaneSplitAxis
34 | ) => void
35 | }
36 |
37 | export default class PaneManager extends React.Component> {
38 | componentDidMount() {
39 | this.props.init()
40 | }
41 |
42 | render() {
43 | const {
44 | root,
45 | panes,
46 | contentTypes,
47 | setCurrentPane,
48 | setPaneCurrentContent,
49 | removePaneContent,
50 | splitPane,
51 | } = this.props
52 |
53 | if (root === undefined) return null
54 |
55 | return (
56 |
57 |
66 |
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/PaneManager_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | position: absolute;
5 | top: calc(${props => props.theme.pane.spacing} / 2);
6 | right: calc(${props => props.theme.pane.spacing} / 2);
7 | bottom: calc(${props => props.theme.pane.spacing} / 2);
8 | left: calc(${props => props.theme.pane.spacing} / 2);
9 | `
10 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/Pane_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | interface IContainerProps {
4 | isCurrent: boolean
5 | }
6 |
7 | export const Container = styled.div`
8 | position: absolute;
9 | top: calc(${props => props.theme.pane.spacing} / 2);
10 | right: calc(${props => props.theme.pane.spacing} / 2);
11 | bottom: calc(${props => props.theme.pane.spacing} / 2);
12 | left: calc(${props => props.theme.pane.spacing} / 2);
13 | ${props => props.theme.pane.css}
14 | ${props => props.isCurrent ? props.theme.pane.current.css : ''}
15 | `
16 |
17 | export const Content = styled.div`
18 | position: absolute;
19 | top: ${props => props.theme.pane.header.height};
20 | right: 0;
21 | bottom: 0;
22 | left: 0;
23 | overflow-y: auto;
24 | overflow-x: hidden;
25 | z-index: 10;
26 | `
27 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/SplitPaneIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Container, Icon, Part, Line } from './SplitPaneIcon_styled'
3 |
4 | export interface ISplitPaneIconProps {
5 | axis: 'horizontal' | 'vertical'
6 | onClick: React.MouseEventHandler
7 | }
8 |
9 | export default class SplitPaneIcon extends React.Component {
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/edikit/panes/components/SplitPaneIcon_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export interface IPartProps {
4 | side: 'left' | 'right'
5 | }
6 |
7 | export const Part = styled.div`
8 | flex-shrink: 0;
9 | width: 5px;
10 | height: 12px;
11 | left: 0;
12 | margin-top: 2px;
13 | border: 1px solid ${props => props.theme.colors.muted};
14 | border-left-width: ${props => (props.side === 'left' ? '1px' : '0')};
15 | border-right-width: ${props => (props.side === 'right' ? '1px' : '0')};
16 | `
17 |
18 | export const Line = styled.span`
19 | flex-shrink: 0;
20 | width: 2px;
21 | height: 16px;
22 | margin: 0 1px;
23 | background: ${props => props.theme.colors.accent};
24 | `
25 |
26 | export interface IAxisProps {
27 | axis: 'horizontal' | 'vertical'
28 | }
29 |
30 | export const Icon = styled.div`
31 | width: 14px;
32 | height: 16px;
33 | display: flex;
34 | overflow: hidden;
35 | transform: ${props => (props.axis === 'horizontal' ? 'rotate(90deg)' : 'none')};
36 | `
37 |
38 | export const Container = styled.div`
39 | flex-shrink: 0;
40 | height: ${props => props.theme.pane.header.height};
41 | width: 26px;
42 | display flex;
43 | align-items: center;
44 | justify-content: center;
45 | cursor: pointer;
46 |
47 | &:hover ${Part} {
48 | background: ${props => props.theme.colors.muted};
49 | }
50 | `
51 |
--------------------------------------------------------------------------------
/src/edikit/panes/createPaneManager.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import PaneManager from './components/PaneManager'
4 | import { actionsFactory, IPanesState } from './store'
5 | import { IPaneContent, IContentType, PaneSplitAxis } from './types'
6 |
7 | export default ({
8 | namespace,
9 | contentTypes,
10 | }: {
11 | namespace: string
12 | contentTypes: Array>
13 | }) => {
14 | const actions = actionsFactory(namespace)
15 |
16 | const mapStateToProps = ({
17 | panes
18 | }: AppState & {
19 | panes: IPanesState
20 | }) => {
21 | const entry = panes[namespace]
22 |
23 | return {
24 | namespace,
25 | root: entry === undefined ? undefined : entry.panes.find(pane => pane.childOf === undefined),
26 | panes: entry === undefined ? [] : entry.panes,
27 | contentTypes
28 | }
29 | }
30 |
31 | const mapDispatchToProps = (dispatch: Dispatch) => ({
32 | init: () => {
33 | dispatch(actions.initPanesNamespace())
34 | },
35 | setCurrentPane: (paneId: string) => {
36 | dispatch(actions.setCurrentPane(paneId))
37 | },
38 | addContentToCurrentPane: (content: IPaneContent) => {
39 | dispatch(actions.addContentToCurrentPane(content))
40 | },
41 | setPaneCurrentContent: (paneId: string, contentId: string) => {
42 | dispatch(actions.setPaneCurrentContent(paneId, contentId))
43 | },
44 | removePaneContent: (paneId: string, contentId: string) => {
45 | dispatch(actions.removePaneContent(paneId, contentId))
46 | },
47 | removeContentFromAllPanes: (contentId: string) => {
48 | dispatch(actions.removeContentFromAllPanes(contentId))
49 | },
50 | splitPane: (paneId: string, axis: PaneSplitAxis) => {
51 | dispatch(actions.splitPane(paneId, axis))
52 | },
53 | })
54 |
55 | class NameSpacedPaneManager extends PaneManager {}
56 |
57 | return connect(
58 | mapStateToProps,
59 | mapDispatchToProps
60 | )(NameSpacedPaneManager)
61 | }
--------------------------------------------------------------------------------
/src/edikit/panes/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './store'
3 | export { default as createPaneManager } from './createPaneManager'
--------------------------------------------------------------------------------
/src/edikit/panes/store/actions.spec.ts:
--------------------------------------------------------------------------------
1 | import { IPaneContent, PaneSplitAxis } from '../types'
2 | import { PanesActionTypes } from './types'
3 | import {
4 | initPanesNamespaceAction,
5 | setCurrentPaneAction,
6 | addContentToCurrentPaneAction,
7 | setPaneCurrentContentAction,
8 | removePaneContentAction,
9 | splitPaneAction,
10 | } from './actions'
11 |
12 | describe('initPanesNamespaceAction', () => {
13 | it('should return corresponding action', () => {
14 | expect(initPanesNamespaceAction('namespace')).toEqual({
15 | type: PanesActionTypes.INIT_PANES_NAMESPACE,
16 | payload: {
17 | namespace: 'namespace',
18 | }
19 | })
20 | })
21 | })
22 |
23 | describe('setCurrentPaneAction', () => {
24 | it('should return corresponding action', () => {
25 | expect(setCurrentPaneAction('namespace', 'pane_id')).toEqual({
26 | type: PanesActionTypes.SET_CURRENT_PANE,
27 | payload: {
28 | namespace: 'namespace',
29 | paneId: 'pane_id',
30 | }
31 | })
32 | })
33 | })
34 |
35 | describe('addContentToCurrentPaneAction', () => {
36 | it('should return corresponding action', () => {
37 | const content: IPaneContent = {
38 | id: 'content_id',
39 | type: 'test',
40 | isCurrent: false,
41 | isUnique: false,
42 | }
43 | expect(addContentToCurrentPaneAction('namespace', content)).toEqual({
44 | type: PanesActionTypes.ADD_CONTENT_TO_CURRENT_PANE,
45 | payload: {
46 | namespace: 'namespace',
47 | content
48 | }
49 | })
50 | })
51 | })
52 |
53 | describe('removePaneContentAction', () => {
54 | it('should return corresponding action', () => {
55 | expect(removePaneContentAction('namespace', 'pane_id', 'content_id')).toEqual({
56 | type: PanesActionTypes.REMOVE_PANE_CONTENT,
57 | payload: {
58 | namespace: 'namespace',
59 | paneId: 'pane_id',
60 | contentId: 'content_id',
61 | }
62 | })
63 | })
64 | })
65 |
66 | describe('setPaneCurrentContentAction', () => {
67 | it('should return corresponding action', () => {
68 | expect(setPaneCurrentContentAction('namespace', 'pane_id', 'content_id')).toEqual({
69 | type: PanesActionTypes.SET_PANE_CURRENT_CONTENT,
70 | payload: {
71 | namespace: 'namespace',
72 | paneId: 'pane_id',
73 | contentId: 'content_id',
74 | }
75 | })
76 | })
77 | })
78 |
79 | describe('splitPaneAction', () => {
80 | it('should return corresponding action', () => {
81 | expect(splitPaneAction('namespace', 'pane_id', PaneSplitAxis.Horizontal)).toEqual({
82 | type: PanesActionTypes.SPLIT_PANE,
83 | payload: {
84 | namespace: 'namespace',
85 | paneId: 'pane_id',
86 | axis: PaneSplitAxis.Horizontal,
87 | }
88 | })
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/src/edikit/panes/store/actions.ts:
--------------------------------------------------------------------------------
1 | import { action } from 'typesafe-actions'
2 | import { PanesActionTypes } from './types'
3 | import { IPaneContent, PaneSplitAxis } from '../types'
4 |
5 | export interface IInitPanesNamespaceAction {
6 | type: PanesActionTypes.INIT_PANES_NAMESPACE
7 | payload: {
8 | namespace: string
9 | }
10 | }
11 |
12 | export const initPanesNamespaceAction = (
13 | namespace: string,
14 | ) => action(
15 | PanesActionTypes.INIT_PANES_NAMESPACE,
16 | {
17 | namespace,
18 | }
19 | )
20 |
21 | export interface ISetCurrentPaneAction {
22 | type: PanesActionTypes.SET_CURRENT_PANE
23 | payload: {
24 | namespace: string
25 | paneId: string
26 | }
27 | }
28 |
29 | export const setCurrentPaneAction = (
30 | namespace: string,
31 | paneId: string
32 | ) => action(
33 | PanesActionTypes.SET_CURRENT_PANE,
34 | {
35 | namespace,
36 | paneId,
37 | }
38 | )
39 |
40 | export interface IAddContentToCurrentPaneAction {
41 | type: PanesActionTypes.ADD_CONTENT_TO_CURRENT_PANE
42 | payload: {
43 | namespace: string
44 | content: IPaneContent
45 | }
46 | }
47 |
48 | export const addContentToCurrentPaneAction = (
49 | namespace: string,
50 | content: IPaneContent
51 | ) => action(
52 | PanesActionTypes.ADD_CONTENT_TO_CURRENT_PANE,
53 | {
54 | namespace,
55 | content,
56 | }
57 | )
58 |
59 | export interface ISetPaneCurrentContentAction {
60 | type: PanesActionTypes.SET_PANE_CURRENT_CONTENT
61 | payload: {
62 | namespace: string
63 | paneId: string
64 | contentId: string
65 | }
66 | }
67 |
68 | export const setPaneCurrentContentAction = (
69 | namespace: string,
70 | paneId: string,
71 | contentId: string
72 | ) => action(
73 | PanesActionTypes.SET_PANE_CURRENT_CONTENT,
74 | {
75 | namespace,
76 | paneId,
77 | contentId,
78 | }
79 | )
80 |
81 | export interface IRemovePaneContentAction {
82 | type: PanesActionTypes.REMOVE_PANE_CONTENT
83 | payload: {
84 | namespace: string
85 | paneId: string
86 | contentId: string
87 | }
88 | }
89 |
90 | export const removePaneContentAction = (
91 | namespace: string,
92 | paneId: string,
93 | contentId: string
94 | ) => action(
95 | PanesActionTypes.REMOVE_PANE_CONTENT,
96 | {
97 | namespace,
98 | paneId,
99 | contentId,
100 | }
101 | )
102 |
103 | export interface IRemoveContentFromAllPanesAction {
104 | type: PanesActionTypes.REMOVE_CONTENT_FROM_ALL_PANES
105 | payload: {
106 | namespace: string
107 | contentId: string
108 | }
109 | }
110 |
111 | export const removeContentFromAllPanesAction = (
112 | namespace: string,
113 | contentId: string
114 | ) => action(
115 | PanesActionTypes.REMOVE_CONTENT_FROM_ALL_PANES,
116 | {
117 | namespace,
118 | contentId,
119 | }
120 | )
121 |
122 | export interface ISplitPaneAction {
123 | type: PanesActionTypes.SPLIT_PANE
124 | payload: {
125 | namespace: string
126 | paneId: string
127 | axis: PaneSplitAxis
128 | }
129 | }
130 |
131 | export const splitPaneAction = (
132 | namespace: string,
133 | paneId: string,
134 | axis: PaneSplitAxis
135 | ) => action(
136 | PanesActionTypes.SPLIT_PANE,
137 | {
138 | namespace,
139 | paneId,
140 | axis,
141 | }
142 | )
143 |
144 | export type PanesAction =
145 | | IInitPanesNamespaceAction
146 | | ISetCurrentPaneAction
147 | | IAddContentToCurrentPaneAction
148 | | ISetPaneCurrentContentAction
149 | | IRemovePaneContentAction
150 | | IRemoveContentFromAllPanesAction
151 | | ISplitPaneAction
152 |
153 | export interface IPanesActions {
154 | initPanesNamespace: () => IInitPanesNamespaceAction
155 | setCurrentPane: (
156 | paneId: string,
157 | ) => ISetCurrentPaneAction
158 | addContentToCurrentPane: (
159 | content: IPaneContent
160 | ) => IAddContentToCurrentPaneAction
161 | setPaneCurrentContent: (
162 | paneId: string,
163 | contentId: string
164 | ) => ISetPaneCurrentContentAction
165 | removePaneContent: (
166 | paneId: string,
167 | contentId: string
168 | ) => IRemovePaneContentAction
169 | removeContentFromAllPanes: (
170 | contentId: string
171 | ) => IRemoveContentFromAllPanesAction
172 | splitPane: (
173 | paneId: string,
174 | axis: PaneSplitAxis
175 | ) => ISplitPaneAction
176 | }
177 |
178 | export const actionsFactory = (namespace: string): IPanesActions => ({
179 | initPanesNamespace: () => action(
180 | PanesActionTypes.INIT_PANES_NAMESPACE,
181 | {
182 | namespace
183 | }
184 | ),
185 | setCurrentPane: (
186 | paneId: string
187 | ) => setCurrentPaneAction(
188 | namespace,
189 | paneId
190 | ),
191 | addContentToCurrentPane: (
192 | content: IPaneContent
193 | ) => addContentToCurrentPaneAction(
194 | namespace,
195 | content
196 | ),
197 | setPaneCurrentContent: (
198 | paneId: string,
199 | contentId: string
200 | ) => setPaneCurrentContentAction(
201 | namespace,
202 | paneId,
203 | contentId
204 | ),
205 | removePaneContent: (
206 | paneId: string,
207 | contentId: string
208 | ) => removePaneContentAction(
209 | namespace,
210 | paneId,
211 | contentId
212 | ),
213 | removeContentFromAllPanes: (
214 | contentId: string
215 | ) => removeContentFromAllPanesAction(
216 | namespace,
217 | contentId
218 | ),
219 | splitPane: (
220 | paneId: string,
221 | axis: PaneSplitAxis
222 | ) => splitPaneAction(
223 | namespace,
224 | paneId,
225 | axis
226 | ),
227 | })
228 |
--------------------------------------------------------------------------------
/src/edikit/panes/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducers'
3 | export * from './selectors'
--------------------------------------------------------------------------------
/src/edikit/panes/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { IPane, PaneSplitAxis } from '../types'
2 | import { PanesActionTypes } from './types'
3 | import { PanesAction } from './actions'
4 | import {
5 | setCurrentPane,
6 | addContentToCurrentPane,
7 | setPaneCurrentContent,
8 | removePaneContent,
9 | removeContentFromAllPanes,
10 | splitPane,
11 | } from '../operations'
12 |
13 | export interface IPanesNamespaceState {
14 | panes: Array>
15 | }
16 |
17 | export interface IPanesState {
18 | [namespace: string]: IPanesNamespaceState
19 | }
20 |
21 | const byNamespace = (
22 | state: IPanesNamespaceState = {
23 | panes: []
24 | },
25 | action: PanesAction
26 | ): IPanesNamespaceState => {
27 | switch (action.type) {
28 | case PanesActionTypes.INIT_PANES_NAMESPACE:
29 | return {
30 | panes: [{
31 | id: action.payload.namespace,
32 | isCurrent: true,
33 | split: false,
34 | splitAxis: PaneSplitAxis.Horizontal,
35 | contents: [],
36 | children: [],
37 | }],
38 | }
39 |
40 | case PanesActionTypes.SET_CURRENT_PANE:
41 | return {
42 | panes: setCurrentPane(
43 | state.panes,
44 | action.payload.paneId
45 | ),
46 | }
47 |
48 | case PanesActionTypes.ADD_CONTENT_TO_CURRENT_PANE:
49 | return {
50 | panes: addContentToCurrentPane(
51 | state.panes,
52 | action.payload.content
53 | ),
54 | }
55 |
56 | case PanesActionTypes.SET_PANE_CURRENT_CONTENT:
57 | return {
58 | panes: setPaneCurrentContent(
59 | state.panes,
60 | action.payload.paneId,
61 | action.payload.contentId
62 | ),
63 | }
64 |
65 | case PanesActionTypes.REMOVE_PANE_CONTENT:
66 | return {
67 | panes: removePaneContent(
68 | state.panes,
69 | action.payload.paneId,
70 | action.payload.contentId
71 | ),
72 | }
73 |
74 | case PanesActionTypes.REMOVE_CONTENT_FROM_ALL_PANES:
75 | return {
76 | panes: removeContentFromAllPanes(
77 | state.panes,
78 | action.payload.contentId
79 | ),
80 | }
81 |
82 | case PanesActionTypes.SPLIT_PANE:
83 | return {
84 | panes: splitPane(
85 | state.panes,
86 | action.payload.paneId,
87 | action.payload.axis
88 | ),
89 | }
90 |
91 | default:
92 | return state
93 | }
94 | }
95 |
96 | export const panesReducer = () => (
97 | state: IPanesState = {},
98 | action: PanesAction
99 | ): IPanesState => {
100 | switch (action.type) {
101 | case PanesActionTypes.INIT_PANES_NAMESPACE:
102 | case PanesActionTypes.SET_CURRENT_PANE:
103 | case PanesActionTypes.ADD_CONTENT_TO_CURRENT_PANE:
104 | case PanesActionTypes.SET_PANE_CURRENT_CONTENT:
105 | case PanesActionTypes.REMOVE_PANE_CONTENT:
106 | case PanesActionTypes.REMOVE_CONTENT_FROM_ALL_PANES:
107 | case PanesActionTypes.SPLIT_PANE:
108 | return {
109 | ...state,
110 | [action.payload.namespace]: byNamespace(
111 | state[action.payload.namespace],
112 | action
113 | )
114 | }
115 |
116 | default:
117 | return state
118 | }
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/src/edikit/panes/store/selectors.ts:
--------------------------------------------------------------------------------
1 | import { IPane, IPaneContent } from '../types'
2 | import { IPanesState, IPanesNamespaceState } from './reducers'
3 |
4 | /**
5 | * Select panes state for a given namespace.
6 | */
7 | export const panesNamespaceSelector = (
8 | state: IPanesState,
9 | namespace: string
10 | ): IPanesNamespaceState | void => state[namespace]
11 |
12 | /**
13 | * Select current pane in pane collection.
14 | */
15 | export const panesNsCurrentPaneSelector = (
16 | panes: Array>
17 | ): IPane | void => panes.find(pane => pane.isCurrent)
18 |
19 | /**
20 | * Select current pane for a given namespace.
21 | */
22 | export const panesCurrentPaneSelector = (
23 | state: IPanesState,
24 | namespace: string
25 | ): IPane | void => {
26 | const namespaceState = panesNamespaceSelector(state, namespace)
27 | if (namespaceState === undefined) return
28 |
29 | return panesNsCurrentPaneSelector(namespaceState.panes)
30 | }
31 |
32 | /**
33 | * Select a pane by its id in pane collection.
34 | */
35 | export const panesNsPaneSelector = (
36 | panes: Array>,
37 | paneId: string
38 | ): IPane | void => panes.find(pane => pane.id === paneId)
39 |
40 | /**
41 | * Select a pane by its id for a given namespace.
42 | */
43 | export const panesPaneSelector = (
44 | state: IPanesState,
45 | namespace: string,
46 | paneId: string
47 | ): IPane | void => {
48 | const namespaceState = panesNamespaceSelector(state, namespace)
49 | if (namespaceState === undefined) return
50 |
51 | return panesNsPaneSelector(namespaceState.panes, paneId)
52 | }
53 |
54 | export interface IPanesContentSelection {
55 | pane: IPane
56 | content: IPaneContent
57 | }
58 |
59 | /**
60 | * Select pane & content for a given content id in pane collection.
61 | */
62 | export const panesNsContentSelector = (
63 | panes: Array>,
64 | contentId: string
65 | ): IPanesContentSelection | void => {
66 | let selection: IPanesContentSelection | void
67 | panes.forEach(pane => {
68 | const content = pane.contents.find(c => c.id === contentId)
69 | if (content !== undefined) {
70 | selection = { pane, content }
71 | }
72 | })
73 |
74 | return selection!
75 | }
76 |
77 | /**
78 | * Select pane & content for a given content id in a specific namespace.
79 | */
80 | export const panesContentSelector = (
81 | state: IPanesState,
82 | namespace: string,
83 | contentId: string
84 | ): IPanesContentSelection | void => {
85 | const namespaceState = panesNamespaceSelector(state, namespace)
86 | if (namespaceState === undefined) return
87 |
88 | return panesNsContentSelector(namespaceState.panes, contentId)
89 | }
90 |
91 | /**
92 | * Select all current contents in pane collection.
93 | */
94 | export const panesNsCurrentContentsSelector = (
95 | panes: Array>
96 | ): Array> => panes.reduce((
97 | agg: Array>,
98 | pane: IPane
99 | ) => {
100 | const currentContent = pane.contents.find(content => content.isCurrent)
101 | if (currentContent === undefined) return agg
102 |
103 | const doesExist = agg.find(content => content.id === currentContent.id)
104 | if (doesExist) return agg
105 |
106 | return [...agg, currentContent]
107 | }, [])
108 |
109 |
110 | /**
111 | * Select all current contents in a specific namespace.
112 | */
113 | export const panesCurrentContentsSelector = (
114 | state: IPanesState,
115 | namespace: string,
116 | ): Array> => {
117 | const namespaceState = panesNamespaceSelector(state, namespace)
118 | if (namespaceState === undefined) return []
119 |
120 | return panesNsCurrentContentsSelector(namespaceState.panes)
121 | }
--------------------------------------------------------------------------------
/src/edikit/panes/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum PanesActionTypes {
2 | INIT_PANES_NAMESPACE = '@@edikit/panes/INIT_PANES_NAMESPACE',
3 | SET_CURRENT_PANE = '@@edikit/panes/SET_CURRENT_PANE',
4 | ADD_CONTENT_TO_CURRENT_PANE = '@@edikit/panes/ADD_CONTENT_TO_CURRENT_PANE',
5 | SET_PANE_CURRENT_CONTENT = '@@edikit/panes/SET_PANE_CURRENT_CONTENT',
6 | REMOVE_PANE_CONTENT = '@@edikit/panes/REMOVE_PANE_CONTENT',
7 | REMOVE_CONTENT_FROM_ALL_PANES = '@@edikit/panes/REMOVE_CONTENT_FROM_ALL_PANES',
8 | SPLIT_PANE = '@@edikit/panes/SPLIT_PANE',
9 | }
10 |
--------------------------------------------------------------------------------
/src/edikit/panes/types.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export enum PaneSplitAxis {
4 | Horizontal = 'horizontal',
5 | Vertical = 'vertical',
6 | }
7 |
8 | export interface IPaneContent {
9 | id: string
10 | type: string
11 | isCurrent: boolean
12 | isUnique: boolean
13 | data?: Data
14 | }
15 |
16 | export interface IPane {
17 | id: string
18 | isCurrent: boolean
19 | split: boolean
20 | splitAxis?: PaneSplitAxis
21 | childOf?: string
22 | children: string[]
23 | contents: Array>
24 | }
25 |
26 | export interface IContentRenderContext {
27 | content: IPaneContent
28 | pane: IPane
29 | extra: {
30 | close: () => void
31 | }
32 | }
33 |
34 | export interface IContentType {
35 | id: string
36 | renderButton: (context: IContentRenderContext) => React.ReactNode
37 | renderIcon: (context: IContentRenderContext) => React.ReactNode
38 | renderPane: (context: IContentRenderContext) => React.ReactNode
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/src/edikit/theming/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './themes'
--------------------------------------------------------------------------------
/src/edikit/theming/themes/black.ts:
--------------------------------------------------------------------------------
1 | import 'brace/theme/chaos'
2 | import { ITheme, IThemeColors, IThemeTypography } from '../types'
3 | import {css} from "styled-components";
4 |
5 | const typography: IThemeTypography = {
6 | fontSize: '14px',
7 | lineHeight: '1.6em',
8 | default: {
9 | fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'`,
10 | },
11 | mono: {
12 | fontFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`,
13 | },
14 | }
15 |
16 | const colors: IThemeColors = {
17 | background: '#111',
18 | text: '#ddd',
19 | border: '#222',
20 | accent: '#803aff',
21 | overAccent: '#e6e9f5',
22 | muted: '#666',
23 | success: '#00ff57',
24 | overSuccess: '#000',
25 | warning: '#ffb400',
26 | overWarning: '#000',
27 | danger: '#b32c00',
28 | overDanger: '#000',
29 | }
30 |
31 | const theme: ITheme = {
32 | typography,
33 | colors,
34 | tree: {
35 | container: {
36 | background: '#000',
37 | },
38 | item: {
39 | color: colors.text,
40 | hover: {
41 | background: '#111111',
42 | color: '#fff',
43 | },
44 | current: {
45 | background: '#191225',
46 | color: '#fff',
47 | },
48 | },
49 | },
50 | builder: {
51 | spacing: '12px',
52 | background: '#111',
53 | header: {
54 | background: '#000',
55 | title: {
56 | color: '#fff'
57 | },
58 | subtitle: {
59 | color: colors.muted
60 | }
61 | },
62 | link: {
63 | color: '#612fc1'
64 | },
65 | label: {
66 | background: colors.accent,
67 | color: '#fff'
68 | },
69 | block: {
70 | background: '#000',
71 | css: css`
72 | border: 2px solid #111;
73 | `,
74 | title: {
75 |
76 | }
77 | },
78 | },
79 | badge: {
80 | background: '#222',
81 | color: '#fff',
82 | },
83 | header: {
84 | height: '44px',
85 | background: '#000',
86 | iconColor: '#955fff',
87 | },
88 | pane: {
89 | spacing: '14px',
90 | css: css`
91 | transition: box-shadow 300ms;
92 | box-shadow: 0 0 0 1px rgba(128, 58, 255, 0), 0 0 0 1px rgba(128, 58, 255, 0);
93 | `,
94 | current: {
95 | css: css`
96 | box-shadow: 0 0 0 5px rgba(128, 58, 255, .08), 0 0 0 1px rgba(128, 58, 255, .15);
97 | `,
98 | },
99 | header: {
100 | height: '34px',
101 | background: '#111',
102 | button: {
103 | background: '#111',
104 | current: {
105 | background: '#000',
106 | },
107 | },
108 | },
109 | body: {
110 | background: '#000'
111 | }
112 | },
113 | editor: {
114 | theme: 'chaos',
115 | },
116 | form: {
117 | input: {
118 | background: '#111',
119 | color: colors.text,
120 | css: css`
121 | border-radius: 2px;
122 | box-shadow: 0 0 0 0 rgba(128, 58, 255, 0);
123 | `,
124 | placeholder: {
125 | color: '#444',
126 | },
127 | hover: {
128 | background: '#222',
129 | },
130 | focus: {
131 | background: '#000',
132 | css: css`
133 | box-shadow: 0 0 0 2px rgba(128, 58, 255, .5);
134 | `,
135 | },
136 | disabled: {},
137 | },
138 | select: {
139 | background: '#111',
140 | color: '#fff',
141 | addonBackground: 'rgba(128, 58, 255, .25)',
142 | arrowsColor: '#9660ff',
143 | borderColor: '#000',
144 | css: css`
145 | &:hover {
146 | background: #222;
147 | }
148 | `,
149 | focus: {
150 | css: css`
151 | background: #000;
152 | box-shadow: 0 0 0 2px rgba(128, 58, 255, .5);
153 | `,
154 | },
155 | },
156 | },
157 | notifications: {
158 | item: {
159 | background: '#222',
160 | color: '#fff',
161 | },
162 | },
163 | }
164 |
165 | export default theme
166 |
--------------------------------------------------------------------------------
/src/edikit/theming/themes/index.js:
--------------------------------------------------------------------------------
1 | export { default as blackTheme } from './black'
2 | export { default as solarizedDarkTheme } from './solarizedDark'
3 | export { default as whiteTheme } from './white'
--------------------------------------------------------------------------------
/src/edikit/theming/themes/solarizedDark.ts:
--------------------------------------------------------------------------------
1 | import 'brace'
2 | import 'brace/theme/solarized_dark'
3 | import { css } from 'styled-components'
4 | import { IThemeTypography, IThemeColors, ITheme } from '../types'
5 |
6 | const typography: IThemeTypography = {
7 | fontSize: '14px',
8 | lineHeight: '1.6em',
9 | default: {
10 | fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'`,
11 | },
12 | mono: {
13 | fontFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`,
14 | },
15 | }
16 |
17 | const colors: IThemeColors = {
18 | background: '#001b21',
19 | text: '#eee',
20 | border: '#001b21',
21 | accent: '#48ceee',
22 | overAccent: '#00222b',
23 | muted: '#357586',
24 | success: '#afd327',
25 | overSuccess: '#00222b',
26 | warning: '#cb8d10',
27 | overWarning: '#00222b',
28 | danger: '#ff4c3f',
29 | overDanger: '#00222b',
30 | }
31 |
32 | const theme: ITheme = {
33 | typography,
34 | colors,
35 | tree: {
36 | container: {
37 | background: '#00171c',
38 | },
39 | item: {
40 | color: colors.text,
41 | hover: {
42 | background: '#012835',
43 | color: '#fff',
44 | },
45 | current: {
46 | background: '#01313f',
47 | color: '#fff',
48 | },
49 | },
50 | },
51 | builder: {
52 | spacing: '12px',
53 | background: '#012833',
54 | header: {
55 | background: '#013340',
56 | title: {
57 | color: '#fff',
58 | },
59 | subtitle: {
60 | color: colors.muted
61 | }
62 | },
63 | link: {
64 | color: '#316878',
65 | },
66 | label: {
67 | background: colors.accent,
68 | color: colors.background
69 | },
70 | block: {
71 | background: '#013340',
72 | css: css`
73 | box-shadow: 0 1px 2px #001b21;
74 | `,
75 | title: {
76 |
77 | }
78 | },
79 | },
80 | badge: {
81 | background: '#001b21',
82 | color: 'inherit',
83 | },
84 | header: {
85 | height: '44px',
86 | background: colors.background,
87 | color: colors.text,
88 | iconColor: colors.accent,
89 | brandColor: colors.text,
90 | },
91 | pane: {
92 | spacing: '12px',
93 | css: css`
94 | transition: box-shadow 300ms;
95 | box-shadow: 0 0 0 0 #00171c;
96 | `,
97 | current: {
98 | css: css`
99 | box-shadow: 0 0 0 3px #001014;
100 | `,
101 | },
102 | header: {
103 | height: '34px',
104 | background: '#001b21',
105 | button: {
106 | background: '#00222b',
107 | current: {
108 | background: '#013340',
109 | },
110 | },
111 | },
112 | body: {
113 | background: '#012833'
114 | }
115 | },
116 | editor: {
117 | theme: 'solarized_dark',
118 | },
119 | form: {
120 | input: {
121 | background: '#01212c',
122 | border: 'none',
123 | color: colors.text,
124 | css: css`
125 | border-radius: 2px;
126 | `,
127 | placeholder: {
128 | color: '#2f5968',
129 | },
130 | hover: {
131 | },
132 | focus: {
133 | background: '#011b25',
134 | },
135 | disabled: {}
136 | },
137 | select: {
138 | background: '#01586a',
139 | color: '#ffffff',
140 | borderColor: '#011e25',
141 | addonBackground: '#014c5e',
142 | css: css`
143 | box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
144 | `,
145 | focus: {
146 |
147 | },
148 | },
149 | },
150 | notifications: {
151 | item: {
152 | background: '#014f60',
153 | css: css`
154 | border: 2px solid #011e25;
155 | `
156 | },
157 | },
158 | }
159 |
160 | export default theme
161 |
--------------------------------------------------------------------------------
/src/edikit/theming/themes/white.ts:
--------------------------------------------------------------------------------
1 | import 'brace/theme/github'
2 | import { css } from 'styled-components'
3 | import { ITheme, IThemeColors, IThemeTypography } from '../types'
4 |
5 | const typography: IThemeTypography = {
6 | fontSize: '14px',
7 | lineHeight: '1.6em',
8 | default: {
9 | fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'`,
10 | },
11 | mono: {
12 | fontFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace`,
13 | },
14 | }
15 |
16 | const colors: IThemeColors = {
17 | background: '#f8f8f8',
18 | text: '#555',
19 | border: '#ddd',
20 | accent: '#2b5fc5',
21 | overAccent: '#e6e9f5',
22 | muted: '#aaa',
23 | success: '#27e227',
24 | overSuccess: '#105b10',
25 | warning: '#ffb400',
26 | overWarning: '#7b3204',
27 | danger: '#e21702',
28 | overDanger: '#fff',
29 | }
30 |
31 | const theme: ITheme = {
32 | typography,
33 | colors,
34 | tree: {
35 | container: {
36 | background: 'transparent',
37 | },
38 | item: {
39 | color: colors.text,
40 | hover: {
41 | background: '#fff',
42 | color: '#333',
43 | },
44 | current: {
45 | background: '#fff',
46 | color: '#000',
47 | },
48 | },
49 | },
50 | builder: {
51 | spacing: '12px',
52 | background: '#f8f8f8',
53 | header: {
54 | background: '#7286fc',
55 | title: {
56 | color: '#fff'
57 | },
58 | subtitle: {
59 | color: 'rgba(255, 255, 255, .65)'
60 | }
61 | },
62 | link: {
63 | color: '#7286fc'
64 | },
65 | label: {
66 | background: colors.accent,
67 | color: '#eee',
68 | css: css`
69 | box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
70 | `,
71 | },
72 | block: {
73 | background: '#fff',
74 | css: css`
75 | box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
76 | `,
77 | title: {
78 | }
79 | },
80 | },
81 | badge: {
82 | background: '#eee',
83 | color: '#000',
84 | },
85 | header: {
86 | height: '44px',
87 | background: '#2b5fc5',
88 | color: '#fff',
89 | iconColor: '#fff',
90 | brandColor: '#fff',
91 | },
92 | pane: {
93 | spacing: '10px',
94 | css: css`
95 | transition: box-shadow 300ms;
96 | box-shadow: 0 0 0 1px rgba(4, 169, 244, 0), 0 0 0 1px rgba(0, 0, 0, .06);
97 | `,
98 | current: {
99 | css: css`
100 | box-shadow: 0 0 0 5px rgba(4, 169, 244, .08), 0 0 0 1px rgba(4, 169, 244, .25);
101 | `,
102 | },
103 | header: {
104 | height: '34px',
105 | background: '#f5f5f5',
106 | button: {
107 | background: '#f5f5f5',
108 | current: {
109 | background: '#fff',
110 | },
111 | },
112 | },
113 | body: {
114 | background: '#fff'
115 | }
116 | },
117 | editor: {
118 | theme: 'github',
119 | },
120 | form: {
121 | input: {
122 | background: '#f5f5f5',
123 | border: '1px solid #eee',
124 | color: colors.text,
125 | css: css`
126 | border-radius: 2px;
127 | `,
128 | placeholder: {
129 | color: '#aaa',
130 | },
131 | hover: {
132 | border: '1px solid #2196f3',
133 | },
134 | focus: {
135 | background: '#fff',
136 | border: '1px solid #2196f3',
137 | css: css`
138 | box-shadow: 0 0 0 3px rgba(4, 169, 244, .2);
139 | `
140 | },
141 | disabled: {}
142 | },
143 | select: {
144 | background: 'white',
145 | arrowsColor: '#2b5fc5',
146 | addonBackground: 'rgba(4, 169, 244, .08)',
147 | borderColor: '#ddd',
148 | color: '#222',
149 | css: css`
150 | box-shadow: 0 1px 1px rgba(0, 0, 0, .06);
151 |
152 | &:hover {
153 | border-color: #2196f3;
154 | box-shadow: 0 0 0 rgba(4, 169, 244, 0);
155 | }
156 | `,
157 | focus: {
158 | css: css`
159 | border-color: #2196f3;
160 | box-shadow: 0 0 0 3px rgba(4, 169, 244, .2);
161 | `
162 | },
163 | },
164 | },
165 | notifications: {
166 | item: {
167 | background: '#444',
168 | color: '#eee',
169 | css: css`
170 | box-shadow: 0 2px 3px rgba(0, 0, 0, .1);
171 | `,
172 | },
173 | },
174 | }
175 |
176 | export default theme
177 |
--------------------------------------------------------------------------------
/src/edikit/theming/types.ts:
--------------------------------------------------------------------------------
1 | export interface IThemeTypography {
2 | fontSize: string
3 | lineHeight: string
4 | default: {
5 | fontFamily: string
6 | }
7 | mono: {
8 | fontFamily: string
9 | }
10 | }
11 |
12 | export interface IThemeColors {
13 | background: string
14 | text: string
15 | border: string
16 | accent: string
17 | overAccent: string
18 | muted: string
19 | success: string
20 | overSuccess: string
21 | warning: string
22 | overWarning: string
23 | danger: string
24 | overDanger: string
25 | }
26 |
27 | export interface IThemeBadge {
28 | background: string
29 | color: string
30 | }
31 |
32 | export interface IThemeHeader {
33 | height: number | string
34 | background?: string
35 | color?: string
36 | iconColor?: string
37 | brandColor?: string
38 | }
39 |
40 | export interface IThemePane {
41 | spacing: number | string
42 | css?: any
43 | current: {
44 | css?: any
45 | }
46 | header: {
47 | height: number | string
48 | background: string
49 | button: {
50 | background: string
51 | current: {
52 | background: string
53 | }
54 | }
55 | }
56 | body: {
57 | background: string
58 | css?: any
59 | }
60 | }
61 |
62 | export interface IThemeTree {
63 | container: {
64 | background: string
65 | }
66 | item: {
67 | color: string
68 | hover: {
69 | background: string
70 | color: string
71 | }
72 | current: {
73 | background: string
74 | color: string
75 | }
76 | }
77 | }
78 |
79 | export interface IThemeBuilder {
80 | spacing: number | string
81 | background?: string
82 | css?: any
83 | header: {
84 | background: string
85 | css?: any
86 | title: {
87 | color?: string
88 | css?: any
89 | },
90 | subtitle: {
91 | color?: string
92 | css?: any
93 | }
94 | }
95 | link: {
96 | color: string
97 | }
98 | label: {
99 | background: string
100 | color: string
101 | css?: any
102 | }
103 | block: {
104 | background: string
105 | css?: any
106 | title: {
107 | color?: string
108 | css?: any
109 | }
110 | }
111 | }
112 |
113 | export interface IThemeForm {
114 | input: {
115 | background?: string
116 | color?: string
117 | border?: string
118 | css?: any
119 | placeholder: {
120 | color?: string
121 | }
122 | hover: {
123 | background?: string
124 | color?: string
125 | border?: string
126 | css?: any
127 | }
128 | focus: {
129 | background?: string
130 | color?: string
131 | border?: string
132 | css?: any
133 | }
134 | disabled: {
135 | background?: string
136 | color?: string
137 | border?: string
138 | css?: any
139 | }
140 | }
141 | select: {
142 | borderColor?: string
143 | background?: string
144 | color?: string
145 | addonBackground?: string
146 | arrowsColor?: string
147 | css?: any
148 | focus: {
149 | css?: any
150 | }
151 | }
152 | }
153 |
154 | export interface IThemeNotifications {
155 | item: {
156 | background?: string
157 | color?: string
158 | css?: any
159 | }
160 | }
161 |
162 | export interface IThemeEditor {
163 | theme: string
164 | }
165 |
166 | export interface ITheme {
167 | typography: IThemeTypography
168 | colors: IThemeColors
169 | badge: IThemeBadge
170 | header: IThemeHeader
171 | pane: IThemePane
172 | tree: IThemeTree
173 | builder: IThemeBuilder
174 | editor: IThemeEditor
175 | form: IThemeForm
176 | notifications: IThemeNotifications
177 | }
178 |
--------------------------------------------------------------------------------
/src/edikit/tree/components/Tree.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Container } from './Tree_styled'
3 | import TreeNode from './TreeNode'
4 | import { ITreeNode as Node, TreeClickHandler, TreeIconGetter } from '../'
5 |
6 | export interface ITreeProps {
7 | root: Node
8 | onClick: TreeClickHandler,
9 | getIcon?: TreeIconGetter
10 | openedIds: string[]
11 | }
12 |
13 | export interface ITreeState {
14 | current?: Node
15 | }
16 |
17 | export default class Tree extends React.Component, ITreeState> {
18 | render() {
19 | const {
20 | root,
21 | openedIds,
22 | onClick,
23 | getIcon
24 | } = this.props
25 |
26 | return (
27 |
28 |
35 |
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/edikit/tree/components/TreeNode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Folder, ChevronRight, ChevronDown } from 'react-feather'
4 | import { Icons, Item, CurrentIndicator } from './TreeNode_styled'
5 | import { ITreeNode as Node, TreeClickHandler, TreeIconGetter } from '../'
6 |
7 | const iconSize = 12
8 |
9 | export interface ITreeNodeProps {
10 | node: Node
11 | openedIds: string[]
12 | depth: number
13 | theme: any
14 | onClick: TreeClickHandler
15 | getIcon?: TreeIconGetter
16 | }
17 |
18 | class TreeNode extends React.Component> {
19 | static defaultProps = {
20 | depth: 0,
21 | }
22 |
23 | handleClick = () => {
24 | const { onClick, node } = this.props
25 | onClick(node)
26 | }
27 |
28 | render() {
29 | const {
30 | node,
31 | openedIds,
32 | depth,
33 | onClick,
34 | getIcon,
35 | theme,
36 | } = this.props
37 |
38 | let icon = node.icon
39 | if (icon === undefined && getIcon !== undefined) {
40 | icon = getIcon(node)
41 | }
42 | if (icon === undefined && node.children !== undefined) {
43 | icon = (
44 |
49 | )
50 | }
51 |
52 | const isOpened = openedIds.includes(node.id)
53 | const iconCount = node.children !== undefined ? 2 : 1
54 |
55 | return (
56 |
57 | - 0)}
59 | isCurrent={node.isCurrent}
60 | depth={depth}
61 | onClick={this.handleClick}
62 | >
63 |
64 | {node.children !== undefined && isOpened && (
65 |
66 | )}
67 | {node.children !== undefined && !isOpened && (
68 |
69 | )}
70 | {icon}
71 |
72 |
73 | {node.label}
74 |
75 | {node.isCurrent === true && }
76 |
77 | {node.children && node.children.length > 0 && isOpened && (
78 |
79 | {node.children.map(child => (
80 |
88 | ))}
89 |
90 | )}
91 |
92 | )
93 | }
94 | }
95 |
96 | const ThemedTreeNode = withTheme(TreeNode)
97 |
98 | export default ThemedTreeNode
99 |
--------------------------------------------------------------------------------
/src/edikit/tree/components/TreeNode_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const iconSize = 12
4 | const iconSpacing = 9
5 | const iconsOffset = iconSize + iconSpacing
6 |
7 | interface IconsProps {
8 | iconCount: number
9 | }
10 |
11 | export const Icons = styled.div`
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 | width: ${props => props.iconCount * iconSize + (props.iconCount - 1) * iconSpacing}px;
16 | margin-right: ${iconSpacing}px;
17 | flex-shrink: 0;
18 | `
19 |
20 | export interface ItemProps {
21 | isDir: boolean
22 | depth: number
23 | isCurrent?: boolean
24 | }
25 |
26 | export const Item = styled.div`
27 | display: flex;
28 | align-items: center;
29 | padding: 3px 12px 3px 7px;
30 | font-size: 13px;
31 | font-weight: 600;
32 | transition: background 300ms;
33 | padding-left: ${props => {
34 | if (props.isDir) {
35 | return props.depth * iconsOffset + 7
36 | }
37 |
38 | return props.depth * iconsOffset + 7
39 | }}px;
40 | cursor: pointer;
41 | white-space: nowrap;
42 | user-select: none;
43 | color: ${props =>
44 | props.isCurrent
45 | ? props.theme.tree.item.current.color
46 | : props.theme.tree.item.color};
47 | background: ${props =>
48 | props.isCurrent ? props.theme.tree.item.current.background : 'transparent'};
49 |
50 | &:hover {
51 | background: ${props => props.theme.tree.item.hover.background};
52 | color: ${props => props.theme.tree.item.hover.color};
53 | ${props => props.theme.tree.item.hover.extend};
54 | }
55 | `
56 |
57 | export const CurrentIndicator = styled.span`
58 | width: 6px;
59 | height: 6px;
60 | border-radius: 3px;
61 | margin-left: ${iconSpacing}px;
62 | background: ${props => props.theme.colors.accent};
63 | `
--------------------------------------------------------------------------------
/src/edikit/tree/components/Tree_styled.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.aside`
4 | overflow: auto;
5 | width: 100%;
6 | height: 100%;
7 | background: ${props => props.theme.tree.container.background};
8 | ${props => props.theme.tree.container.extend};
9 | `
10 |
--------------------------------------------------------------------------------
/src/edikit/tree/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Tree } from './components/Tree'
2 | export * from './lib'
3 | export * from './types'
--------------------------------------------------------------------------------
/src/edikit/tree/lib.ts:
--------------------------------------------------------------------------------
1 | import { set } from 'lodash'
2 | import { ITreeNode } from './'
3 |
4 | export function walkTree (
5 | node: ITreeNode,
6 | predicate: (node: ITreeNode) => boolean,
7 | fn: (node: ITreeNode) => ITreeNode,
8 | path: string = '',
9 | accc?: ITreeNode
10 | ): ITreeNode {
11 | const acc = accc || { ...node }
12 |
13 | let newNode = node
14 | if (predicate(node)) {
15 | newNode = fn(node)
16 | if (path === '') return newNode
17 | }
18 |
19 | set(acc, path === '' ? '.' : path, newNode)
20 |
21 | if (node.children !== undefined) {
22 | node.children.forEach((child, i: number) => {
23 | walkTree(child, predicate, fn, `${path}children[${i}]`, acc)
24 | })
25 | }
26 |
27 | return acc
28 | }
29 |
--------------------------------------------------------------------------------
/src/edikit/tree/types.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export interface ITreeNode {
4 | id: string
5 | type: string
6 | label: string
7 | icon?: React.ReactNode
8 | isCurrent?: boolean
9 | data?: Data
10 | children?: Array>
11 | }
12 |
13 | export type TreeIconGetter = (node: ITreeNode) => React.ReactNode
14 |
15 | export type TreeClickHandler = (node: ITreeNode) => void
16 |
--------------------------------------------------------------------------------
/src/edikit/util/uuid.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @see https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
3 | */
4 | const s4 = (): string =>
5 | Math.floor((1 + Math.random()) * 0x10000)
6 | .toString(16)
7 | .substring(1)
8 |
9 | export default (): string => `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`
10 |
--------------------------------------------------------------------------------
/src/editorConfig.ts:
--------------------------------------------------------------------------------
1 | import 'brace'
2 | import 'brace/mode/json'
3 |
--------------------------------------------------------------------------------
/src/globalStyles.ts:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components'
2 |
3 | export default injectGlobal`
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | overflow: hidden;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | html, body, #root {
13 | width: 100%;
14 | height: 100%;
15 | margin: 0;
16 | }
17 |
18 | #root {
19 | position: fixed;
20 | }
21 |
22 | * {
23 | box-sizing: border-box;
24 | }
25 |
26 | .Resizer {
27 | background: red;
28 | opacity: 0;
29 | z-index: 1;
30 | -moz-box-sizing: border-box;
31 | -webkit-box-sizing: border-box;
32 | box-sizing: border-box;
33 | -moz-background-clip: padding;
34 | -webkit-background-clip: padding;
35 | background-clip: padding-box;
36 | }
37 |
38 | .Resizer:hover {
39 | -webkit-transition: all 2s ease;
40 | transition: all 2s ease;
41 | }
42 |
43 | .Resizer.horizontal {
44 | height: 11px;
45 | margin: -5px 0;
46 | border-top: 5px solid rgba(255, 255, 255, 0);
47 | border-bottom: 5px solid rgba(255, 255, 255, 0);
48 | cursor: row-resize;
49 | width: 100%;
50 | }
51 |
52 | .Resizer.horizontal:hover {
53 | border-top: 5px solid rgba(0, 0, 0, 0.5);
54 | border-bottom: 5px solid rgba(0, 0, 0, 0.5);
55 | }
56 |
57 | .Resizer.vertical {
58 | width: 11px;
59 | margin: 0 -5px;
60 | border-left: 5px solid rgba(255, 255, 255, 0);
61 | border-right: 5px solid rgba(255, 255, 255, 0);
62 | cursor: col-resize;
63 | }
64 |
65 | .Resizer.vertical:hover {
66 | border-left: 5px solid rgba(0, 0, 0, 0.5);
67 | border-right: 5px solid rgba(0, 0, 0, 0.5);
68 | }
69 | .Resizer.disabled {
70 | cursor: not-allowed;
71 | }
72 | .Resizer.disabled:hover {
73 | border-color: transparent;
74 | }
75 | `
76 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import './globalStyles'
4 | import './editorConfig'
5 | import Root from './Root'
6 | import configureStore from './configureStore'
7 |
8 | const store = configureStore()
9 |
10 | ReactDOM.render(
11 | ,
12 | document.getElementById('root') as HTMLElement
13 | )
14 |
--------------------------------------------------------------------------------
/src/lib/status.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/wiremock-ui/af76868257ec7e3bd77770f614f6d5ed09052c55/src/lib/status.ts
--------------------------------------------------------------------------------
/src/modules/core/components/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import SplitPane from 'react-split-pane'
3 | import { ThemeProvider } from 'styled-components'
4 | import { createPaneManager, IPaneContent, NotificationsContainer } from 'edikit'
5 | import themes from '../../../themes'
6 | import { ISettings, settingsContentTypes } from '../../settings'
7 | import { serversContentTypes } from '../../servers'
8 | import { mappingsContentTypes } from '../../mappings'
9 | import ExplorerContainer from '../containers/ExplorerContainer'
10 | import AppBar from './AppBar'
11 | import { Container, Inner } from './App_styled'
12 | import { IApplicationState } from '../../../store'
13 | import { IData } from '../../../types'
14 |
15 | const PaneManager = createPaneManager({
16 | namespace: 'default',
17 | contentTypes: [
18 | ...settingsContentTypes,
19 | ...serversContentTypes,
20 | ...mappingsContentTypes,
21 | ],
22 | })
23 |
24 | export interface IAppProps {
25 | loadState: () => void
26 | hasBeenInitialized: boolean
27 | settings: ISettings,
28 | addContentToCurrentPane(content: IPaneContent): void
29 | }
30 |
31 | export default class App extends React.Component {
32 | componentDidMount() {
33 | this.props.loadState()
34 | }
35 |
36 | render() {
37 | const {
38 | hasBeenInitialized,
39 | settings,
40 | addContentToCurrentPane,
41 | } = this.props
42 |
43 | if (!hasBeenInitialized) return null
44 |
45 | return (
46 |
47 |
48 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/modules/core/components/AppBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Monitor, Settings, Github } from 'react-feather'
4 | import { ITheme, Button, IPaneContent } from 'edikit'
5 | import { IData } from '../../../types'
6 | import { Container, Icons, AppName } from './AppBar_styled'
7 |
8 | export interface IAppBarProps {
9 | addContentToCurrentPane(content: IPaneContent): void
10 | theme: ITheme
11 | }
12 |
13 | class AppBar extends React.Component {
14 | openSettings = () => {
15 | this.props.addContentToCurrentPane({
16 | id: 'settings',
17 | type: 'settings',
18 | isCurrent: true,
19 | isUnique: true,
20 | })
21 | }
22 |
23 | visitGithub = () => {
24 | window.open('https://github.com/plouc/wiremock-ui', '_blank')
25 | }
26 |
27 | render() {
28 | const { theme } = this.props
29 |
30 | return (
31 |
32 |
33 |
38 | wiremock:UI
39 |
40 |
41 |
49 | }
50 | onClick={this.openSettings}
51 | >
52 | settings
53 |
54 |
61 | }
62 | onClick={this.visitGithub}
63 | >
64 | GitHub
65 |
66 |
67 |
68 | )
69 | }
70 | }
71 |
72 | export default withTheme(AppBar)
73 |
--------------------------------------------------------------------------------
/src/modules/core/components/AppBar_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | height: ${props => props.theme.header.height};
5 | padding: 0 12px 0 26px;
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | background: ${props => props.theme.header.background};
10 | color: ${props => props.theme.header.color};
11 | `
12 |
13 | export const Icons = styled.div`
14 | display: flex;
15 | align-items: center;
16 | height: 100%;
17 | `
18 |
19 | export const AppName = styled.span`
20 | font-weight: 900;
21 | font-size: 14px;
22 | display: flex;
23 | align-items: center;
24 | color: ${props => props.theme.header.brandColor};
25 | `
26 |
--------------------------------------------------------------------------------
/src/modules/core/components/App_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | width: 100%;
5 | height: 100%;
6 | position: relative;
7 | background: ${props => props.theme.colors.background};
8 | color: ${props => props.theme.colors.text};
9 | font-family: ${props => props.theme.typography.default.fontFamily};
10 | font-size: ${props => props.theme.typography.fontSize};
11 | line-height: ${props => props.theme.typography.lineHeight};
12 | `
13 |
14 | export const Inner = styled.div`
15 | position: absolute;
16 | left: 0;
17 | top: ${props => props.theme.header.height};
18 | right: 0;
19 | bottom: 0;
20 | `
21 |
--------------------------------------------------------------------------------
/src/modules/core/components/Explorer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Server as ServerIcon, PlusCircle } from 'react-feather'
4 | import {IPaneContent, ITheme} from 'edikit'
5 | import { Tree, ITreeNode } from './Tree'
6 | import { IData } from '../../../types'
7 | import { IServer } from '../../servers'
8 | import MapingIcon from '../../mappings/components/MappingIcon'
9 |
10 | export interface IExplorerProps {
11 | tree: ITreeNode
12 | servers: IServer[]
13 | loadServerMappings(server: IServer): void
14 | addContentToCurrentPane(content: IPaneContent): void
15 | theme: ITheme
16 | }
17 |
18 | export interface IExplorerState {
19 | openedIds: string[]
20 | }
21 |
22 | class Explorer extends React.Component {
23 | constructor(props: IExplorerProps) {
24 | super(props)
25 |
26 | this.state = {
27 | openedIds: ['root'],
28 | }
29 | }
30 |
31 | getTreeNodeIcon = (node: ITreeNode): React.ReactNode => {
32 | const { theme } = this.props
33 | if (node.type === 'server') {
34 | return
35 | }
36 |
37 | if (node.type === 'server.create' || node.type === 'mapping.create') {
38 | return
39 | }
40 |
41 | if (node.type === 'mapping') {
42 | return
43 | }
44 |
45 | return
46 | }
47 |
48 | handleNodeClick = (node: ITreeNode) => {
49 | const {
50 | loadServerMappings,
51 | servers,
52 | addContentToCurrentPane,
53 | } = this.props
54 |
55 | if (node.type === 'server.create') {
56 | addContentToCurrentPane({
57 | id: 'server.create',
58 | type: 'server.create',
59 | isCurrent: true,
60 | isUnique: true,
61 | })
62 | }
63 |
64 | if (node.type === 'mappings' && node.data !== undefined) {
65 | const server = servers.find(s => s.name === node.data!.serverName)
66 | if (server !== undefined) {
67 | loadServerMappings(server)
68 | }
69 | }
70 |
71 | if (node.type === 'mapping' && node.data !== undefined) {
72 | const server = servers.find(s => s.name === node.data!.serverName)
73 | if (server !== undefined) {
74 | addContentToCurrentPane({
75 | id: node.id,
76 | type: 'mapping',
77 | isCurrent: true,
78 | isUnique: false,
79 | data: {
80 | serverName: server.name,
81 | mappingId: node.data.mappingId,
82 | },
83 | })
84 | }
85 | }
86 |
87 | if (node.type === 'server.create') {
88 | addContentToCurrentPane({
89 | id: 'server.create',
90 | type: 'server.create',
91 | isCurrent: true,
92 | isUnique: true,
93 | })
94 | }
95 |
96 | if (node.type === 'mapping.create' && node.data !== undefined) {
97 | addContentToCurrentPane({
98 | id: node.id,
99 | type: 'mapping.create',
100 | isCurrent: true,
101 | isUnique: false,
102 | data: {
103 | serverName: node.data.serverName,
104 | creationId: node.data.creationId,
105 | },
106 | })
107 | }
108 |
109 | let openedIds
110 | if (this.state.openedIds.includes(node.id)) {
111 | openedIds = this.state.openedIds.filter(id => id !== node.id)
112 | } else {
113 | openedIds = [...this.state.openedIds, node.id]
114 | }
115 |
116 | this.setState({
117 | openedIds
118 | })
119 | }
120 |
121 | render() {
122 | const { tree } = this.props
123 | const { openedIds } = this.state
124 |
125 | return (
126 |
132 | )
133 | }
134 | }
135 |
136 | export default withTheme(Explorer)
137 |
--------------------------------------------------------------------------------
/src/modules/core/components/StatusIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Slash, Check, X } from 'react-feather'
4 | import { ITheme, IThemeColors } from 'edikit'
5 |
6 | const mapping = {
7 | 'ok': 'success',
8 | 'ko': 'danger',
9 | 'warn': 'warning',
10 | 'none': 'muted',
11 | }
12 |
13 | export const getColorForStatus = (colors: IThemeColors, status?: 'ok' | 'ko' | 'warn' | 'none') => {
14 | if (status === undefined || status === null) {
15 | return colors.accent
16 | }
17 |
18 | return colors[mapping[status]]
19 | }
20 |
21 |
22 | export interface IStatusIconProps {
23 | size: number | string
24 | status?: 'ok' | 'ko' | 'warn' | 'none'
25 | style?: React.CSSProperties
26 | theme: ITheme
27 | }
28 |
29 | class StatusIcon extends React.Component {
30 | public static defaultProps = {
31 | style: {},
32 | }
33 |
34 | public render() {
35 | const { size, status, style, theme } = this.props
36 |
37 | const color = getColorForStatus(theme.colors, status)
38 | const props = { size, color, style }
39 |
40 | if (status === undefined || status === null) {
41 | return
42 | }
43 |
44 | if (status === 'ok') return
45 | if (status === 'ko') return
46 |
47 | return
48 | }
49 | }
50 |
51 | export default withTheme(StatusIcon)
52 |
--------------------------------------------------------------------------------
/src/modules/core/components/Tree.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tree as BaseTree,
3 | ITreeNode as BaseTreeNode,
4 | TreeClickHandler as BaseTreeClickHandler,
5 | TreeIconGetter as BaseTreeIconGetter,
6 | } from 'edikit'
7 | import { IData } from '../../../types'
8 |
9 | export interface ITreeNode extends BaseTreeNode {}
10 |
11 | export interface ITreeClickHandler extends BaseTreeClickHandler {}
12 |
13 | export interface ITreeIconGetter extends BaseTreeIconGetter {}
14 |
15 | export class Tree extends BaseTree {}
16 |
--------------------------------------------------------------------------------
/src/modules/core/containers/AppContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Dispatch } from 'redux'
3 | import { connect } from 'react-redux'
4 | import {
5 | addContentToCurrentPaneAction,
6 | IPaneContent,
7 | } from 'edikit'
8 | import { IApplicationState } from '../../../store'
9 | import { IData } from '../../../types'
10 | import { loadState } from '../store'
11 | import App from '../components/App'
12 |
13 | const mapStateToProps = ({
14 | core: { hasBeenInitialized },
15 | settings: { settings },
16 | }: IApplicationState) => ({
17 | hasBeenInitialized,
18 | settings,
19 | })
20 |
21 | const mapDispatchToProps = (dispatch: Dispatch) => ({
22 | loadState: () => {
23 | dispatch(loadState())
24 | },
25 | addContentToCurrentPane: (content: IPaneContent) => {
26 | dispatch(addContentToCurrentPaneAction(
27 | 'default',
28 | content
29 | ))
30 | },
31 | })
32 |
33 | export default connect(
34 | mapStateToProps,
35 | mapDispatchToProps
36 | )(App)
37 |
--------------------------------------------------------------------------------
/src/modules/core/containers/ExplorerContainer.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import { panesCurrentContentsSelector, uuid } from 'edikit'
4 | import { ITreeNode } from '../components/Tree'
5 | import { IApplicationState } from '../../../store'
6 | import { loadServerMappings, getMappingUrl } from '../../mappings'
7 | import { IServer } from '../../servers'
8 | import Explorer from '../components/Explorer'
9 |
10 | const mapStateToProps = (
11 | {
12 | panes,
13 | servers: { servers },
14 | mappings: serversMappings
15 | }: IApplicationState
16 | ): {
17 | tree: ITreeNode
18 | servers: IServer[]
19 | } => {
20 | const currentContentIds: string[] = panesCurrentContentsSelector(panes, 'default')
21 | .map(({ id }) => id)
22 |
23 | const tree: ITreeNode = {
24 | id: 'root',
25 | type: 'root',
26 | label: 'servers',
27 | children: []
28 | }
29 |
30 | servers.forEach(server => {
31 | const serverNode = {
32 | id: server.name,
33 | label: server.name,
34 | type: 'server',
35 | children: [] as ITreeNode[],
36 | }
37 |
38 | const mappings = serversMappings[server.name]
39 | if (mappings !== undefined) {
40 | const creationId = uuid()
41 | serverNode.children.push({
42 | id: `${server.name}.mapping.create.${creationId}`,
43 | type: 'mapping.create',
44 | label: 'create mapping',
45 | data: {
46 | serverName: server.name,
47 | creationId,
48 | },
49 | })
50 |
51 | const mappingsNode: ITreeNode = {
52 | id: `${server.name}.mappings`,
53 | type: 'mappings',
54 | label: 'mappings',
55 | data: {
56 | serverName: server.name,
57 | },
58 | children: [],
59 | }
60 | mappings.ids.forEach(mappingId => {
61 | const mapping = mappings.byId[mappingId].mapping
62 | if (mapping !== undefined) {
63 | mappingsNode.children!.push({
64 | id: mappingId,
65 | type: 'mapping',
66 | label: mapping.name || `${mapping.request.method} ${getMappingUrl(mapping)}`,
67 | isCurrent: currentContentIds.includes(mappingId),
68 | data: {
69 | serverName: server.name,
70 | mappingId,
71 | },
72 | })
73 | }
74 | })
75 | serverNode.children.push(mappingsNode)
76 | }
77 |
78 | tree.children!.push(serverNode)
79 | })
80 |
81 | tree.children!.push({
82 | id: 'server.create',
83 | type: 'server.create',
84 | label: 'create server',
85 | })
86 |
87 | return { tree, servers }
88 | }
89 |
90 | const mapDispatchToProps = (dispatch: Dispatch) => ({
91 | loadServerMappings: (server: IServer) => {
92 | dispatch(loadServerMappings(server))
93 | },
94 | })
95 |
96 | export default connect(
97 | mapStateToProps,
98 | mapDispatchToProps
99 | )(Explorer)
100 |
--------------------------------------------------------------------------------
/src/modules/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './store'
2 | export * from './components/Tree'
--------------------------------------------------------------------------------
/src/modules/core/store/actions.ts:
--------------------------------------------------------------------------------
1 | import { action } from 'typesafe-actions'
2 | import { IAction } from '../../../store'
3 | import { CoreActionTypes } from './types'
4 |
5 | export const loadState = () => action(CoreActionTypes.LOAD_STATE)
6 |
7 | export const loadStateFinished = () => action(CoreActionTypes.LOAD_STATE_FINISHED)
8 |
9 | export type CoreAction =
10 | | IAction
11 |
--------------------------------------------------------------------------------
/src/modules/core/store/epics.ts:
--------------------------------------------------------------------------------
1 | import { Epic, combineEpics } from 'redux-observable'
2 | import { from } from 'rxjs'
3 | import { mergeMap } from 'rxjs/operators'
4 | import { initSettings } from '../../settings'
5 | import { initServers } from '../../servers'
6 | import { IAction } from '../../../store'
7 | import { loadState, loadStateFinished } from './actions'
8 | import { CoreActionTypes } from './types'
9 |
10 | export const loadStateEpic: Epic = action$ =>
11 | action$.ofType(CoreActionTypes.LOAD_STATE)
12 | .pipe(
13 | mergeMap((action: typeof loadState) => {
14 | const theme = localStorage.getItem('theme') || 'solarized dark'
15 | const actions: IAction[] = [initSettings({
16 | theme
17 | })]
18 |
19 | let servers: any = localStorage.getItem('servers')
20 | if (servers) {
21 | servers = JSON.parse(servers)
22 | actions.push(initServers(servers))
23 | }
24 |
25 | return from([
26 | ...actions,
27 | loadStateFinished()
28 | ])
29 | })
30 | )
31 |
32 | export const coreEpic = combineEpics(
33 | loadStateEpic
34 | )
35 |
--------------------------------------------------------------------------------
/src/modules/core/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './epics'
3 | export * from './reducers'
--------------------------------------------------------------------------------
/src/modules/core/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { CoreActionTypes } from './types'
2 | import { CoreAction } from './actions'
3 |
4 | export interface ICoreState {
5 | hasBeenInitialized: boolean
6 | }
7 |
8 | const initialState = {
9 | hasBeenInitialized: false,
10 | }
11 |
12 | export const coreReducer = (
13 | state: ICoreState = initialState,
14 | action: CoreAction
15 | ): ICoreState => {
16 | switch (action.type) {
17 | case CoreActionTypes.LOAD_STATE_FINISHED:
18 | return {
19 | ...state,
20 | hasBeenInitialized: true
21 | }
22 |
23 | default:
24 | return state
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/src/modules/core/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum CoreActionTypes {
2 | LOAD_STATE = '@@core/LOAD_STATE',
3 | LOAD_STATE_FINISHED = '@@core/LOAD_STATE_FINISHED',
4 | }
5 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/CreateMapping.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { IMapping } from '../types'
3 | import MappingJsonEditor from './MappingJsonEditor'
4 | import MappingBuilder from './MappingBuilder'
5 | import { Wrapper, Overlay } from './Mapping_styled'
6 |
7 | interface ICreateMappingProps {
8 | serverName: string
9 | creationId: string
10 | mapping?: IMapping
11 | isCreating: boolean
12 | init(): void
13 | save(mapping: IMapping): void
14 | cancel(): void
15 | }
16 |
17 | interface ICreateMappingState {
18 | mode: 'builder' | 'json'
19 | }
20 |
21 | export default class CreateMapping extends React.Component {
22 | constructor(props: ICreateMappingProps) {
23 | super(props)
24 |
25 | this.state = {
26 | mode: 'builder',
27 | }
28 | }
29 |
30 | componentDidMount() {
31 | this.props.init()
32 | }
33 |
34 | setBuilderMode = () => {
35 | this.setState({ mode: 'builder' })
36 | }
37 |
38 | setJsonMode = () => {
39 | this.setState({ mode: 'json' })
40 | }
41 |
42 | render() {
43 | const {
44 | creationId,
45 | mapping,
46 | isCreating,
47 | save,
48 | } = this.props
49 |
50 | if (mapping === undefined) return null
51 |
52 | const { mode } = this.state
53 |
54 | return (
55 |
56 | {mode === 'builder' && (
57 |
65 | )}
66 | {mode === 'json' && (
67 |
75 | )}
76 | {isCreating && }
77 |
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/Mapping.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { IServer } from '../../servers'
3 | import { IMapping } from '../types'
4 | import MappingJsonEditor from './MappingJsonEditor'
5 | import MappingBuilder from './MappingBuilder'
6 | import { Wrapper, Overlay } from './Mapping_styled'
7 |
8 | interface IMappingProps {
9 | serverName: string
10 | mappingId: string
11 | server?: IServer
12 | mapping?: IMapping
13 | isLoading: boolean
14 | fetchMapping(): void
15 | initWorkingCopy(): void
16 | syncWorkingCopy(update: Partial): void
17 | updateMapping(update: IMapping): void
18 | deleteMapping(): void
19 | }
20 |
21 | interface IMappingState {
22 | mode: 'builder' | 'json'
23 | }
24 |
25 | export default class Mapping extends React.Component {
26 | constructor(props: IMappingProps) {
27 | super(props)
28 |
29 | this.state = {
30 | mode: 'builder'
31 | }
32 | }
33 |
34 | componentDidMount() {
35 | this.props.initWorkingCopy()
36 | }
37 |
38 | componentDidUpdate(prevProps: IMappingProps) {
39 | if (this.props.mappingId !== prevProps.mappingId) {
40 | this.props.initWorkingCopy()
41 | }
42 | }
43 |
44 | setBuilderMode = () => {
45 | this.setState({ mode: 'builder' })
46 | }
47 |
48 | setJsonMode = () => {
49 | this.setState({ mode: 'json' })
50 | }
51 |
52 | render() {
53 | const {
54 | mapping,
55 | isLoading,
56 | syncWorkingCopy,
57 | updateMapping,
58 | deleteMapping,
59 | } = this.props
60 | const { mode } = this.state
61 |
62 | if (mapping === undefined) return null
63 |
64 | return (
65 |
66 | {mode === 'builder' && (
67 |
77 | )}
78 | {mode === 'json' && (
79 |
89 | )}
90 | {isLoading && }
91 |
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/MappingBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Button } from 'edikit'
3 | import styled from 'styled-components'
4 | import {
5 | Save as SaveIcon,
6 | Code as JsonModeIcon,
7 | Grid as BuilderModeIcon,
8 | Trash2 as DeleteIcon,
9 | // AlertOctagon as HasErrorIcon,
10 | // X as CancelIcon,
11 | // Copy as CloneIcon,
12 | } from 'react-feather'
13 |
14 | const Container = styled.div`
15 | padding: 12px 16px;
16 | display: flex;
17 | justify-content: space-between;
18 | align-items: center;
19 | background: ${props => props.theme.builder.block.background};
20 | `
21 |
22 | const ButtonsWrapper = styled.div`
23 | display: flex;
24 | align-items: center;
25 | `
26 |
27 | const actionButtonStyle = {
28 | fontSize: '11px',
29 | lineHeight: '1.6em',
30 | height: '32px',
31 | width: '32px',
32 | alignItems: 'center',
33 | padding: 0,
34 | justifyContent: 'center',
35 | marginLeft: '6px',
36 | }
37 |
38 | interface IMappingBarProps {
39 | mode: 'builder' | 'json'
40 | setBuilderMode(): void
41 | setJsonMode(): void
42 | save?: () => void
43 | deleteMapping?: () => void
44 | }
45 |
46 | export default class MappingBar extends React.Component {
47 | render() {
48 | const {
49 | mode,
50 | setBuilderMode,
51 | setJsonMode,
52 | save,
53 | deleteMapping,
54 | } = this.props
55 |
56 | return (
57 |
58 |
59 | }
68 | >
69 | builder
70 |
71 | }
81 | >
82 | json
83 |
84 |
85 |
86 | {save !== undefined && (
87 |
115 |
116 | )
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/MappingBuilder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { InjectedFormikProps, withFormik } from 'formik'
3 | import { Builder, Block, Input } from 'edikit'
4 | import { IMapping, IMappingFormValues } from '../types'
5 | import { mappingValidationSchema } from '../validation'
6 | import { mappingToFormValues, mappingFormValuesToMapping } from '../dto'
7 | import { Container, Content } from './Mapping_styled'
8 | import MappingBar from './MappingBar'
9 | import { Grid } from './builder/Builder_styled'
10 | import BuilderRequest from './builder/BuilderRequest'
11 | import BuilderResponse from './builder/BuilderResponse'
12 |
13 | interface IMappingBuilderProps {
14 | mapping: IMapping
15 | isLoading: boolean
16 | sync?: (values: IMapping) => void
17 | save(values: IMapping): void
18 | deleteMapping?: () => void
19 | mode: 'builder' | 'json'
20 | setBuilderMode(): void
21 | setJsonMode(): void
22 | }
23 |
24 | interface IMappingBuilderState {
25 | isRequestOpened: boolean
26 | isResponseOpened: boolean
27 | requestParamsType: 'query' | 'headers' | 'cookies' | 'body'
28 | }
29 |
30 | const enhance = withFormik({
31 | enableReinitialize: true,
32 | isInitialValid: true,
33 | mapPropsToValues: props => {
34 | return mappingToFormValues(props.mapping)
35 | },
36 | validationSchema: mappingValidationSchema,
37 | handleSubmit: (values, { props }) => {
38 | props.save(mappingFormValuesToMapping(values))
39 | }
40 | })
41 |
42 | class MappingBuilder extends React.Component<
43 | InjectedFormikProps,
44 | IMappingBuilderState
45 | > {
46 | constructor(props: any) {
47 | super(props)
48 |
49 | this.state = {
50 | isRequestOpened: true,
51 | isResponseOpened: true,
52 | requestParamsType: 'query',
53 | }
54 | }
55 |
56 | sync = () => {
57 | const { sync, values } = this.props
58 | if (sync !== undefined) {
59 | sync(mappingFormValuesToMapping(values))
60 | }
61 | }
62 |
63 | handleBlur = (e: React.SyntheticEvent) => {
64 | const { handleBlur, sync } = this.props
65 | handleBlur(e)
66 | if (sync !== undefined) this.sync()
67 | }
68 |
69 | toggleRequest = () => {
70 | this.setState({
71 | isRequestOpened: !this.state.isRequestOpened
72 | })
73 | }
74 |
75 | toggleResponse = () => {
76 | this.setState({
77 | isResponseOpened: !this.state.isResponseOpened
78 | })
79 | }
80 |
81 | updateRequestParamsType = (requestParamsType: 'query' | 'headers' | 'cookies' | 'body') => {
82 | this.setState({ requestParamsType })
83 | }
84 |
85 | render() {
86 | const {
87 | isLoading,
88 | deleteMapping,
89 | values,
90 | errors,
91 | touched,
92 | handleChange,
93 | mode,
94 | setBuilderMode,
95 | setJsonMode,
96 | submitForm,
97 | } = this.props
98 | const {
99 | isRequestOpened,
100 | isResponseOpened,
101 | requestParamsType,
102 | } = this.state
103 |
104 | return (
105 |
106 |
113 |
114 |
115 |
116 |
117 |
120 |
130 |
131 |
132 |
143 |
153 |
154 |
155 |
156 | )
157 | }
158 | }
159 |
160 | export default enhance(MappingBuilder)
--------------------------------------------------------------------------------
/src/modules/mappings/components/MappingIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const Icon = styled.div`
5 | color: ${props => props.theme.colors.accent};
6 | font-weight: 900;
7 | font-size: 11px;
8 | `
9 |
10 | export default class MappingIcon extends React.Component {
11 | render() {
12 | return M
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/MappingJsonEditor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import AceEditor from 'react-ace'
4 | import { ITheme } from 'edikit'
5 | import { IMapping } from '../types'
6 | import { Container, Content } from './Mapping_styled'
7 | import MappingBar from './MappingBar'
8 |
9 | interface IMappingJsonEditorProps {
10 | mapping: IMapping
11 | isLoading: boolean
12 | save(values: IMapping): void
13 | sync?: (values: IMapping) => void
14 | deleteMapping?: () => void
15 | mode: 'builder' | 'json'
16 | setBuilderMode(): void
17 | setJsonMode(): void
18 | theme: ITheme
19 | }
20 |
21 | class MappingJsonEditor extends React.Component {
22 | render() {
23 | const {
24 | mapping,
25 | isLoading,
26 | mode,
27 | setBuilderMode,
28 | setJsonMode,
29 | deleteMapping,
30 | theme,
31 | } = this.props
32 |
33 | const source = JSON.stringify(mapping, null, ' ')
34 |
35 | return (
36 |
37 |
43 |
44 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
71 | export default withTheme(MappingJsonEditor)
72 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/Mapping_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Wrapper = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | position: relative;
7 | `
8 |
9 | export const Container = styled.div`
10 | height: 100%;
11 | width: 100%;
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: flex-end;
15 | background: ${props => props.theme.pane.body.background};
16 | ${props => props.theme.pane.body.css}
17 | `
18 |
19 | interface IContentProps {
20 | isLoading: boolean
21 | }
22 |
23 | export const Content = styled.div`
24 | flex: 1;
25 | overflow-x: hidden;
26 | overflow-y: auto;
27 | transition: opacity 200ms;
28 | opacity: ${props => props.isLoading ? .5 : 1};
29 | `
30 |
31 | export const Overlay = styled.div`
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | width: 100%;
36 | height: 100%;
37 | background: rgba(0, 0, 0, 0);
38 | z-index: 100;
39 | `
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/BuilderRequest.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FormikErrors, FormikTouched } from 'formik'
3 | import { Block } from 'edikit'
4 | import { IMappingFormValues } from '../../types'
5 | import BuilderSectionLabel from './BuilderSectionLabel'
6 | import RequestUrl from './RequestUrl'
7 | import RequestUrlDetails from './RequestUrlDetails'
8 | import RequestParamsSwitcher from './RequestParamsSwitcher'
9 | import RequestParams from './RequestParams'
10 | import { Grid } from './Builder_styled'
11 |
12 | interface IBuilderRequestProps {
13 | isOpened: boolean
14 | onToggle(): void
15 | values: IMappingFormValues
16 | errors: FormikErrors
17 | touched: FormikTouched
18 | paramsType: 'query' | 'headers' | 'cookies' | 'body'
19 | onChange(e: React.ChangeEvent): void
20 | onBlur(e: any): void
21 | updateParamsType(paramsType: 'query' | 'headers' | 'cookies' | 'body'): void
22 | }
23 |
24 | export default class BuilderRequest extends React.Component {
25 | render() {
26 | const {
27 | isOpened,
28 | onToggle,
29 | values,
30 | errors,
31 | touched,
32 | onChange,
33 | onBlur,
34 | paramsType,
35 | updateParamsType,
36 | } = this.props
37 |
38 | return (
39 |
40 |
45 | {isOpened && (
46 |
47 |
48 |
55 |
62 |
67 | {paramsType === 'query' && (
68 |
77 | )}
78 | {paramsType === 'headers' && (
79 |
88 | )}
89 | {paramsType === 'cookies' && (
90 |
99 | )}
100 |
101 |
102 | )}
103 |
104 | )
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/BuilderResponse.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Block } from 'edikit'
3 | import { FormikErrors, FormikTouched } from 'formik'
4 | import { IMappingFormValues } from '../../types'
5 | import BuilderSectionLabel from './BuilderSectionLabel'
6 | import ResponseBase from './ResponseBase'
7 | import { Grid } from './Builder_styled'
8 |
9 | interface IBuilderResponseProps {
10 | isOpened: boolean
11 | onToggle(): void
12 | values: IMappingFormValues
13 | errors: FormikErrors
14 | touched: FormikTouched
15 | onChange(e: React.ChangeEvent): void
16 | onBlur(e: any): void
17 | sync(): void
18 | }
19 |
20 | export default class BuilderResponse extends React.Component {
21 | render() {
22 | const {
23 | isOpened,
24 | onToggle,
25 | values,
26 | errors,
27 | touched,
28 | onChange,
29 | onBlur,
30 | sync,
31 | } = this.props
32 |
33 | return (
34 |
35 |
40 | {isOpened && (
41 |
42 |
43 |
51 |
52 |
53 | )}
54 |
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/BuilderSectionLabel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { ChevronRight, ChevronDown } from 'react-feather'
4 | import { BuilderLabel, ITheme } from 'edikit'
5 |
6 | interface IBuilderSectionLabelProps {
7 | label: string
8 | isOpened: boolean
9 | onToggle: () => void
10 | theme: ITheme
11 | }
12 |
13 | class BuilderSectionLabel extends React.Component {
14 | render() {
15 | const { label, isOpened, onToggle, theme } = this.props
16 |
17 | const style: any = {
18 | width: '110px',
19 | cursor: 'pointer',
20 | justifyContent: 'space-between',
21 | transition: 'background 200ms',
22 | paddingRight: '9px',
23 | userSelect: 'none',
24 | background: isOpened ?
25 | theme.builder.label.background :
26 | theme.colors.muted
27 | }
28 |
29 | if (!isOpened) {
30 | style.boxShadow = 'none'
31 | }
32 |
33 | return (
34 |
37 | {label}
38 | {isOpened && (
39 |
45 | )}
46 | {!isOpened && (
47 |
53 | )}
54 |
55 | }
56 | style={style}
57 | onClick={onToggle}
58 | withLink={true}
59 | />
60 | )
61 | }
62 | }
63 |
64 | export default withTheme(BuilderSectionLabel)
65 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/Builder_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Grid = styled.div`
4 | margin: 5px 0;
5 | display: grid;
6 | grid-template-columns: repeat(8, 1fr);
7 | grid-column-gap: 12px;
8 | grid-row-gap: 14px;
9 | align-items: center;
10 | `
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/RequestParams.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Trash2 } from 'react-feather'
3 | import {FormikErrors, FormikTouched, FieldArray, getIn} from 'formik'
4 | import { Button, Input, Select } from 'edikit'
5 | import { IMappingFormValues, mappingRequestParamMatchTypes } from '../../types'
6 |
7 | interface IRequestParamsProps {
8 | type: 'queryParameters' | 'requestHeaders' | 'requestCookies'
9 | label: string
10 | values: IMappingFormValues
11 | errors: FormikErrors
12 | touched: FormikTouched
13 | onChange(e: React.ChangeEvent): void
14 | onBlur(e: any): void
15 | }
16 |
17 | export default class RequestParams extends React.Component {
18 | render() {
19 | const {
20 | type,
21 | label,
22 | values,
23 | errors,
24 | touched,
25 | onChange,
26 | onBlur,
27 | } = this.props
28 |
29 | return (
30 | (
33 |
34 | {values[type].map((param, index) => (
35 |
36 |
47 |
61 |
72 |
73 | { arrayHelpers.remove(index) }}
75 | variant="danger"
76 | icon={}
77 | style={{
78 | height: '30px',
79 | }}
80 | />
81 |
82 | {getIn(errors, `${type}.${index}.key`) && getIn(touched, `${type}.${index}.key`) && (
83 |
84 | {getIn(errors, `${type}.${index}.key`)}
85 |
86 | )}
87 | {getIn(errors, `${type}.${index}.value`) && getIn(touched, `${type}.${index}.value`) && (
88 |
89 | {getIn(errors, `${type}.${index}.value`)}
90 |
91 | )}
92 |
93 | ))}
94 | {
97 | arrayHelpers.push({
98 | key: '',
99 | matchType: 'equalTo',
100 | value: '',
101 | })
102 | }}
103 | style={{
104 | gridColumnStart: 1,
105 | gridColumnEnd: 3,
106 | height: '30px',
107 | }}
108 | >
109 | Add {label}
110 |
111 |
112 | )}
113 | />
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/RequestParamsSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Button } from 'edikit'
3 | import { IMappingFormValues } from '../../types'
4 |
5 | interface IRequestParamsSwitcherProps {
6 | paramsType: 'query' | 'headers' | 'cookies' | 'body'
7 | values: IMappingFormValues
8 | onChange(paramsType: 'query' | 'headers' | 'cookies' | 'body'): void
9 | }
10 |
11 | export default class RequestParamsSwitcher extends React.Component {
12 | render() {
13 | const {
14 | paramsType,
15 | values,
16 | onChange,
17 | } = this.props
18 |
19 | return (
20 |
21 | { onChange('query') }}
24 | style={{
25 | gridColumnStart: 1,
26 | gridColumnEnd: 3,
27 | height: '30px',
28 | }}
29 | >
30 | Query params
31 | {values.queryParameters.length > 0 ? ` [${values.queryParameters.length}]` : ''}
32 |
33 | { onChange('headers') }}
36 | style={{
37 | gridColumnStart: 3,
38 | gridColumnEnd: 5,
39 | height: '30px',
40 | }}
41 | >
42 | Headers
43 | {values.requestHeaders.length > 0 ? ` [${values.requestHeaders.length}]` : ''}
44 |
45 | { onChange('cookies') }}
48 | style={{
49 | gridColumnStart: 5,
50 | gridColumnEnd: 7,
51 | height: '30px',
52 | }}
53 | >
54 | Cookies
55 | {values.requestCookies.length > 0 ? ` [${values.requestCookies.length}]` : ''}
56 |
57 | { onChange('body') }}
60 | style={{
61 | gridColumnStart: 7,
62 | gridColumnEnd: 9,
63 | height: '30px',
64 | }}
65 | >
66 | Body
67 | {values.requestBodyPatterns.length > 0 ? ` [${values.requestBodyPatterns.length}]` : ''}
68 |
69 |
70 | )
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/RequestUrl.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FormikErrors, FormikTouched } from 'formik'
3 | import { Input, Select } from 'edikit'
4 | import { IMappingFormValues, mappingRequestMethods } from '../../types'
5 |
6 | interface IRequetsUrlProps {
7 | values: IMappingFormValues
8 | errors: FormikErrors
9 | touched: FormikTouched
10 | onChange(e: React.ChangeEvent): void
11 | onBlur(e: any): void
12 | }
13 |
14 | export default class RequestUrl extends React.Component {
15 | render() {
16 | const {
17 | values,
18 | onChange,
19 | onBlur,
20 | } = this.props
21 |
22 | return (
23 |
24 |
34 |
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/modules/mappings/components/builder/RequestUrlDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FormikErrors, FormikTouched } from 'formik'
3 | import { Select } from 'edikit'
4 | import { IMappingFormValues } from '../../types'
5 |
6 | interface IRequestUrlDetailsProps {
7 | values: IMappingFormValues
8 | errors: FormikErrors
9 | touched: FormikTouched
10 | onChange(e: React.ChangeEvent): void
11 | onBlur(e: any): void
12 | }
13 |
14 | export default class RequestUrlDetails extends React.Component {
15 | render() {
16 | const { values, onChange, onBlur } = this.props
17 |
18 | return (
19 |
20 |
23 |
41 |
50 |
66 |
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/modules/mappings/containers/CreateMappingContainer.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import { IApplicationState } from '../../../store'
4 | import { IServer } from '../../servers'
5 | import CreateMapping from '../components/CreateMapping'
6 | import { IMapping } from '../types'
7 | import {
8 | initCreateMapping,
9 | createMappingRequest,
10 | cancelMappingCreation,
11 | } from '../store'
12 |
13 | interface IOwnProps {
14 | serverName: string
15 | creationId: string
16 | }
17 |
18 | interface IPropsFromState {
19 | server?: IServer
20 | mapping?: IMapping
21 | isCreating: boolean
22 | }
23 |
24 | const mapStateToProps = (
25 | {
26 | servers: { servers },
27 | mappings: serversMappings,
28 | }: IApplicationState,
29 | {
30 | serverName,
31 | creationId,
32 | }: IOwnProps
33 | ): IPropsFromState => {
34 | const server = servers.find(s => s.name === serverName)
35 | if (server! === undefined) {
36 | throw new Error(`no server found having name: '${serverName}'`)
37 | }
38 |
39 | let mapping: IMapping | undefined
40 | let isCreating = false
41 | const serverMappings = serversMappings[serverName]
42 | if (serverMappings !== undefined) {
43 | const creation = serverMappings.creations[creationId]
44 | if (creation !== undefined) {
45 | mapping = creation.mapping
46 | isCreating = creation.isCreating
47 | }
48 | }
49 |
50 | return {
51 | server,
52 | mapping,
53 | isCreating,
54 | }
55 | }
56 |
57 |
58 | const mapDispatchToProps = (dispatch: Dispatch, props: IOwnProps) => ({
59 | init: () => {
60 | dispatch(initCreateMapping(
61 | props.serverName,
62 | props.creationId
63 | ))
64 | },
65 | save: (mapping: IMapping) => {
66 | dispatch(createMappingRequest(
67 | props.serverName,
68 | props.creationId,
69 | mapping
70 | ))
71 | },
72 | cancel: () => {
73 | dispatch(cancelMappingCreation(
74 | props.serverName,
75 | props.creationId
76 | ))
77 | },
78 | })
79 |
80 | export default connect(
81 | mapStateToProps,
82 | mapDispatchToProps
83 | )(CreateMapping)
84 |
--------------------------------------------------------------------------------
/src/modules/mappings/containers/MappingContainer.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import { IApplicationState } from '../../../store'
4 | import { IServer } from '../../servers'
5 | import Mapping from '../components/Mapping'
6 | import { IMapping } from '../types'
7 | import {
8 | fetchMappingRequest,
9 | initMappingWorkingCopy,
10 | syncMappingWorkingCopy,
11 | updateMappingRequest,
12 | deleteMappingRequest,
13 | IMappingState,
14 | } from '../store'
15 |
16 | interface IOwnProps {
17 | serverName: string
18 | mappingId: string
19 | }
20 |
21 | interface IPropsFromState {
22 | server?: IServer
23 | mapping?: IMapping
24 | isLoading: boolean
25 | }
26 |
27 | const mapStateToProps = (
28 | {
29 | servers: { servers },
30 | mappings: serversMappings,
31 | }: IApplicationState,
32 | { serverName, mappingId }: IOwnProps
33 | ): IPropsFromState => {
34 | const server = servers.find(s => s.name === serverName)
35 |
36 | let mapping: IMappingState
37 | const serverMappings = serversMappings[serverName]
38 | if (serverMappings !== undefined) {
39 | mapping = serverMappings.byId[mappingId]
40 | }
41 |
42 | if (mapping! === undefined) {
43 | throw new Error(`no mapping found for server: '${serverName}' fot id: ${mappingId}`)
44 | }
45 |
46 | return {
47 | server,
48 | isLoading: mapping!.isFetching || mapping!.isUpdating || mapping!.isDeleting,
49 | mapping: mapping!.workingCopy,
50 | }
51 | }
52 |
53 |
54 | const mapDispatchToProps = (dispatch: Dispatch, props: IOwnProps) => ({
55 | fetchMapping: () => {
56 | dispatch(fetchMappingRequest(
57 | props.serverName,
58 | props.mappingId
59 | ))
60 | },
61 | initWorkingCopy: () => {
62 | dispatch(initMappingWorkingCopy(
63 | props.serverName,
64 | props.mappingId
65 | ))
66 | },
67 | syncWorkingCopy: (update: IMapping) => {
68 | dispatch(syncMappingWorkingCopy(
69 | props.serverName,
70 | props.mappingId,
71 | update
72 | ))
73 | },
74 | updateMapping: (mapping: IMapping) => {
75 | dispatch(updateMappingRequest(
76 | props.serverName,
77 | props.mappingId,
78 | mapping
79 | ))
80 | },
81 | deleteMapping: () => {
82 | dispatch(deleteMappingRequest(
83 | props.serverName,
84 | props.mappingId
85 | ))
86 | },
87 | })
88 |
89 | export default connect(
90 | mapStateToProps,
91 | mapDispatchToProps
92 | )(Mapping)
93 |
--------------------------------------------------------------------------------
/src/modules/mappings/contentTypes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { PlusCircle } from 'react-feather'
3 | import { IContentRenderContext } from 'edikit'
4 | import { IData } from '../../types'
5 | import MappingIcon from './components/MappingIcon'
6 | import MappingContainer from './containers/MappingContainer'
7 | import CreateMappingContainer from './containers/CreateMappingContainer'
8 |
9 | export const mappingsContentTypes = [
10 | {
11 | id: 'mapping',
12 | renderButton: (context: IContentRenderContext) => {
13 | return 'mapping'
14 | /*
15 | const mapping = mappingFromContent(app, content)
16 |
17 | return `${mapping.request.method} ${mapping.request.url}`
18 | */
19 | },
20 | renderIcon: () => ,
21 | renderPane: (context: IContentRenderContext) => (
22 |
27 | ),
28 | },
29 | {
30 | id: 'mapping.create',
31 | renderButton: () => 'create mapping',
32 | renderIcon: () => ,
33 | renderPane: (context: IContentRenderContext) => (
34 |
39 | )
40 | }
41 | ]
42 |
--------------------------------------------------------------------------------
/src/modules/mappings/index.ts:
--------------------------------------------------------------------------------
1 | export * from './store'
2 | export * from './types'
3 | export * from './contentTypes'
4 | export * from './dto'
--------------------------------------------------------------------------------
/src/modules/mappings/store/actions.spec.ts:
--------------------------------------------------------------------------------
1 | import { IServer } from '../../servers'
2 | import { IMapping } from '../types'
3 | import { MappingsActionTypes } from './types'
4 | import {
5 | loadServerMappings,
6 | loadServerMappingsRequest,
7 | loadServerMappingsSuccess,
8 | fetchMappingRequest,
9 | fetchMappingSuccess,
10 | initMappingWorkingCopy,
11 | syncMappingWorkingCopy,
12 | updateMappingRequest,
13 | updateMappingSuccess,
14 | deleteMappingRequest,
15 | deleteMappingSuccess,
16 | } from './actions'
17 |
18 | const testServer: IServer = {
19 | name: 'test_server',
20 | url: 'http://localhost',
21 | port: 8080,
22 | mappingsHaveBeenLoaded: false,
23 | isLoadingMappings: false,
24 | mappings: []
25 | }
26 |
27 | const testMapping: IMapping = {
28 | id: 'test_mapping_id',
29 | uuid: 'test_mapping_id',
30 | request: {
31 | method: 'GET',
32 | },
33 | response: {
34 | status: 200,
35 | }
36 | }
37 |
38 | describe('loadServerMappings', () => {
39 | it('should return corresponding action', () => {
40 | expect(loadServerMappings(testServer)).toEqual({
41 | type: MappingsActionTypes.LOAD_SERVER_MAPPINGS,
42 | payload: {
43 | serverName: testServer.name,
44 | server: testServer,
45 | }
46 | })
47 | })
48 | })
49 |
50 | describe('loadServerMappingsRequest', () => {
51 | it('should return corresponding action', () => {
52 | expect(loadServerMappingsRequest(testServer)).toEqual({
53 | type: MappingsActionTypes.LOAD_SERVER_MAPPINGS_REQUEST,
54 | payload: {
55 | serverName: testServer.name,
56 | server: testServer,
57 | }
58 | })
59 | })
60 | })
61 |
62 | describe('loadServerMappingsSuccess', () => {
63 | it('should return corresponding action', () => {
64 | expect(loadServerMappingsSuccess(testServer, [])).toEqual({
65 | type: MappingsActionTypes.LOAD_SERVER_MAPPINGS_SUCCESS,
66 | payload: {
67 | serverName: testServer.name,
68 | server: testServer,
69 | mappings: [],
70 | }
71 | })
72 | })
73 | })
74 |
75 | describe('fetchMappingRequest', () => {
76 | it('should return corresponding action', () => {
77 | expect(fetchMappingRequest(testServer.name, testMapping.id)).toEqual({
78 | type: MappingsActionTypes.FETCH_MAPPING_REQUEST,
79 | payload: {
80 | serverName: testServer.name,
81 | mappingId: testMapping.id,
82 | }
83 | })
84 | })
85 | })
86 |
87 | describe('fetchMappingSuccess', () => {
88 | it('should return corresponding action', () => {
89 | expect(fetchMappingSuccess(testServer.name, testMapping.id, testMapping)).toEqual({
90 | type: MappingsActionTypes.FETCH_MAPPING_SUCCESS,
91 | payload: {
92 | serverName: testServer.name,
93 | mappingId: testMapping.id,
94 | mapping: testMapping,
95 | }
96 | })
97 | })
98 | })
99 |
100 | describe('initMappingWorkingCopy', () => {
101 | it('should return corresponding action', () => {
102 | expect(initMappingWorkingCopy(testServer.name, testMapping.id)).toEqual({
103 | type: MappingsActionTypes.INIT_MAPPING_WORKING_COPY,
104 | payload: {
105 | serverName: testServer.name,
106 | mappingId: testMapping.id,
107 | }
108 | })
109 | })
110 | })
111 |
112 | describe('syncMappingWorkingCopy', () => {
113 | it('should return corresponding action', () => {
114 | expect(syncMappingWorkingCopy(testServer.name, testMapping.id, testMapping)).toEqual({
115 | type: MappingsActionTypes.SYNC_MAPPING_WORKING_COPY,
116 | payload: {
117 | serverName: testServer.name,
118 | mappingId: testMapping.id,
119 | update: testMapping,
120 | }
121 | })
122 | })
123 | })
124 |
125 | describe('updateMappingRequest', () => {
126 | it('should return corresponding action', () => {
127 | expect(updateMappingRequest(testServer.name, testMapping.id, testMapping)).toEqual({
128 | type: MappingsActionTypes.UPDATE_MAPPING_REQUEST,
129 | payload: {
130 | serverName: testServer.name,
131 | mappingId: testMapping.id,
132 | mapping: testMapping,
133 | }
134 | })
135 | })
136 | })
137 |
138 | describe('updateMappingSuccess', () => {
139 | it('should return corresponding action', () => {
140 | expect(updateMappingSuccess(testServer.name, testMapping.id, testMapping)).toEqual({
141 | type: MappingsActionTypes.UPDATE_MAPPING_SUCCESS,
142 | payload: {
143 | serverName: testServer.name,
144 | mappingId: testMapping.id,
145 | mapping: testMapping,
146 | }
147 | })
148 | })
149 | })
150 |
151 | describe('deleteMappingRequest', () => {
152 | it('should return corresponding action', () => {
153 | expect(deleteMappingRequest(testServer.name, testMapping.id)).toEqual({
154 | type: MappingsActionTypes.DELETE_MAPPING_REQUEST,
155 | payload: {
156 | serverName: testServer.name,
157 | mappingId: testMapping.id,
158 | }
159 | })
160 | })
161 | })
162 |
163 | describe('deleteMappingSuccess', () => {
164 | it('should return corresponding action', () => {
165 | expect(deleteMappingSuccess(testServer.name, testMapping.id)).toEqual({
166 | type: MappingsActionTypes.DELETE_MAPPING_SUCCESS,
167 | payload: {
168 | serverName: testServer.name,
169 | mappingId: testMapping.id,
170 | }
171 | })
172 | })
173 | })
--------------------------------------------------------------------------------
/src/modules/mappings/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './epics'
3 | export * from './reducers'
--------------------------------------------------------------------------------
/src/modules/mappings/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum MappingsActionTypes {
2 | LOAD_SERVER_MAPPINGS = '@@mappings/LOAD_SERVER_MAPPINGS',
3 | LOAD_SERVER_MAPPINGS_REQUEST = '@@mappings/LOAD_SERVER_MAPPINGS_REQUEST',
4 | LOAD_SERVER_MAPPINGS_SUCCESS = '@@mappings/LOAD_SERVER_MAPPINGS_SUCCESS',
5 | FETCH_MAPPING_REQUEST = '@@mappings/FETCH_MAPPING_REQUEST',
6 | FETCH_MAPPING_SUCCESS = '@@mappings/FETCH_MAPPING_SUCCESS',
7 | INIT_MAPPING_WORKING_COPY = '@@mappings/INIT_MAPPING_WORKING_COPY',
8 | SYNC_MAPPING_WORKING_COPY = '@@mappings/SYNC_MAPPING_WORKING_COPY',
9 | UPDATE_MAPPING_REQUEST = '@@mappings/UPDATE_MAPPING_REQUEST',
10 | UPDATE_MAPPING_SUCCESS = '@@mappings/UPDATE_MAPPING_SUCCESS',
11 | DELETE_MAPPING_REQUEST = '@@mappings/DELETE_MAPPING_REQUEST',
12 | DELETE_MAPPING_SUCCESS = '@@mappings/DELETE_MAPPING_SUCCESS',
13 | INIT_CREATE_MAPPING = '@@mappings/INIT_CREATE_MAPPING',
14 | CREATE_MAPPING_REQUEST = '@@mappings/CREATE_MAPPING_REQUEST',
15 | CREATE_MAPPING_SUCCESS = '@@mappings/CREATE_MAPPING_SUCCESS',
16 | CANCEL_CREATE_MAPPING = '@@mappings/CANCEL_CREATE_MAPPING',
17 | }
--------------------------------------------------------------------------------
/src/modules/mappings/types.ts:
--------------------------------------------------------------------------------
1 | export type MappingRequestMethod =
2 | | 'GET'
3 | | 'POST'
4 | | 'PUT'
5 | | 'DELETE'
6 | | 'PATCH'
7 | | 'HEAD'
8 | | 'ANY'
9 |
10 | export type MappingRequestUrlMatchType =
11 | | 'url'
12 | | 'urlPattern'
13 | | 'urlPath'
14 | | 'urlPathPattern'
15 | | 'anyUrl'
16 |
17 | export type MappingRequestParamMatchType =
18 | | 'equalTo'
19 | | 'matches'
20 | | 'contains'
21 | | 'doesNotMatch'
22 | | 'absent'
23 | | 'equalToXml'
24 | | 'matchesXPath'
25 | | 'equalToJson'
26 | | 'matchesJsonPath'
27 |
28 | export const mappingRequestMethods: MappingRequestMethod[] = [
29 | 'GET',
30 | 'POST',
31 | 'PUT',
32 | 'DELETE',
33 | 'PATCH',
34 | 'HEAD',
35 | 'ANY',
36 | ]
37 |
38 | export const mappingRequestParamMatchTypes: MappingRequestParamMatchType[] = [
39 | 'equalTo',
40 | 'matches',
41 | 'contains',
42 | 'doesNotMatch',
43 | 'absent',
44 | ]
45 |
46 | export const mappingRequestBodyPatternMatchTypes: MappingRequestParamMatchType[] = [
47 | ...mappingRequestParamMatchTypes,
48 | 'equalToXml',
49 | 'matchesXPath',
50 | 'equalToJson',
51 | 'matchesJsonPath',
52 | ]
53 |
54 | export interface IMappingRequestParams {
55 | [key: string]: {
56 | [matchType in MappingRequestParamMatchType]?: string
57 | }
58 | }
59 |
60 | export type IMappingRequestBodyPattern = {
61 | [matchType in MappingRequestParamMatchType]?: string
62 | }
63 |
64 | export interface IMappingRequest {
65 | method: MappingRequestMethod
66 | url?: string
67 | urlPattern?: string
68 | urlPath?: string
69 | urlPathPattern?: string
70 | queryParameters?: IMappingRequestParams
71 | headers?: IMappingRequestParams
72 | cookies?: IMappingRequestParams
73 | bodyPatterns?: IMappingRequestBodyPattern[]
74 | }
75 |
76 | export enum MappingResponseDelayType {
77 | Uniform = 'uniform',
78 | LogNormal = 'lognormal',
79 | }
80 |
81 | export enum MappingResponseFault {
82 | EmptyResponse = 'EMPTY_RESPONSE',
83 | RandomDataThenClose = 'RANDOM_DATA_THEN_CLOSE',
84 | MalformedResponseChunk = 'MALFORMED_RESPONSE_CHUNK',
85 | ConnectionResetByPeer = 'CONNECTION_RESET_BY_PEER',
86 | }
87 |
88 | export interface IMappingResponseUniformDelayDistribution {
89 | type: MappingResponseDelayType.Uniform
90 | lower: number
91 | upper: number
92 | }
93 |
94 | export interface IMappingResponseLogNormalDelayDistribution {
95 | type: MappingResponseDelayType.LogNormal
96 | median: number
97 | sigma: number
98 | }
99 |
100 | export interface IMappingResponse {
101 | status: number
102 | fault?: MappingResponseFault
103 | body?: string
104 | bodyFileName?: string
105 | headers?: {
106 | [key: string]: string
107 | }
108 | fixedDelayMilliseconds?: number
109 | delayDistribution?:
110 | | IMappingResponseUniformDelayDistribution
111 | | IMappingResponseLogNormalDelayDistribution
112 | }
113 |
114 | export interface IMapping {
115 | id: string
116 | uuid: string
117 | name?: string
118 | priority?: number
119 | request: IMappingRequest
120 | response: IMappingResponse
121 | }
122 |
123 | export interface IMappingCollection {
124 | mappings: IMapping[]
125 | meta: {
126 | total: number
127 | }
128 | }
129 |
130 | export interface IMappingRequestParamFormValue {
131 | key: string
132 | matchType: MappingRequestParamMatchType
133 | value: string
134 | }
135 |
136 | export interface IMappingRequestBodyPatternFormValue {
137 | matchType: MappingRequestParamMatchType
138 | value: string
139 | }
140 |
141 | export interface IMappingResponseHeaderFormValue {
142 | key: string
143 | value: string
144 | }
145 |
146 | export interface IMappingFormValues {
147 | id: string
148 | uuid: string
149 | name?: string
150 | priority: 'auto' | number
151 | method: MappingRequestMethod
152 | url: string
153 | urlMatchType: MappingRequestUrlMatchType
154 | queryParameters: IMappingRequestParamFormValue[]
155 | requestHeaders: IMappingRequestParamFormValue[]
156 | requestCookies: IMappingRequestParamFormValue[]
157 | requestBodyPatterns: IMappingRequestBodyPatternFormValue[]
158 | responseStatus: number
159 | responseFault?: MappingResponseFault
160 | responseHeaders: IMappingResponseHeaderFormValue[]
161 | responseBody?: string
162 | responseBodyFileName?: string
163 | responseDelayMilliseconds?: number
164 | responseDelayDistribution?:
165 | | IMappingResponseUniformDelayDistribution
166 | | IMappingResponseLogNormalDelayDistribution
167 | }
168 |
--------------------------------------------------------------------------------
/src/modules/mappings/validation.ts:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup'
2 |
3 | export const mappingValidationSchema = Yup.object().shape({
4 | method: Yup.string()
5 | .required('Request method is required'),
6 | queryParameters: Yup.array().of(Yup.object().shape({
7 | key: Yup.string()
8 | .required('Query parameter name is required'),
9 | value: Yup.string()
10 | .required('Query parameter value is required'),
11 | })),
12 | requestHeaders: Yup.array().of(Yup.object().shape({
13 | key: Yup.string()
14 | .required('Header name is required'),
15 | value: Yup.string()
16 | .required('Header value is required'),
17 | })),
18 | requestCookies: Yup.array().of(Yup.object().shape({
19 | key: Yup.string()
20 | .required('Cookie name is required'),
21 | value: Yup.string()
22 | .required('Cookie value is required'),
23 | })),
24 | responseStatus: Yup.number()
25 | .min(100, 'Response status code is invalid')
26 | .max(527, 'Response status code is invalid')
27 | .required('Response status code is required'),
28 | responseHeaders: Yup.array().of(Yup.object().shape({
29 | key: Yup.string()
30 | .required('Header name is required'),
31 | value: Yup.string()
32 | .required('Header value is required'),
33 | }))
34 | })
--------------------------------------------------------------------------------
/src/modules/servers/components/CreateServer_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | padding: 9px 16px;
7 | background: ${props => props.theme.pane.body.background};
8 | ${props => props.theme.pane.body.css}
9 | `
10 |
11 | export const Grid = styled.div`
12 | display: grid;
13 | grid-template-columns: 1fr 1fr;
14 | grid-row-gap: 16px;
15 |
16 | `
17 |
18 | export const Title = styled.div`
19 | padding: 5px 0;
20 | font-weight: bold;
21 | font-size: 12px;
22 | text-transform: uppercase;
23 | `
24 |
25 | export interface ItemProps {
26 | isActive: boolean
27 | }
28 |
29 | export const Item = styled.div`
30 | display: flex;
31 | align-items: center;
32 | padding: 2px 0;
33 | cursor: pointer;
34 | user-select: none;
35 | color: ${props => (props.isActive ? 'inherit' : props.theme.colors.muted)};
36 | `
37 |
--------------------------------------------------------------------------------
/src/modules/servers/containers/CreateServerContainer.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import { createServer } from '../store'
4 | import CreateServer from '../components/CreateServer'
5 | import { IServer } from '../types'
6 |
7 | const mapDispatchToProps = (dispatch: Dispatch) => ({
8 | createServer: (server: Pick) => {
9 | return dispatch(createServer(server))
10 | }
11 | })
12 |
13 | export default connect(
14 | undefined,
15 | mapDispatchToProps
16 | )(CreateServer)
17 |
--------------------------------------------------------------------------------
/src/modules/servers/contentTypes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { PlusCircle } from 'react-feather'
3 | import { IContentRenderContext } from 'edikit'
4 | import { IData } from '../../types'
5 | import CreateServerContainer from './containers/CreateServerContainer'
6 |
7 | export const serversContentTypes = [
8 | {
9 | id: 'server.create',
10 | renderButton: (context: IContentRenderContext) => 'create server',
11 | renderIcon: (context: IContentRenderContext) => ,
12 | renderPane: (context: IContentRenderContext) => (
13 |
14 | ),
15 | }
16 | ]
--------------------------------------------------------------------------------
/src/modules/servers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './store'
3 | export * from './contentTypes'
--------------------------------------------------------------------------------
/src/modules/servers/store/actions.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { action } from 'typesafe-actions'
3 | import { ServersActionTypes } from './types'
4 | import { IServer } from '../types'
5 |
6 | export interface IInitServersAction {
7 | type: ServersActionTypes.INIT_SERVERS
8 | payload: {
9 | servers: IServer[]
10 | }
11 | }
12 |
13 | export const initServers = (servers: IServer[]) =>
14 | action(ServersActionTypes.INIT_SERVERS, { servers })
15 |
16 | export interface ICreateServerAction {
17 | type: ServersActionTypes.CREATE_SERVER
18 | payload: {
19 | server: IServer
20 | }
21 | }
22 |
23 | export const createServer = (server: Pick) => action(
24 | ServersActionTypes.CREATE_SERVER,
25 | { server },
26 | {
27 | notification: {
28 | type: 'success',
29 | content: (
30 |
31 | server {server.name} successfully created
32 |
33 | ),
34 | ttl: 3000,
35 | },
36 | }
37 | )
38 |
39 | export interface ISelectServerAction {
40 | type: ServersActionTypes.SELECT_SERVER
41 | payload: {
42 | serverId: string
43 | }
44 | }
45 |
46 | export const selectServer = (serverId: string) =>
47 | action(ServersActionTypes.SELECT_SERVER, { serverId })
48 |
49 | export interface IUpdateServerAction {
50 | type: ServersActionTypes.UPDATE_SERVER
51 | payload: {
52 | server: IServer
53 | }
54 | }
55 |
56 | export const updateServer = (server: IServer) =>
57 | action(ServersActionTypes.UPDATE_SERVER, { server })
58 |
59 | export interface IRemoveServerAction {
60 | type: ServersActionTypes.REMOVE_SERVER
61 | payload: {
62 | serverId: string
63 | }
64 | }
65 |
66 | export const removeServer = (serverId: string) =>
67 | action(ServersActionTypes.REMOVE_SERVER, { serverId })
68 |
69 | export type ServersAction =
70 | | IInitServersAction
71 | | ICreateServerAction
72 | | ISelectServerAction
73 | | IUpdateServerAction
74 | | IRemoveServerAction
75 |
--------------------------------------------------------------------------------
/src/modules/servers/store/epics.ts:
--------------------------------------------------------------------------------
1 | import { Epic, combineEpics } from 'redux-observable'
2 | import { of } from 'rxjs'
3 | import { mergeMap } from 'rxjs/operators'
4 | import { removeContentFromAllPanesAction } from 'edikit'
5 | import { IApplicationState } from '../../../store'
6 | import { ServersAction, ICreateServerAction } from './actions'
7 | import { ServersActionTypes } from './types'
8 |
9 | export const createServerEpic: Epic = action$ =>
10 | action$.ofType(ServersActionTypes.CREATE_SERVER)
11 | .pipe(
12 | mergeMap(({ payload }: ICreateServerAction) => of(
13 | removeContentFromAllPanesAction('default', 'server.create')
14 | ))
15 | )
16 |
17 | export const serversEpic = combineEpics(
18 | createServerEpic
19 | )
20 |
--------------------------------------------------------------------------------
/src/modules/servers/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 | export * from './types'
4 | export * from './epics'
--------------------------------------------------------------------------------
/src/modules/servers/store/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux'
2 | import { ServersActionTypes } from './types'
3 | import { IServer } from '../types'
4 |
5 | export interface IServersState {
6 | readonly servers: IServer[]
7 | }
8 |
9 | const initialState: IServersState = {
10 | servers: [],
11 | }
12 |
13 | const reducer: Reducer = (state = initialState, action) => {
14 | switch (action.type) {
15 | case ServersActionTypes.INIT_SERVERS: {
16 | return {
17 | ...state,
18 | servers: action.payload.servers.map((server: Partial) => ({
19 | ...server,
20 | mappingsHaveBeenLoaded: false,
21 | isLoadingMappings: false,
22 | mappings: []
23 | }))
24 | }
25 | }
26 |
27 | case ServersActionTypes.CREATE_SERVER: {
28 | return {
29 | ...state,
30 | servers: [
31 | ...state.servers,
32 | action.payload.server,
33 | ]
34 | }
35 | }
36 |
37 | default: {
38 | return state
39 | }
40 | }
41 | }
42 |
43 | export { reducer as serversReducer }
--------------------------------------------------------------------------------
/src/modules/servers/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum ServersActionTypes {
2 | INIT_SERVERS = '@@servers/INIT_SERVERS',
3 | CREATE_SERVER = '@@servers/CREATE_SERVER',
4 | SELECT_SERVER = '@@servers/SELECT_SERVER',
5 | UPDATE_SERVER = '@@servers/UPDATE_SERVER',
6 | REMOVE_SERVER = '@@servers/REMOVE_SERVER',
7 | }
8 |
--------------------------------------------------------------------------------
/src/modules/servers/types.ts:
--------------------------------------------------------------------------------
1 | import { IMapping } from '../mappings'
2 |
3 | export interface IServer {
4 | name: string
5 | url: string
6 | port?: number
7 | mappingsHaveBeenLoaded: boolean
8 | isLoadingMappings: boolean
9 | mappings: IMapping[]
10 | }
--------------------------------------------------------------------------------
/src/modules/settings/components/Settings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import themes from '../../../themes'
3 | import { ISettings } from '../types'
4 | import { Container, Grid, Title, Item } from './Settings_styled'
5 |
6 | interface ISettingsProps {
7 | settings: ISettings
8 | setSetting: (key: string, value: any) => void
9 | }
10 |
11 | class Settings extends React.Component {
12 | render() {
13 | const { settings, setSetting } = this.props
14 |
15 | return (
16 |
17 |
18 |
19 |
Theme
20 | {Object.keys(themes).map((t: string) => (
21 | - {
24 | setSetting('theme', t)
25 | }}
26 | isActive={t === settings.theme}
27 | >
28 | {t}
29 |
30 | ))}
31 |
32 |
33 |
34 | )
35 | }
36 | }
37 |
38 | export default Settings
39 |
--------------------------------------------------------------------------------
/src/modules/settings/components/SettingsIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { withTheme } from 'styled-components'
3 | import { Settings as Icon } from 'react-feather'
4 |
5 | export interface ISettingsIconProps {
6 | theme: any
7 | }
8 |
9 | class SettingsIcon extends React.Component {
10 | render() {
11 | const { theme } = this.props
12 |
13 | return (
14 |
15 | )
16 | }
17 | }
18 |
19 | export default withTheme(SettingsIcon)
20 |
--------------------------------------------------------------------------------
/src/modules/settings/components/Settings_styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | height: 100%;
5 | width: 100%;
6 | background: ${props => props.theme.pane.body.background};
7 | ${props => props.theme.pane.body.css}
8 | `
9 |
10 | export const Grid = styled.div`
11 | display: grid;
12 | grid-template-columns: 1fr 1fr;
13 | grid-row-gap: 16px;
14 | padding: 16px 32px;
15 | `
16 |
17 | export const Title = styled.div`
18 | padding: 5px 0;
19 | font-weight: bold;
20 | font-size: 12px;
21 | text-transform: uppercase;
22 | `
23 |
24 | export interface ItemProps {
25 | isActive: boolean
26 | }
27 |
28 | export const Item = styled.div`
29 | display: flex;
30 | align-items: center;
31 | padding: 2px 0;
32 | cursor: pointer;
33 | user-select: none;
34 | color: ${props => (props.isActive ? 'inherit' : props.theme.colors.muted)};
35 | `
36 |
--------------------------------------------------------------------------------
/src/modules/settings/containers/SettingsContainer.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import { connect } from 'react-redux'
3 | import { setSetting } from '../store'
4 | import { IApplicationState } from '../../../store'
5 | import Settings from '../components/Settings'
6 | import { ISettings } from '../types'
7 |
8 | const mapStateToProps = ({ settings: { settings } }: IApplicationState): {
9 | settings: ISettings
10 | } => ({
11 | settings
12 | })
13 |
14 | const mapDispatchToProps = (dispatch: Dispatch) => ({
15 | setSetting: (key: string, value: any) => {
16 | return dispatch(setSetting(key, value))
17 | }
18 | })
19 |
20 | export default connect(
21 | mapStateToProps,
22 | mapDispatchToProps
23 | )(Settings)
24 |
--------------------------------------------------------------------------------
/src/modules/settings/contentTypes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import SettingsIcon from './components/SettingsIcon'
3 | import Settings from './containers/SettingsContainer'
4 |
5 | export const settingsContentTypes = [
6 | {
7 | id: 'settings',
8 | renderButton: () => 'settings',
9 | renderIcon: () => ,
10 | renderPane: () => ,
11 | }
12 | ]
--------------------------------------------------------------------------------
/src/modules/settings/index.ts:
--------------------------------------------------------------------------------
1 | export * from './store'
2 | export * from './types'
3 | export * from './contentTypes'
4 |
--------------------------------------------------------------------------------
/src/modules/settings/store/actions.ts:
--------------------------------------------------------------------------------
1 | import { isString, isBoolean } from 'lodash'
2 | import { action, createAction } from 'typesafe-actions'
3 | import { IAction } from '../../../store'
4 | import { SettingsActionTypes } from './types'
5 | import { ISettings } from '../types'
6 |
7 | export interface IInitSettingsAction extends IAction {
8 | type: SettingsActionTypes.INIT_SETTINGS
9 | payload: ISettings
10 | }
11 |
12 | export const initSettings = (settings: ISettings) => action(SettingsActionTypes.INIT_SETTINGS, settings)
13 |
14 | export interface ISetSettingAction extends IAction {
15 | type: SettingsActionTypes.SET_SETTING
16 | payload: {
17 | key: string
18 | value: any
19 | }
20 | }
21 |
22 | export const setSetting = createAction(
23 | SettingsActionTypes.SET_SETTING,
24 | resolve => (key: string, value: any) => {
25 | if (isString(value)) {
26 | localStorage.setItem(key, value)
27 | } else if (isBoolean(value)) {
28 | localStorage.setItem(key, value ? 'true' : 'false')
29 | }
30 |
31 | return resolve({ key, value })
32 | }
33 | )
34 |
35 | export type SettingsAction =
36 | | IInitSettingsAction
37 | | ISetSettingAction
--------------------------------------------------------------------------------
/src/modules/settings/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducers'
--------------------------------------------------------------------------------
/src/modules/settings/store/reducers.ts:
--------------------------------------------------------------------------------
1 | import { ISettings } from '../types'
2 | import { SettingsActionTypes } from './types'
3 | import { SettingsAction } from './actions'
4 |
5 | export interface ISettingsState {
6 | settings: ISettings
7 | }
8 |
9 | const initialState = {
10 | settings: {
11 | theme: 'white'
12 | }
13 | }
14 |
15 | export const settingsReducer = (
16 | state: ISettingsState = initialState,
17 | action: SettingsAction
18 | ): ISettingsState => {
19 | switch (action.type) {
20 | case SettingsActionTypes.INIT_SETTINGS:
21 | return {
22 | ...state,
23 | settings: action.payload
24 | }
25 |
26 | case SettingsActionTypes.SET_SETTING:
27 | return {
28 | ...state,
29 | settings: {
30 | ...state.settings,
31 | [action.payload.key]: action.payload.value
32 | }
33 | }
34 |
35 | default:
36 | return state
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/src/modules/settings/store/types.ts:
--------------------------------------------------------------------------------
1 | export const INIT_SETTINGS = '@@settings/INIT_SETTINGS'
2 | export const SET_SETTING = '@@settings/SET_SETTING'
3 |
4 | export enum SettingsActionTypes {
5 | INIT_SETTINGS = '@@settings/INIT_SETTINGS',
6 | SET_SETTING = '@@settings/SET_SETTING',
7 | }
--------------------------------------------------------------------------------
/src/modules/settings/types.ts:
--------------------------------------------------------------------------------
1 | export interface ISettings {
2 | theme: string
3 | }
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { combineEpics } from 'redux-observable'
3 | import {
4 | panesReducer,
5 | IPanesState,
6 | notificationsReducer,
7 | notificationsEpic,
8 | INotificationsState,
9 | NotificationsAction,
10 | } from 'edikit'
11 | import { coreReducer, CoreAction, ICoreState, coreEpic } from './modules/core'
12 | import { mappingsReducer, IMappingsState, MappingsAction, mappingsEpic } from './modules/mappings'
13 | import { serversReducer, IServersState, ServersAction, serversEpic } from './modules/servers'
14 | import { settingsReducer, ISettingsState, SettingsAction } from './modules/settings'
15 | import { IData } from './types'
16 |
17 | export interface IAction {
18 | type: string
19 | payload?: {}
20 | }
21 |
22 | export type RootAction =
23 | | NotificationsAction
24 | | CoreAction
25 | | ServersAction
26 | | SettingsAction
27 | | MappingsAction
28 |
29 | export const rootEpic = combineEpics(
30 | notificationsEpic,
31 | coreEpic,
32 | serversEpic,
33 | mappingsEpic
34 | )
35 |
36 | export interface IApplicationState {
37 | notifications: INotificationsState
38 | panes: IPanesState
39 | core: ICoreState
40 | settings: ISettingsState
41 | servers: IServersState
42 | mappings: IMappingsState
43 | }
44 |
45 | export const rootReducer = combineReducers({
46 | notifications: notificationsReducer,
47 | panes: panesReducer(),
48 | core: coreReducer,
49 | settings: settingsReducer,
50 | servers: serversReducer,
51 | mappings: mappingsReducer,
52 | })
53 |
--------------------------------------------------------------------------------
/src/themes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ITheme,
3 | blackTheme,
4 | solarizedDarkTheme,
5 | whiteTheme,
6 | } from 'edikit'
7 |
8 | const themes: { [name: string]: ITheme } = {
9 | black: blackTheme,
10 | 'solarized dark': solarizedDarkTheme,
11 | white: whiteTheme,
12 | }
13 |
14 | export default themes
15 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface IData {
2 | serverName: string
3 | mappingId?: string
4 | creationId?: string
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "build/dist",
5 | "module": "esnext",
6 | "target": "es5",
7 | "lib": ["es7", "dom"],
8 | "sourceMap": true,
9 | "allowJs": true,
10 | "jsx": "react",
11 | "moduleResolution": "node",
12 | "rootDir": "src",
13 | "forceConsistentCasingInFileNames": false,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": false,
20 | "paths": {
21 | "edikit": ["src/edikit"]
22 | }
23 | },
24 | "exclude": [
25 | "node_modules",
26 | "build",
27 | "scripts",
28 | "acceptance-tests",
29 | "webpack",
30 | "jest",
31 | "src/setupTests.ts"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended",
4 | "tslint-react",
5 | "tslint-config-prettier"
6 | ],
7 | "linterOptions": {
8 | "exclude": [
9 | "config/**/*.js",
10 | "node_modules/**/*.ts",
11 | "coverage/lcov-report/*.js"
12 | ]
13 | },
14 | "rules": {
15 | "object-literal-sort-keys": false,
16 | "ordered-imports": false,
17 | "member-ordering": false,
18 | "member-access": false,
19 | "curly": [true, "ignore-same-line"],
20 | "interface-over-type-literal": false,
21 | "no-string-literal": false,
22 | "no-implicit-dependencies": [true, "dev"],
23 | "max-classes-per-file": false,
24 | "one-variable-per-declaration": false,
25 | "no-duplicate-imports": true,
26 | "jsx-no-lambda": false
27 | }
28 | }
29 |
--------------------------------------------------------------------------------