├── .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 | [![wiremock ui license](https://img.shields.io/github/license/plouc/wiremock-ui.svg?longCache=true&style=for-the-badge)](https://github.com/plouc/wiremock-ui/blob/master/LICENSE) 4 | [![wiremock ui issues](https://img.shields.io/github/issues/plouc/wiremock-ui.svg?longCache=true&style=for-the-badge)](https://github.com/plouc/wiremock-ui/issues) 5 | [![wiremock ui build status](https://img.shields.io/travis/plouc/wiremock-ui.svg?longCache=true&style=for-the-badge)](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 | ![UI screenshot](https://raw.githubusercontent.com/plouc/wiremock-ui/master/screenshots/ui_solarized_dark.png) 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 | ![UI screenshot](https://raw.githubusercontent.com/plouc/wiremock-ui/master/screenshots/ui_white.png) 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 |
110 | {title} 111 |
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 | 54 | 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 | 71 | 84 | 85 | 86 | {save !== undefined && ( 87 | 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 | 33 | 45 | 57 | 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 | --------------------------------------------------------------------------------