├── .env ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── clean.js ├── docs ├── ads.txt ├── favicon.png ├── index.html ├── js │ ├── 1.4e589ca11a954ff9.js │ ├── 1.4e589ca11a954ff9.js.LICENSE │ └── app.ee8198729c4a30fb.js ├── manifest.json ├── precache-manifest.92c9c0748b248b7ab83d5757550fd370.js └── sw.js ├── package.json ├── public ├── ads.txt ├── favicon.png └── manifest.json ├── src ├── App.tsx ├── components │ ├── chart │ │ ├── index.tsx │ │ └── psychrometrics │ │ │ ├── Core.ts │ │ │ ├── StatePointω.ts │ │ │ ├── index.tsx │ │ │ ├── model.ts │ │ │ └── psychrometrics.less │ ├── editor │ │ ├── Editor.tsx │ │ └── index.tsx │ ├── error │ │ ├── ErrorBoundary.tsx │ │ └── index.tsx │ ├── form │ │ ├── DynamicForm.tsx │ │ ├── Form.tsx │ │ ├── FormItem.tsx │ │ ├── index.tsx │ │ └── test.json │ ├── label │ │ ├── Label.tsx │ │ └── index.tsx │ ├── layout │ │ ├── Content.tsx │ │ ├── Header.tsx │ │ ├── Menus.tsx │ │ ├── Sider.tsx │ │ └── index.tsx │ ├── panel │ │ ├── ChartPanel.tsx │ │ ├── DataGridPanel.tsx │ │ ├── Panel.tsx │ │ ├── StructurePanel.tsx │ │ ├── StylePanel.tsx │ │ ├── index.tsx │ │ ├── structure │ │ │ ├── GridPanel.tsx │ │ │ ├── SeriesPanel.tsx │ │ │ ├── TooltipPanel.tsx │ │ │ ├── XAxisPanel.tsx │ │ │ ├── YAxisPanel.tsx │ │ │ └── index.tsx │ │ └── style │ │ │ ├── GridPanel.tsx │ │ │ ├── SeriesPanel.tsx │ │ │ ├── XAxisPanel.tsx │ │ │ ├── YAxisPanel.tsx │ │ │ └── index.tsx │ ├── picker │ │ ├── ColorPicker.tsx │ │ └── index.tsx │ ├── resizer │ │ ├── Resizer.tsx │ │ └── index.tsx │ └── virtualized │ │ ├── VirtualizedTable.tsx │ │ └── index.tsx ├── containers │ ├── ChartContainer.tsx │ ├── StructureContainer.tsx │ └── StyleContainer.tsx ├── examples │ ├── Table.tsx │ └── index.tsx ├── i18n │ ├── i18nClient.ts │ └── index.ts ├── index.tsx ├── locales │ ├── index.ts │ ├── locale.constant-ko.json │ └── locale.constant.json ├── serviceWorker.ts └── styles │ ├── antd │ ├── form │ │ ├── form.less │ │ └── index.less │ └── index.less │ ├── editor │ ├── editor.less │ └── index.less │ ├── index.less │ ├── normalize.less │ ├── react-data-grid │ └── index.less │ ├── react-split-pane │ └── index.less │ └── virtualized │ └── index.less ├── tsconfig.json ├── tslint.json ├── types └── global.d.ts ├── webpack.common.js ├── webpack.dev.js ├── webpack.lib.js └── webpack.prod.js /.env: -------------------------------------------------------------------------------- 1 | # Public URL for production 2 | PUBLIC_URL=./ 3 | 4 | # Webpack Dev Server 5 | DEV_PORT=8080 6 | DEV_HOST=localhost 7 | DEV_PROXY_HTTP=http://localhost 8 | DEV_PROXY_WS=ws://localhost 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "max-len": [1, 120, 2, {"ignoreComments": true}], 5 | "indent": [1, 4, { "SwitchCase": 1 }], 6 | "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": true }], 7 | "react/jsx-indent": [1, 4], 8 | "linebreak-style": 0, 9 | "react/jsx-filename-extension": 0, 10 | "react/prefer-stateless-function": 0, 11 | "react/prop-types": 0, 12 | "react/jsx-indent-props": [2, 4], 13 | "react/forbid-prop-types": 0, 14 | "react/require-default-props": [0, { "forbidDefaultForRequired": false }], 15 | "jsx-a11y/href-no-hash": 0, 16 | "no-mixed-operators": [ 17 | "error", 18 | { 19 | "groups": [ 20 | ["+", "-", "*", "/", "%", "**"], 21 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 22 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 23 | ["&&", "||"], 24 | ["in", "instanceof"] 25 | ], 26 | "allowSamePrecedence": false 27 | } 28 | ], 29 | "jsx-a11y/anchor-is-valid": 0, 30 | "object-curly-newline": 0, 31 | "import/no-unresolved": [ 32 | "error", 33 | { 34 | "ignore": [ "src/" ] 35 | } 36 | ], 37 | "react/sort-comp": false, 38 | "func-names": "off" 39 | }, 40 | "env": { 41 | "browser": true, 42 | "node": true 43 | }, 44 | "extends": ["airbnb", "prettier"] 45 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | dist/ 4 | lib/ 5 | .vscode/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "tabWidth": 4, 7 | "useTabs": true 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sung Gyun Oh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-analytics 2 | Data visualization analysis editor developed with react, antd, echarts 3 | -------------------------------------------------------------------------------- /clean.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const fs = require('fs-extra'); 3 | 4 | del.sync([ 5 | 'dist/**', 6 | 'lib/**', 7 | 'docs/**', 8 | ]); 9 | 10 | fs.copySync('public', 'docs'); 11 | -------------------------------------------------------------------------------- /docs/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-8569372752842198, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salgum1114/react-analytics/519e4e79ba8dd1c9f4db818f91c214b04ab0e4cf/docs/favicon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Analytics 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/js/1.4e589ca11a954ff9.js.LICENSE: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2017 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | 7 | /* 8 | object-assign 9 | (c) Sindre Sorhus 10 | @license MIT 11 | */ 12 | 13 | /** @license React v16.12.0 14 | * react.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v16.12.0 23 | * react-dom.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v0.18.0 32 | * scheduler.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** 41 | * @license 42 | * Lodash 43 | * Copyright OpenJS Foundation and other contributors 44 | * Released under MIT license 45 | * Based on Underscore.js 1.8.3 46 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 47 | */ 48 | 49 | /** @license React v16.12.0 50 | * react-is.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | 58 | /*! 59 | * UAParser.js v0.7.21 60 | * Lightweight JavaScript-based User-Agent string parser 61 | * https://github.com/faisalman/ua-parser-js 62 | * 63 | * Copyright © 2012-2019 Faisal Salman 64 | * Licensed under MIT License 65 | */ 66 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Analytics", 3 | "name": "React Analytics", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /docs/precache-manifest.92c9c0748b248b7ab83d5757550fd370.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "aa729c46f0e0df11b16c75915b6b327a", 4 | "url": "./index.html" 5 | }, 6 | { 7 | "revision": "4e589ca11a954ff992a3", 8 | "url": "./js/1.4e589ca11a954ff9.js" 9 | }, 10 | { 11 | "revision": "13a030e9116f2be59e02dcf38e06f317", 12 | "url": "./js/1.4e589ca11a954ff9.js.LICENSE" 13 | }, 14 | { 15 | "revision": "ee8198729c4a30fbe3c1", 16 | "url": "./js/app.ee8198729c4a30fb.js" 17 | } 18 | ]); -------------------------------------------------------------------------------- /docs/sw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "./precache-manifest.92c9c0748b248b7ab83d5757550fd370.js" 18 | ); 19 | 20 | workbox.core.skipWaiting(); 21 | 22 | workbox.core.clientsClaim(); 23 | 24 | /** 25 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 26 | * requests for URLs in the manifest. 27 | * See https://goo.gl/S9QRab 28 | */ 29 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 30 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-analytics", 3 | "version": "0.0.1", 4 | "description": "Data visualization analysis editor developed with react, antd, echarts", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "node clean && webpack -p --config webpack.prod.js", 9 | "build:lib": "npm run tsc && webpack -p --config webpack.lib.js", 10 | "start": "npm install && npm run start:dev", 11 | "start:dev": "webpack-dev-server --config webpack.dev.js --inline", 12 | "deploy": "npm run build:lib && npm publish", 13 | "lint": "npm run tsc", 14 | "clean": "node clean", 15 | "tsc": "tsc" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/salgum1114/react-analytics.git" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "antd", 24 | "echarts", 25 | "analytics", 26 | "editor" 27 | ], 28 | "author": "salgum1114", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/salgum1114/react-analytics/issues" 32 | }, 33 | "homepage": "https://github.com/salgum1114/react-analytics#readme", 34 | "dependencies": { 35 | "antd": "^4.0.2", 36 | "classnames": "^2.2.6", 37 | "css-element-queries": "^1.2.2", 38 | "d3": "^5.15.0", 39 | "echarts": "^4.5.0", 40 | "echarts-for-react": "^2.0.15-beta.1", 41 | "faker": "^4.1.0", 42 | "i18next": "^19.0.2", 43 | "i18next-browser-languagedetector": "^4.0.1", 44 | "lodash": "^4.17.15", 45 | "rc-resize-observer": "^0.1.3", 46 | "react": "^16.12.0", 47 | "react-ace": "^8.0.0", 48 | "react-color": "^2.17.3", 49 | "react-custom-scrollbars": "^4.2.1", 50 | "react-data-grid": "^6.1.0", 51 | "react-dom": "^16.12.0", 52 | "react-helmet": "^5.2.1", 53 | "react-hot-loader": "^4.12.18", 54 | "react-resize-detector": "^4.2.1", 55 | "react-router": "^5.1.2", 56 | "react-router-dom": "^5.1.2", 57 | "react-split": "^2.0.7", 58 | "react-split-pane": "^0.1.89", 59 | "react-window": "^1.8.5", 60 | "resize-observer-polyfill": "^1.5.1", 61 | "store": "^2.0.12", 62 | "typescript": "^3.7.4", 63 | "uuid": "^3.3.3", 64 | "warning": "^4.0.3" 65 | }, 66 | "devDependencies": { 67 | "@babel/core": "^7.7.7", 68 | "@babel/plugin-proposal-class-properties": "^7.7.4", 69 | "@babel/plugin-proposal-decorators": "^7.7.4", 70 | "@babel/plugin-transform-runtime": "^7.7.6", 71 | "@babel/polyfill": "^7.7.0", 72 | "@babel/preset-env": "^7.7.7", 73 | "@babel/preset-react": "^7.7.4", 74 | "@babel/preset-typescript": "^7.7.7", 75 | "@types/classnames": "^2.2.9", 76 | "@types/d3": "^5.7.2", 77 | "@types/echarts": "^4.4.2", 78 | "@types/lodash": "^4.14.149", 79 | "@types/react": "^16.9.17", 80 | "@types/react-color": "^3.0.1", 81 | "@types/react-custom-scrollbars": "^4.0.6", 82 | "@types/react-data-grid": "^4.0.5", 83 | "@types/react-dom": "^16.9.4", 84 | "@types/react-helmet": "^5.0.14", 85 | "@types/react-resize-detector": "^4.2.0", 86 | "@types/react-router": "^5.1.4", 87 | "@types/react-router-dom": "^5.1.3", 88 | "@types/react-window": "^1.8.1", 89 | "@types/uuid": "^3.4.6", 90 | "@types/warning": "^3.0.0", 91 | "@types/webpack-env": "^1.14.1", 92 | "babel-eslint": "^10.0.3", 93 | "babel-loader": "^8.0.6", 94 | "babel-plugin-dynamic-import-webpack": "^1.1.0", 95 | "babel-plugin-import": "^1.13.0", 96 | "css-loader": "^3.4.0", 97 | "del": "^5.1.0", 98 | "dotenv": "^8.2.0", 99 | "eslint": "^6.8.0", 100 | "fs-extra": "^8.1.0", 101 | "html-webpack-plugin": "^3.2.0", 102 | "less": "^3.10.3", 103 | "less-loader": "^5.0.0", 104 | "style-loader": "^1.1.2", 105 | "terser-webpack-plugin": "^2.3.1", 106 | "tslint": "^5.20.1", 107 | "tslint-react": "^4.1.0", 108 | "url-loader": "^3.0.0", 109 | "webpack": "^4.41.4", 110 | "webpack-cli": "^3.3.10", 111 | "webpack-dev-server": "^3.10.1", 112 | "webpack-merge": "^4.2.2", 113 | "workbox-webpack-plugin": "^4.3.1" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-8569372752842198, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salgum1114/react-analytics/519e4e79ba8dd1c9f4db818f91c214b04ab0e4cf/public/favicon.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Analytics", 3 | "name": "React Analytics", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { Layout } from 'antd'; 6 | 7 | import Editor from './components/editor/Editor'; 8 | import { Psychrometrics } from './components/chart'; 9 | import { Menus } from './components/layout'; 10 | import { Table } from './examples'; 11 | 12 | class App extends Component { 13 | render() { 14 | return ( 15 |
16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/components/chart/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Psychrometrics } from './psychrometrics'; 2 | -------------------------------------------------------------------------------- /src/components/chart/psychrometrics/Core.ts: -------------------------------------------------------------------------------- 1 | /* global ko, d3 */ 2 | /* global Blob */ 3 | /* global saveSvgAsPng */ 4 | export const c8 = -1.0440397e4; 5 | export const c9 = -1.129465e1; 6 | export const c10 = -2.7022355e-2; 7 | export const c11 = 1.289036e-5; 8 | export const c12 = -2.4780681e-9; 9 | export const c13 = 6.5459673; 10 | 11 | export const minTempF = 32; 12 | export const maxTempF = 120; 13 | export const maxω = 0.03; 14 | export const totalPressure = 14.7; 15 | 16 | export const xOffsetPercentLeft = 2; 17 | export const xOffsetPercentRight = 15; 18 | export const yOffsetPercent = 10; 19 | 20 | export const Rda = 53.35; // Dry air gas constant, ft-lbf / lbda-R 21 | 22 | export const constantRHvalues = [10, 20, 30, 40, 50, 60, 70, 80, 90]; 23 | 24 | export const convertFahrenheitToCelsius = (temp: number): number => (temp - 32) / 1.8; 25 | 26 | export const convertCelsiusToFahrenheit = (temp: number): number => (temp * 1.8) + 32; 27 | 28 | export const getRandomInt = (min: number, max: number) => { 29 | min = Math.ceil(min); 30 | max = Math.floor(max); 31 | return Math.floor(Math.random() * (max - min)) + min; // The maximum is exclusive and the minimum is inclusive 32 | } 33 | 34 | export const getRandomArbitrary = (min: number, max: number) => { 35 | return Math.random() * (max - min) + min; 36 | } 37 | 38 | export const isMult = (val: number, mult: number) => val % mult === 0; 39 | 40 | export const newtonRaphson = (zeroFunc: (...args: any) => any, derivativeFunc: (...args: any) => any, initialX: number, tolerance?: number) => { 41 | if (typeof tolerance === 'undefined') { 42 | tolerance = 0.0001; 43 | } 44 | let testX = initialX; 45 | while (Math.abs(zeroFunc(testX)) > tolerance) { 46 | testX = testX - zeroFunc(testX) / derivativeFunc(testX); 47 | } 48 | return testX; 49 | } 50 | 51 | // Utility method that guarantees that min and max are exactly 52 | // as input, with the step size based on 0. 53 | export const range = (min: any, max: any, stepsize: number) => { 54 | const parsedMin = parseFloat(min); 55 | const toReturn = parsedMin % stepsize === 0 ? [] : [parsedMin]; 56 | let n = 0; 57 | const baseValue = stepsize * Math.ceil(parsedMin / stepsize); 58 | while (baseValue + n * stepsize < parseFloat(max)) { 59 | toReturn.push(baseValue + n * stepsize); 60 | n = n + 1; 61 | } 62 | toReturn.push(max); 63 | return toReturn; 64 | } 65 | 66 | // Saturation pressure in psia from temp in °F. Pws 67 | export const satPressFromTempIp = (temp: number) => { 68 | const t = temp + 459.67; 69 | const lnOfSatPress = 70 | c8 / t + 71 | c9 + 72 | c10 * t + 73 | c11 * Math.pow(t, 2) + 74 | c12 * Math.pow(t, 3) + 75 | c13 * Math.log(t); 76 | const satPress = Math.exp(lnOfSatPress); 77 | return satPress; 78 | } 79 | 80 | export const satHumidRatioFromTempIp = (temp: number, totalPressure: number) => { 81 | if (!temp && !totalPressure) { 82 | throw Error(`Not all parameters specified. temp: ${temp}; P: ${totalPressure}`); 83 | } 84 | const satPress = satPressFromTempIp(temp); 85 | return (0.621945 * satPress) / (totalPressure - satPress); 86 | } 87 | 88 | export const wFromPv = (pv: number, totalPressure: number) => { 89 | if (!pv && !totalPressure) { 90 | throw Error(`Not all parameters specified. pv: ${pv}; P: ${totalPressure}`); 91 | } 92 | return (0.621945 * pv) / (totalPressure - pv); 93 | } 94 | 95 | export const pvFromw = (w: any, totalPressure: number) => { 96 | if (typeof w === 'string') { 97 | w = parseFloat(w); 98 | } 99 | if (w < 0.000001) { 100 | return 0; 101 | } 102 | return totalPressure / (1 + 0.621945 / w); 103 | } 104 | 105 | // partial pressure of vapor from dry bulb temp (°F) and rh (0-1) 106 | export const pvFromTempRh = (temp: number, rh: number) => { 107 | if (rh < 0 || rh > 1) { 108 | throw new Error('RH value must be between 0-1'); 109 | } 110 | return rh * satPressFromTempIp(temp); 111 | } 112 | 113 | export const tempFromRhAndPv = (rh: number, pv: number) => { 114 | if (!rh || rh > 1) { 115 | throw new Error('RH value must be between 0-1'); 116 | } 117 | const goalPsat = pv / rh; 118 | // Employ Newton-Raphson method. 119 | const funcToZero = (temp: number) => { 120 | return satPressFromTempIp(temp) - goalPsat; 121 | } 122 | const derivativeFunc = (temp: number) => dPvdT(1, temp); 123 | return newtonRaphson(funcToZero, derivativeFunc, 80, 0.00001) 124 | } 125 | 126 | export const tempFromEnthalpyPv = (h: number, pv: number, totalPressure: number) => { 127 | const ω = wFromPv(pv, totalPressure); 128 | return (h - ω * 1061) / (0.24 + ω * 0.445); 129 | } 130 | 131 | // Returns object with temperature (°F) and vapor pressure (psia) 132 | export const tempPvFromvRh = (v: number, rh: number, totalPressure: number) => { 133 | const rAir = 53.35; // Gas constant in units of ft-lbf / lbm - R 134 | const funcToZero = (temp: number) => { 135 | // The 144 is a conversion factor from psf to psi. The 469.67 is to go from F to R. 136 | const term1 = satPressFromTempIp(temp) * rh; 137 | const term2 = (totalPressure - rAir * (temp + 459.67) / (v * 144)); 138 | return term1 - term2; 139 | } 140 | const derivative = (temp: number) => { 141 | return dPvdT(rh, temp) + rAir / (v * 144); 142 | } 143 | // Employ the Newton-Raphson method. 144 | const testTemp = newtonRaphson(funcToZero, derivative, 80); 145 | return { temp: testTemp, pv: pvFromTempRh(testTemp, rh) }; 146 | } 147 | 148 | export const wetBulbRh = (wetBulb: number, rh: number, totalP: number) => { 149 | if (rh < 0 || rh > 1) { 150 | throw new Error('RH expected to be between 0 and 1'); 151 | } 152 | const funcToZero = (testTemp: number) => { 153 | const ω1 = ωFromWetbulbDryBulb(wetBulb, testTemp, totalP); 154 | const pv2 = rh * satPressFromTempIp(testTemp); 155 | const ω2 = wFromPv(pv2, totalP); 156 | return ω1 - ω2; 157 | } 158 | let updatedMaxTemp = 200; 159 | let updatedMinTemp = 0; 160 | let looping = true; 161 | let testTemp; 162 | while (looping) { 163 | testTemp = (updatedMaxTemp + updatedMinTemp) / 2; 164 | const result = funcToZero(testTemp); 165 | if (Math.abs(result) < 0.00001) { 166 | looping = false; 167 | } else { 168 | // Too low case 169 | if (result > 0) { 170 | updatedMinTemp = testTemp; 171 | } else { 172 | updatedMaxTemp = testTemp; 173 | } 174 | } 175 | } 176 | return { temp: testTemp, pv: pvFromTempRh(testTemp, rh) } 177 | } 178 | 179 | // temp: Dry bulb temperature in °F 180 | // ω: Humidity ratio 181 | // totalPressure: Total Pressure in psia. 182 | export const wetBulbFromTempω = (temp: number, ω: number, totalPressure: number) => { 183 | // Function we'd like to 0. A difference in ω's. 184 | const testWetbulbResult = (testWetbulb: number) => { 185 | const satωAtWetBulb = satHumidRatioFromTempIp(testWetbulb, totalPressure); 186 | return ((1093 - 0.556 * testWetbulb) * satωAtWetBulb - 0.24 * (temp - testWetbulb)) / 187 | (1093 + 0.444 * temp - testWetbulb) - ω; 188 | } 189 | let updatedMaxTemp = temp; 190 | let updatedMinTemp = 0; 191 | let testTemp = (updatedMaxTemp + updatedMinTemp) / 2; 192 | let iterations = 0; 193 | let testResult = testWetbulbResult(testTemp); 194 | while (Math.abs(testResult) > 0.000001) { 195 | if (iterations > 500) { 196 | throw new Error('Infinite loop in temp from Rh and Pv.'); 197 | } 198 | if (testResult > 0) { 199 | updatedMaxTemp = testTemp; 200 | testTemp = (updatedMaxTemp + updatedMinTemp) / 2; 201 | } else { 202 | updatedMinTemp = testTemp; 203 | testTemp = (updatedMaxTemp + updatedMinTemp) / 2; 204 | } 205 | testResult = testWetbulbResult(testTemp); 206 | iterations++; 207 | } 208 | return testTemp; 209 | } 210 | 211 | export const tempFromWetbulbω = (wetBulb: number, ω: number, totalPressure: number) => { 212 | const ωsatWetBulb = satHumidRatioFromTempIp(wetBulb, totalPressure); 213 | return ((1093 - 0.556 * wetBulb) * ωsatWetBulb + 0.24 * wetBulb - ω * (1093 - wetBulb)) / (0.444 * ω + 0.24); 214 | } 215 | 216 | export const ωFromWetbulbDryBulb = (wetbulbTemp: number, temp: number, totalPressure: number) => { 217 | const ωsatWetBulb = satHumidRatioFromTempIp(wetbulbTemp, totalPressure); 218 | return ((1093 - 0.556 * wetbulbTemp) * ωsatWetBulb - 0.24 * (temp - wetbulbTemp)) / (1093 + 0.444 * temp - wetbulbTemp); 219 | } 220 | 221 | export const vFromTempω = (temp: number, ω: number, totalPressure: number) => { 222 | return 0.370486 * (temp + 459.67) * (1 + 1.607858 * ω) / totalPressure; 223 | } 224 | 225 | export const tempFromvω = (v: number, ω: number, totalPressure: number) => { 226 | return (v * totalPressure) / (0.370486 * (1 + 1.607858 * ω)) - 459.67; 227 | } 228 | 229 | export const ωFromTempv = (temp: number, v: number, totalPressure: number) => { 230 | const numerator = ((totalPressure * v) / (0.370486 * (temp + 459.67))) - 1; 231 | return numerator / 1.607858; 232 | } 233 | 234 | // Calculate derivative of pv vs. T at given RH (0-1) and temp (°F) 235 | export const dPvdT = (rh: number, temp: number) => { 236 | if (rh < 0 || rh > 1) { 237 | throw Error('rh should be specified 0-1'); 238 | } 239 | const absTemp = temp + 459.67; 240 | const term1 = 241 | -c8 / (absTemp * absTemp) + 242 | c10 + 243 | 2 * c11 * absTemp + 244 | 3 * c12 * absTemp * absTemp + 245 | c13 / absTemp; 246 | return rh * satPressFromTempIp(temp) * term1; 247 | } 248 | 249 | // 250 | export const humidityRatioFromEnthalpyTemp = (enthalpy: number, temp: number) => { 251 | return (enthalpy - 0.24 * temp) / (1061 + 0.445 * temp); 252 | } 253 | 254 | export const enthalpyFromTempPv = (temp: number, pv: number, totalPressure: number) => { 255 | const ω = wFromPv(pv, totalPressure); 256 | return 0.24 * temp + ω * (1061 + 0.445 * temp); 257 | } 258 | 259 | export const pvFromEnthalpyTemp = (enthalpy: number, temp: number, totalPressure: number) => { 260 | return pvFromw(humidityRatioFromEnthalpyTemp(enthalpy, temp), totalPressure); 261 | } 262 | 263 | export const satTempAtEnthalpy = (enthalpy: number, totalPressure: number) => { 264 | let currentLowTemp = 0; 265 | let currentHighTemp = 200; 266 | let error = 1; 267 | let testTemp = (currentLowTemp + currentHighTemp) / 2; 268 | let iterations = 0; 269 | do { 270 | iterations++; 271 | if (iterations > 500) { 272 | throw Error('Inifite loop in satTempAtEnthalpy'); 273 | } 274 | testTemp = (currentLowTemp + currentHighTemp) / 2; 275 | const testSatHumidityRatio = satHumidRatioFromTempIp(testTemp, totalPressure); 276 | const testHumidityRatio = humidityRatioFromEnthalpyTemp(enthalpy, testTemp); 277 | error = testSatHumidityRatio - testHumidityRatio; 278 | if (testSatHumidityRatio > testHumidityRatio) { 279 | currentHighTemp = testTemp; 280 | } else { 281 | currentLowTemp = testTemp; 282 | } 283 | } while (Math.abs(error) > 0.00005); 284 | return testTemp; 285 | } 286 | -------------------------------------------------------------------------------- /src/components/chart/psychrometrics/StatePointω.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | 3 | import { 4 | wFromPv, 5 | pvFromTempRh, 6 | vFromTempω, 7 | totalPressure, 8 | } from './Core'; 9 | 10 | interface StatePointω { 11 | id: string; 12 | temperature: number; 13 | humidityRatio: number; 14 | pv: number; 15 | name: string; 16 | humidity: number; 17 | v: number; // Specific Volume 18 | } 19 | 20 | class StatePointω implements StatePointω { 21 | constructor(temp: number, humidity: number, name: string) { 22 | this.id = uuid(); 23 | // this.temperature = getRandomInt(minTempF, maxTemp); 24 | // const maxωrange = Math.min(satHumidRatioFromTempIp(this.temperature, totalPressure), maxω); 25 | // this.humidityRatio = Math.round(getRandomArbitrary(0, maxωrange) / 0.001) * 0.001; 26 | this.temperature = 75; 27 | this.humidity = 0.4; 28 | this.pv = pvFromTempRh(this.temperature, this.humidity); 29 | this.v = vFromTempω(this.temperature, this.humidityRatio, totalPressure); 30 | this.humidityRatio = wFromPv(this.pv, totalPressure); 31 | // vFromTempω 32 | console.log(this.temperature, this.humidityRatio, this.pv, this.v); 33 | this.name = name; 34 | } 35 | } 36 | 37 | export default StatePointω; 38 | -------------------------------------------------------------------------------- /src/components/chart/psychrometrics/model.ts: -------------------------------------------------------------------------------- 1 | export interface IWetBulbLine { 2 | wetBulbTemp?: number; 3 | data?: { x: number, y: number }[]; 4 | midTemp?: number; 5 | midPv?: number; 6 | x?: number; 7 | y?: number; 8 | rotationAngle?: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/chart/psychrometrics/psychrometrics.less: -------------------------------------------------------------------------------- 1 | .constantTemp:hover { 2 | stroke-width: 1; 3 | pointer-events: all; 4 | } 5 | 6 | .ticks { 7 | font-size: 10px; 8 | font-family: sans-serif; 9 | background: white; 10 | } 11 | 12 | .state-label { 13 | display: inline-block; 14 | width: 15em; 15 | } 16 | 17 | #vizcontainer { 18 | 19 | } 20 | 21 | .states { 22 | padding-top: 0.5em; 23 | padding-bottom: 0.5em; 24 | } 25 | 26 | .state { 27 | padding: 10px; 28 | border-bottom-style: solid; 29 | border-bottom-color: gray; 30 | } 31 | 32 | .remove-button { 33 | padding-top: 5px; 34 | } 35 | 36 | .base-chart-options { 37 | padding: 10px; 38 | margin-top: 15px; 39 | margin-bottom: 15px; 40 | margin-right: 5px; 41 | border-style: solid; 42 | border-color: gray; 43 | } 44 | 45 | input { 46 | padding: 5px; 47 | cursor: inherit; 48 | } 49 | 50 | .base-chart-options input { 51 | margin-right: 10px; 52 | } 53 | 54 | .base-chart-options > div { 55 | margin-bottom: 5px; 56 | } 57 | 58 | .download-buttons { 59 | margin-top: 10px; 60 | margin-bottom: 10px; 61 | } 62 | 63 | .input-options { 64 | margin-top: 10px; 65 | } 66 | 67 | label { 68 | cursor: inherit; 69 | } 70 | 71 | .checkbox-option { 72 | cursor: pointer; 73 | } 74 | 75 | .checkbox-option:hover { 76 | background: lightgray; 77 | user-select: none; 78 | -moz-user-select: none; 79 | -ms-user-select: none; 80 | -webkit-user-select: none; 81 | } 82 | 83 | .chart-container { 84 | width: 100%; 85 | height: 100%; 86 | position: relative; 87 | } 88 | 89 | .tooltip { 90 | position: absolute; 91 | left: 24px; 92 | top: 24px; 93 | font-weight: bold; 94 | &-content { 95 | display: flex; 96 | } 97 | } -------------------------------------------------------------------------------- /src/components/editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Layout } from 'antd'; 3 | import { SelectParam } from 'antd/lib/menu'; 4 | 5 | import { i18nClient } from '../../i18n'; 6 | import { Sider, Content, Menus } from '../layout'; 7 | import StructureContainer from '../../containers/StructureContainer'; 8 | import { ErrorBoundary } from '../error'; 9 | import '../../styles/index.less'; 10 | 11 | i18nClient(); 12 | 13 | interface IState { 14 | panelKey: string; 15 | } 16 | 17 | class Editor extends Component<{}, IState> { 18 | state: IState = { 19 | panelKey: 'structure:series', 20 | } 21 | 22 | handleSelectMenu = (param: SelectParam) => { 23 | const { key } = param; 24 | this.setState({ 25 | panelKey: key, 26 | }); 27 | } 28 | 29 | render() { 30 | const { panelKey } = this.state; 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default Editor; 47 | -------------------------------------------------------------------------------- /src/components/editor/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Editor } from './Editor'; 2 | -------------------------------------------------------------------------------- /src/components/error/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ErrorInfo } from 'react'; 2 | 3 | interface IState { 4 | error?: Error; 5 | errorInfo?: ErrorInfo; 6 | } 7 | 8 | class ErrorBoundary extends Component<{}, IState> { 9 | state: IState = { 10 | error: null, 11 | errorInfo: null, 12 | } 13 | 14 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 15 | this.setState({ 16 | error, 17 | errorInfo, 18 | }); 19 | } 20 | 21 | componentWillUnmount() { 22 | this.setState({ 23 | error: null, 24 | errorInfo: null, 25 | }); 26 | } 27 | 28 | render() { 29 | const { children } = this.props; 30 | const { error } = this.state; 31 | return error ? ( 32 |
33 | {error.toString()} 34 |
35 | ) : children; 36 | } 37 | } 38 | 39 | export default ErrorBoundary; 40 | -------------------------------------------------------------------------------- /src/components/error/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as ErrorBoundary } from './ErrorBoundary'; 2 | -------------------------------------------------------------------------------- /src/components/form/DynamicForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import uuid from 'uuid'; 3 | import { Collapse, Button, Empty } from 'antd'; 4 | import i18next from 'i18next'; 5 | import debounce from 'lodash/debounce'; 6 | import { CopyOutlined, DeleteOutlined } from '@ant-design/icons'; 7 | import Form, { FormProps } from './Form'; 8 | 9 | export interface DynamicFormProps extends Omit { 10 | values?: { [key: string]: DynamicData }; 11 | label?: React.ReactNode; 12 | onChange?: (datas: { [key: string]: DynamicData }) => void; 13 | onChangeActiveKey?: (activeKey: string[]) => void; 14 | delay?: number; 15 | addButton?: boolean; 16 | deleteButton?: boolean; 17 | cloneButton?: boolean; 18 | allDelete?: boolean; 19 | activeKey?: string[]; 20 | } 21 | 22 | interface DynamicData { 23 | [key: string]: any; 24 | } 25 | 26 | interface IState { 27 | datas?: { [key: string]: DynamicData }; 28 | activeKey?: string[]; 29 | } 30 | 31 | class DynamicForm extends Component { 32 | forms: { [key: string]: typeof Form } = {}; 33 | 34 | state: IState = { 35 | datas: this.props.values || { [uuid()]: {} }, 36 | activeKey: this.props.activeKey || [], 37 | }; 38 | 39 | UNSAFE_componentWillReceiveProps(nextProps: DynamicFormProps) { 40 | if (JSON.stringify(nextProps.values) !== JSON.stringify(this.props.values)) { 41 | this.setState({ 42 | datas: Object.assign({}, nextProps.values), 43 | }); 44 | } 45 | if (JSON.stringify(nextProps.activeKey) !== JSON.stringify(this.props.activeKey)) { 46 | this.setState({ 47 | activeKey: nextProps.activeKey, 48 | }); 49 | } 50 | } 51 | 52 | handleAddForm = () => { 53 | const id = uuid(); 54 | this.setState( 55 | { 56 | datas: Object.assign({}, this.state.datas, { [id]: {} }), 57 | activeKey: this.state.activeKey.concat(id), 58 | }, 59 | () => { 60 | const { onChange, onChangeActiveKey } = this.props; 61 | if (onChange) { 62 | onChange(this.state.datas); 63 | } 64 | if (onChangeActiveKey) { 65 | onChangeActiveKey(this.state.activeKey); 66 | } 67 | }, 68 | ); 69 | }; 70 | 71 | handleCloneForm = (data: DynamicData) => { 72 | const id = uuid(); 73 | this.setState( 74 | { 75 | datas: Object.assign({}, this.state.datas, { [id]: data }), 76 | activeKey: this.state.activeKey.concat(id), 77 | }, 78 | () => { 79 | const { onChange, onChangeActiveKey } = this.props; 80 | if (onChange) { 81 | onChange(this.state.datas); 82 | } 83 | if (onChangeActiveKey) { 84 | onChangeActiveKey(this.state.activeKey); 85 | } 86 | }, 87 | ); 88 | }; 89 | 90 | handleRemoveForm = (id: string) => { 91 | delete this.state.datas[id]; 92 | this.setState( 93 | { 94 | datas: this.state.datas, 95 | activeKey: this.state.activeKey.filter(activeKey => activeKey !== id), 96 | }, 97 | () => { 98 | const { onChange, onChangeActiveKey } = this.props; 99 | if (onChange) { 100 | onChange(this.state.datas); 101 | } 102 | if (onChangeActiveKey) { 103 | onChangeActiveKey(this.state.activeKey); 104 | } 105 | }, 106 | ); 107 | }; 108 | 109 | handleValuesChange = (changedValues: any, allValues: any, formKey: string) => { 110 | const targetDatas = Object.assign({}, this.state.datas[formKey], allValues); 111 | const datas = Object.assign({}, this.state.datas, { [formKey]: targetDatas }); 112 | const { onChange } = this.props; 113 | if (onChange) { 114 | onChange(datas); 115 | } 116 | }; 117 | 118 | handleChangeActiveKey = (activeKey: string | string[]) => { 119 | this.setState({ 120 | activeKey, 121 | }); 122 | const { onChangeActiveKey } = this.props; 123 | if (onChangeActiveKey) { 124 | onChangeActiveKey(activeKey as string[]); 125 | } 126 | }; 127 | 128 | render() { 129 | const { 130 | formSchema, 131 | label, 132 | addButton = true, 133 | onChange, 134 | onChangeActiveKey, 135 | activeKey: activeKeys, 136 | allDelete, 137 | cloneButton = true, 138 | deleteButton = true, 139 | delay = 300, 140 | ...other 141 | } = this.props; 142 | const { datas, activeKey } = this.state; 143 | const datasLength = Object.keys(datas).length; 144 | return ( 145 |
146 | {datasLength ? ( 147 | console.log(name, info)}> 148 | 149 | {Object.keys(datas).map((key, index) => { 150 | return ( 151 | { 160 | e.stopPropagation(); 161 | this.handleCloneForm(datas[key]); 162 | }} 163 | /> 164 | ), 165 | deleteButton && datasLength > 1 ? ( 166 | { 170 | e.stopPropagation(); 171 | this.handleRemoveForm(key); 172 | }} 173 | /> 174 | ) : ( 175 | allDelete && ( 176 | { 180 | e.stopPropagation(); 181 | this.handleRemoveForm(key); 182 | }} 183 | /> 184 | ) 185 | ), 186 | ]} 187 | > 188 |
{ 191 | this.forms[key] = c; 192 | }} 193 | formSchema={formSchema} 194 | formKey={key} 195 | onValuesChange={debounce(this.handleValuesChange, delay < 0 ? 0 : delay)} 196 | values={datas[key]} 197 | /> 198 |
199 | ); 200 | })} 201 |
202 |
203 | ) : ( 204 | 205 | )} 206 | {addButton && ( 207 | 210 | )} 211 |
212 | ); 213 | } 214 | } 215 | 216 | export default DynamicForm; 217 | -------------------------------------------------------------------------------- /src/components/form/Form.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form as AntForm, Col, Row, Tooltip, Input, Slider, Select, InputNumber, Divider, Switch } from 'antd'; 3 | import { Rule, FormInstance, FormProps as AntFormProps } from 'antd/lib/form'; 4 | import isEmpty from 'lodash/isEmpty'; 5 | import { QuestionCircleOutlined } from '@ant-design/icons'; 6 | import i18next from 'i18next'; 7 | 8 | import { ColorPicker } from '../picker'; 9 | import DynamicForm from './DynamicForm'; 10 | import { Label } from '../label'; 11 | 12 | export type FormComponentType = 13 | | 'divider' 14 | | 'label' 15 | | 'text' 16 | | 'textarea' 17 | | 'number' 18 | | 'boolean' 19 | | 'select' 20 | | 'template' 21 | | 'templatearea' 22 | | 'json' 23 | | 'cron' 24 | | 'tags' 25 | | 'dynamic' 26 | | 'custom' 27 | | 'password' 28 | | 'color' 29 | | 'form' 30 | | 'slider'; 31 | 32 | export type FormSchema = MultipleFormConfig | FormConfig; 33 | 34 | export interface MultipleFormConfig { 35 | [key: string]: FormConfig; 36 | } 37 | 38 | export interface SelectItemConfig { 39 | label: string; 40 | value: string | number; 41 | forms?: FormSchema; 42 | } 43 | 44 | export type SelectMode = 'multiple' | 'tags'; 45 | 46 | export interface FormConfig { 47 | type?: FormComponentType; 48 | disabled?: boolean; 49 | icon?: string; 50 | extra?: React.ReactNode; 51 | help?: React.ReactNode; 52 | description?: React.ReactNode; 53 | span?: number; 54 | max?: number; 55 | min?: number; 56 | placeholder?: string; 57 | valuePropName?: string; 58 | required?: boolean; 59 | initialValue?: any; 60 | label?: React.ReactNode; 61 | style?: React.CSSProperties; 62 | /** 63 | * Press enter 64 | */ 65 | onPressEnter?: () => void; 66 | /** 67 | * Required Items when type is Select 68 | */ 69 | items?: SelectItemConfig[]; 70 | rules?: Rule[]; 71 | /** 72 | * Required Render when type is Custom 73 | */ 74 | render?: (form: FormInstance, values: any, disabled: boolean, validate: (errors: any) => void) => React.ReactNode; 75 | hasFeedback?: boolean; 76 | /** 77 | * Required Component when type is Custom 78 | */ 79 | component?: React.ComponentType; 80 | /** 81 | * Required Mode when type is Select 82 | */ 83 | mode?: SelectMode; 84 | /** 85 | * If type is dynamic, require formSchema 86 | */ 87 | forms?: FormSchema; 88 | /** 89 | * If type is dynamic, require header 90 | */ 91 | header?: React.ReactNode; 92 | step?: number; 93 | ref?: React.Ref; 94 | } 95 | 96 | export interface FormProps extends Omit { 97 | /** 98 | * Row gutter 99 | * @default 16 100 | */ 101 | gutter?: number; 102 | formKey?: string; 103 | values?: any; 104 | formSchema?: FormSchema; 105 | render?: (form: FormInstance) => React.ReactNode; 106 | onValuesChange?: (changedValues: any, allValues: any, formKey?: string) => void; 107 | children?: React.ReactNode; 108 | } 109 | 110 | const WrappedForm = React.forwardRef((props, ref) => { 111 | const { formKey, gutter = 16, formSchema, onValuesChange, values, layout = 'vertical', ...other } = props; 112 | const [selectedValues, setSelectedValues] = useState>({}); 113 | const [errors, setErrors] = useState>({}); 114 | const handleSelect = (selectedValue: any, key: string) => { 115 | setSelectedValues(Object.assign({}, selectedValues, { [key]: selectedValue })); 116 | }; 117 | const handleValidate = (errors: any) => { 118 | setErrors(errors); 119 | }; 120 | // const handleDefaultValidator = async (_rule: any, _value: any, callback: (errors?: any) => void) => { 121 | // console.log(errors); 122 | // if (errors && errors.length) { 123 | // throw errors; 124 | // } 125 | // // callback(); 126 | // }; 127 | const handleValuesChange = (changedValues: any, allValues: any) => { 128 | if (onValuesChange) { 129 | onValuesChange(changedValues, allValues, formKey); 130 | } 131 | }; 132 | const createFormItem = (key: string | string[], formConfig: FormConfig) => { 133 | const { colon = false, values, form } = props; 134 | let component = null as any; 135 | const { 136 | disabled, 137 | icon, 138 | extra, 139 | help, 140 | description, 141 | span, 142 | max, 143 | min, 144 | placeholder, 145 | valuePropName, 146 | items, 147 | required, 148 | rules, 149 | label, 150 | type, 151 | render, 152 | hasFeedback, 153 | mode, 154 | style, 155 | forms, 156 | header, 157 | onPressEnter, 158 | initialValue, 159 | step, 160 | ref, 161 | } = formConfig; 162 | let value: any; 163 | if (Array.isArray(key)) { 164 | value = !isEmpty(values) 165 | ? key.reduce((prev, curr) => { 166 | if (typeof prev !== 'object') { 167 | return prev; 168 | } 169 | return prev[curr]; 170 | }, values) 171 | : initialValue; 172 | } 173 | if (typeof value === 'undefined') { 174 | value = initialValue; 175 | } 176 | let newRules: Rule[] = required 177 | ? [{ required: true, message: i18next.t('validation.enter-arg', { arg: label }) }] 178 | : []; 179 | if (rules) { 180 | newRules = newRules.concat(rules); 181 | } 182 | let selectFormItems = null; 183 | switch (type) { 184 | case 'divider': 185 | component = ( 186 | 187 | {label} 188 | 189 | ); 190 | return component; 191 | case 'label': 192 | component =
355 | {type === 'form' ? ( 356 | component 357 | ) : ( 358 | 368 | {component} 369 | 370 | )} 371 | 372 | {selectFormItems} 373 | 374 | ); 375 | }; 376 | const createFormItemList = () => { 377 | let components; 378 | const schema = formSchema as MultipleFormConfig; 379 | components = Object.keys(formSchema).map(key => createFormItem(key, schema[key])); 380 | return {components}; 381 | }; 382 | return ( 383 | 384 | {createFormItemList()} 385 | 386 | ); 387 | }); 388 | 389 | type Form = typeof WrappedForm 390 | & { 391 | Item: typeof AntForm.Item; 392 | Provider: typeof AntForm.Provider; 393 | } 394 | 395 | const Form: Form = WrappedForm as Form; 396 | 397 | Form.Item = AntForm.Item; 398 | Form.Provider = AntForm.Provider; 399 | 400 | export default Form; 401 | -------------------------------------------------------------------------------- /src/components/form/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Form as AntForm, 4 | Row, 5 | Divider, 6 | Input, 7 | InputNumber, 8 | Switch, 9 | Select, 10 | Tooltip, 11 | Col, 12 | Icon, 13 | } from 'antd'; 14 | import { ValidationRule } from 'antd/lib/form'; 15 | import { WrappedFormUtils } from 'antd/lib/form/Form'; 16 | import i18next from 'i18next'; 17 | import isEmpty from 'lodash/isEmpty'; 18 | 19 | import DynamicForm from './DynamicForm'; 20 | import { FormSchema, FormConfig, MultipleFormConfig } from './LegacyForm'; 21 | 22 | export interface FormItemProps { 23 | /** 24 | * Row gutter 25 | * @default 16 26 | */ 27 | gutter?: number; 28 | colon?: boolean; 29 | formKey?: string; 30 | /** 31 | * Whether form schema is single 32 | * @default false 33 | */ 34 | isSingle?: boolean; 35 | values?: any; 36 | formSchema?: FormSchema; 37 | form: WrappedFormUtils; 38 | render?: (form: WrappedFormUtils) => React.ReactNode; 39 | } 40 | 41 | interface IState { 42 | errors: any; 43 | selectedValues: any; 44 | } 45 | 46 | class FormItem extends Component { 47 | state: IState = { 48 | errors: null, 49 | selectedValues: {}, 50 | } 51 | 52 | UNSAFE_componentWillReceiveProps(nextProps: FormItemProps) { 53 | if (JSON.stringify(nextProps.values) !== JSON.stringify(this.props.values)) { 54 | this.setState({ 55 | selectedValues: {}, 56 | }); 57 | } 58 | } 59 | 60 | createFormItem = (key: string, formConfig: FormConfig) => { 61 | const { colon = false, isSingle, values, form } = this.props; 62 | let component = null; 63 | const { 64 | disabled, 65 | icon, 66 | extra, 67 | help, 68 | description, 69 | span, 70 | max, 71 | min, 72 | placeholder, 73 | valuePropName, 74 | items, 75 | required, 76 | rules, 77 | initialValue, 78 | label, 79 | type, 80 | render, 81 | hasFeedback, 82 | mode, 83 | style, 84 | forms, 85 | header, 86 | onPressEnter, 87 | ref, 88 | } = formConfig; 89 | let value = !isEmpty(values) ? values[key] : initialValue; 90 | if (isSingle) { 91 | value = values || initialValue; 92 | } 93 | let newRules = required ? [{ required: true, message: i18next.t('validation.enter-arg', { arg: label }) }] : [] as ValidationRule[]; 94 | if (rules) { 95 | newRules = newRules.concat(rules); 96 | } 97 | let selectFormItems = null; 98 | switch (type) { 99 | case 'divider': 100 | component = {label}; 101 | return component; 102 | case 'label': 103 | component = ( 104 | 105 | {initialValue} 106 | 107 | ); 108 | break; 109 | case 'text': 110 | component = ; 111 | break; 112 | case 'password': 113 | component = ; 114 | break; 115 | case 'textarea': 116 | component = ; 117 | break; 118 | case 'number': 119 | component = ; 120 | break; 121 | case 'boolean': 122 | component = ; 123 | if (typeof value === 'undefined') { 124 | value = true; 125 | } 126 | break; 127 | case 'select': 128 | value = this.state.selectedValues[key] || value; 129 | component = ( 130 | 145 | ); 146 | break; 147 | case 'tags': 148 | component = ( 149 | 164 | ); 165 | break; 166 | case 'dynamic': 167 | component = ; 168 | break; 169 | case 'custom': 170 | component = render ? render(form, values, disabled, this.validators.validate) : (formConfig.component ? ( 171 | 180 | ) : null); 181 | break; 182 | default: 183 | component = ; 184 | } 185 | const newLabel = description ? ( 186 | <> 187 | {icon ? : null} 188 | {label} 189 | 190 | 191 | 192 | 193 | 194 | 195 | ) : ( 196 | <> 197 | {icon ? : null} 198 | {label} 199 | 200 | ); 201 | return ( 202 | 203 | 204 | 211 | { 212 | form.getFieldDecorator(key, { 213 | initialValue: value, 214 | rules: newRules, 215 | valuePropName: typeof value === 'boolean' ? 'checked' : valuePropName || 'value', 216 | })(component) 217 | } 218 | 219 | 220 | {selectFormItems} 221 | 222 | ); 223 | } 224 | 225 | handlers = { 226 | onSelect: (selectedValue: any, key: any) => { 227 | const { selectedValues } = this.state; 228 | this.setState({ 229 | selectedValues: Object.assign({}, selectedValues, { [key]: selectedValue }), 230 | }); 231 | }, 232 | } 233 | 234 | validators = { 235 | validate: (errors: any) => { 236 | this.setState({ 237 | errors, 238 | }); 239 | }, 240 | aceEditorValidator: (_rule: any, _value: any, callback: any) => { 241 | if (this.state.errors && this.state.errors.length) { 242 | callback(this.state.errors); 243 | return; 244 | } 245 | callback(); 246 | }, 247 | cronValidator: (_rule: any, _value: any, callback: any) => { 248 | if (this.state.errors && this.state.errors.length) { 249 | callback(this.state.errors); 250 | return; 251 | } 252 | callback(); 253 | }, 254 | } 255 | 256 | createForm = () => { 257 | const { gutter = 16, isSingle, formKey, formSchema } = this.props; 258 | let components; 259 | if (isSingle) { 260 | components = this.createFormItem(formKey, formSchema); 261 | } else { 262 | const schema = formSchema as MultipleFormConfig; 263 | components = Object.keys(formSchema).map(key => this.createFormItem(key, schema[key])); 264 | } 265 | return ( 266 | 267 | {components} 268 | 269 | ); 270 | } 271 | 272 | render() { 273 | const { 274 | children, 275 | formSchema, 276 | form, 277 | render, 278 | } = this.props; 279 | let component; 280 | if (formSchema) { 281 | component = this.createForm(); 282 | } else if (typeof children === 'function') { 283 | component = children(form); 284 | } else if (typeof render === 'function') { 285 | component = render(form); 286 | } else { 287 | component = children; 288 | } 289 | return component; 290 | } 291 | } 292 | 293 | export default FormItem; 294 | -------------------------------------------------------------------------------- /src/components/form/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Form } from './Form'; 2 | 3 | export { default as DynamicForm } from './DynamicForm'; 4 | 5 | export { default as FormItem } from './FormItem'; 6 | -------------------------------------------------------------------------------- /src/components/form/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": { 3 | "Date": ["Tue, 17 Mar 2020 04:30:26 GMT"], 4 | "Content-Type": ["application/json"], 5 | "Content-Length": ["4005"], 6 | "Connection": ["keep-alive"], 7 | "x-amzn-RequestId": ["3326103a-a2f3-4bb3-96d2-93c29104e718"], 8 | "x-amz-apigw-id": ["JhHPbEXfoE0FTZw="], 9 | "X-Amzn-Trace-Id": ["Root=1-5e705262-8653502326579b564d8570e7;Sampled=0"] 10 | }, 11 | "body": { 12 | "response": { 13 | "command": "SELECT", 14 | "rowCount": 14, 15 | "oid": null, 16 | "rows": [ 17 | { 18 | "dga_acq_date": "2019-01-01T00:00:00.000Z", 19 | "h2": "5.300000", 20 | "ch4": "16.900000", 21 | "c2h4": "9.600000", 22 | "c2h6": "7.500000", 23 | "c2h2": "0.000000", 24 | "co": "171.800003" 25 | }, 26 | { 27 | "dga_acq_date": "2018-01-01T00:00:00.000Z", 28 | "h2": "5.900000", 29 | "ch4": "8.500000", 30 | "c2h4": "15.800000", 31 | "c2h6": "1.000000", 32 | "c2h2": "0.000000", 33 | "co": "248.600006" 34 | }, 35 | { 36 | "dga_acq_date": "2017-01-01T00:00:00.000Z", 37 | "h2": "5.540000", 38 | "ch4": "4.400000", 39 | "c2h4": "7.140000", 40 | "c2h6": "2.190000", 41 | "c2h2": "0.000000", 42 | "co": "132.240005" 43 | }, 44 | { 45 | "dga_acq_date": "2015-01-15T00:00:00.000Z", 46 | "h2": "8.000000", 47 | "ch4": "2.200000", 48 | "c2h4": "0.700000", 49 | "c2h6": "1.300000", 50 | "c2h2": "0.000000", 51 | "co": "36.200001" 52 | }, 53 | { 54 | "dga_acq_date": "2015-01-01T00:00:00.000Z", 55 | "h2": "8.000000", 56 | "ch4": "8.600000", 57 | "c2h4": "10.100000", 58 | "c2h6": "4.100000", 59 | "c2h2": "0.000000", 60 | "co": "226.899994" 61 | }, 62 | { 63 | "dga_acq_date": "2013-01-01T00:00:00.000Z", 64 | "h2": "8.000000", 65 | "ch4": "5.000000", 66 | "c2h4": "7.000000", 67 | "c2h6": "1.000000", 68 | "c2h2": "0.000000", 69 | "co": "156.000000" 70 | }, 71 | { 72 | "dga_acq_date": "2012-10-01T00:00:00.000Z", 73 | "h2": "11.000000", 74 | "ch4": "3.000000", 75 | "c2h4": "1.800000", 76 | "c2h6": "0.500000", 77 | "c2h2": "0.000000", 78 | "co": "79.300003" 79 | }, 80 | { 81 | "dga_acq_date": "2012-09-01T00:00:00.000Z", 82 | "h2": "11.000000", 83 | "ch4": "3.700000", 84 | "c2h4": "3.300000", 85 | "c2h6": "0.500000", 86 | "c2h2": "0.000000", 87 | "co": "105.000000" 88 | }, 89 | { 90 | "dga_acq_date": "2012-05-01T00:00:00.000Z", 91 | "h2": "9.100000", 92 | "ch4": "4.900000", 93 | "c2h4": "1.400000", 94 | "c2h6": "1.200000", 95 | "c2h2": "0.000000", 96 | "co": "12.600000" 97 | }, 98 | { 99 | "dga_acq_date": "2012-04-01T00:00:00.000Z", 100 | "h2": "9.100000", 101 | "ch4": "4.900000", 102 | "c2h4": "1.400000", 103 | "c2h6": "1.200000", 104 | "c2h2": "0.000000", 105 | "co": "236.800003" 106 | }, 107 | { 108 | "dga_acq_date": "2012-01-15T00:00:00.000Z", 109 | "h2": "6.000000", 110 | "ch4": "5.000000", 111 | "c2h4": "1.000000", 112 | "c2h6": "1.000000", 113 | "c2h2": "0.000000", 114 | "co": "328.000000" 115 | }, 116 | { 117 | "dga_acq_date": "2012-01-01T00:00:00.000Z", 118 | "h2": "6.000000", 119 | "ch4": "5.100000", 120 | "c2h4": "2.200000", 121 | "c2h6": "1.200000", 122 | "c2h2": "0.000000", 123 | "co": "261.299988" 124 | }, 125 | { 126 | "dga_acq_date": "2011-12-01T00:00:00.000Z", 127 | "h2": "2.000000", 128 | "ch4": "4.000000", 129 | "c2h4": "1.000000", 130 | "c2h6": "1.000000", 131 | "c2h2": "0.000000", 132 | "co": "337.000000" 133 | }, 134 | { 135 | "dga_acq_date": "2010-01-01T00:00:00.000Z", 136 | "h2": "4.700000", 137 | "ch4": "2.400000", 138 | "c2h4": "5.600000", 139 | "c2h6": "3.200000", 140 | "c2h2": "0.000000", 141 | "co": "148.199997" 142 | } 143 | ], 144 | "fields": [ 145 | { 146 | "name": "dga_acq_date", 147 | "tableID": 17410, 148 | "columnID": 1, 149 | "dataTypeID": 1114, 150 | "dataTypeSize": 8, 151 | "dataTypeModifier": -1, 152 | "format": "text" 153 | }, 154 | { 155 | "name": "h2", 156 | "tableID": 0, 157 | "columnID": 0, 158 | "dataTypeID": 1700, 159 | "dataTypeSize": -1, 160 | "dataTypeModifier": 4194314, 161 | "format": "text" 162 | }, 163 | { 164 | "name": "ch4", 165 | "tableID": 0, 166 | "columnID": 0, 167 | "dataTypeID": 1700, 168 | "dataTypeSize": -1, 169 | "dataTypeModifier": 4194314, 170 | "format": "text" 171 | }, 172 | { 173 | "name": "c2h4", 174 | "tableID": 0, 175 | "columnID": 0, 176 | "dataTypeID": 1700, 177 | "dataTypeSize": -1, 178 | "dataTypeModifier": 4194314, 179 | "format": "text" 180 | }, 181 | { 182 | "name": "c2h6", 183 | "tableID": 0, 184 | "columnID": 0, 185 | "dataTypeID": 1700, 186 | "dataTypeSize": -1, 187 | "dataTypeModifier": 4194314, 188 | "format": "text" 189 | }, 190 | { 191 | "name": "c2h2", 192 | "tableID": 0, 193 | "columnID": 0, 194 | "dataTypeID": 1700, 195 | "dataTypeSize": -1, 196 | "dataTypeModifier": 4194314, 197 | "format": "text" 198 | }, 199 | { 200 | "name": "co", 201 | "tableID": 0, 202 | "columnID": 0, 203 | "dataTypeID": 1700, 204 | "dataTypeSize": -1, 205 | "dataTypeModifier": 4194314, 206 | "format": "text" 207 | } 208 | ], 209 | "_parsers": [null, null, null, null, null, null, null], 210 | "_types": { 211 | "_types": { 212 | "arrayParser": {}, 213 | "builtins": { 214 | "BOOL": 16, 215 | "BYTEA": 17, 216 | "CHAR": 18, 217 | "INT8": 20, 218 | "INT2": 21, 219 | "INT4": 23, 220 | "REGPROC": 24, 221 | "TEXT": 25, 222 | "OID": 26, 223 | "TID": 27, 224 | "XID": 28, 225 | "CID": 29, 226 | "JSON": 114, 227 | "XML": 142, 228 | "PG_NODE_TREE": 194, 229 | "SMGR": 210, 230 | "PATH": 602, 231 | "POLYGON": 604, 232 | "CIDR": 650, 233 | "FLOAT4": 700, 234 | "FLOAT8": 701, 235 | "ABSTIME": 702, 236 | "RELTIME": 703, 237 | "TINTERVAL": 704, 238 | "CIRCLE": 718, 239 | "MACADDR8": 774, 240 | "MONEY": 790, 241 | "MACADDR": 829, 242 | "INET": 869, 243 | "ACLITEM": 1033, 244 | "BPCHAR": 1042, 245 | "VARCHAR": 1043, 246 | "DATE": 1082, 247 | "TIME": 1083, 248 | "TIMESTAMP": 1114, 249 | "TIMESTAMPTZ": 1184, 250 | "INTERVAL": 1186, 251 | "TIMETZ": 1266, 252 | "BIT": 1560, 253 | "VARBIT": 1562, 254 | "NUMERIC": 1700, 255 | "REFCURSOR": 1790, 256 | "REGPROCEDURE": 2202, 257 | "REGOPER": 2203, 258 | "REGOPERATOR": 2204, 259 | "REGCLASS": 2205, 260 | "REGTYPE": 2206, 261 | "UUID": 2950, 262 | "TXID_SNAPSHOT": 2970, 263 | "PG_LSN": 3220, 264 | "PG_NDISTINCT": 3361, 265 | "PG_DEPENDENCIES": 3402, 266 | "TSVECTOR": 3614, 267 | "TSQUERY": 3615, 268 | "GTSVECTOR": 3642, 269 | "REGCONFIG": 3734, 270 | "REGDICTIONARY": 3769, 271 | "JSONB": 3802, 272 | "REGNAMESPACE": 4089, 273 | "REGROLE": 4096 274 | } 275 | }, 276 | "text": {}, 277 | "binary": {} 278 | }, 279 | "RowCtor": null, 280 | "rowAsArray": false 281 | } 282 | }, 283 | "statusCode": "OK", 284 | "statusCodeValue": 200 285 | } 286 | -------------------------------------------------------------------------------- /src/components/label/Label.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IProps { 4 | value?: any; 5 | } 6 | 7 | const Label: React.SFC = props => { 8 | const { value } = props; 9 | return ( 10 | 11 | {value} 12 | 13 | ); 14 | }; 15 | 16 | export default Label; 17 | -------------------------------------------------------------------------------- /src/components/label/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label'; 2 | -------------------------------------------------------------------------------- /src/components/layout/Content.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Layout } from 'antd'; 3 | import Split from 'react-split'; 4 | 5 | import { DataGridPanel, ChartPanel, StructurePanel, StylePanel, Panel } from '../panel'; 6 | import ChartContainer from '../../containers/ChartContainer'; 7 | 8 | const panels: Record = { 9 | structure: StructurePanel, 10 | style: StylePanel, 11 | } 12 | 13 | interface IProps { 14 | panelKey: string; 15 | } 16 | 17 | class Content extends Component { 18 | render() { 19 | const { panelKey } = this.props; 20 | const [mainKey, subKey] = panelKey.split(':'); 21 | const PanelComponent = panels[mainKey]; 22 | return ( 23 | 24 | 30 | 31 | 32 | 33 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Content; 56 | -------------------------------------------------------------------------------- /src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Layout } from 'antd'; 3 | 4 | class Header extends Component { 5 | render() { 6 | return ( 7 | 8 | test 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default Header; 15 | -------------------------------------------------------------------------------- /src/components/layout/Menus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TableOutlined, LineChartOutlined, DashboardOutlined } from '@ant-design/icons'; 3 | import { Layout, Menu } from 'antd'; 4 | import i18next from 'i18next'; 5 | import { useHistory } from 'react-router'; 6 | 7 | const Menus = () => { 8 | const history = useHistory(); 9 | return ( 10 | 11 | 12 | history.push('/')}> 13 | 14 | {i18next.t('dashboard.dashboard')} 15 | 16 | history.push('/psychrometrics/kelvin')}> 17 | 18 | {i18next.t('dashboard.dashboard')} 19 | 20 | history.push('/table')}> 21 | 22 | {i18next.t('dashboard.dashboard')} 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Menus; 30 | -------------------------------------------------------------------------------- /src/components/layout/Sider.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Layout, Menu } from 'antd'; 3 | import i18next from 'i18next'; 4 | import { SelectParam } from 'antd/lib/menu'; 5 | 6 | interface IProps { 7 | onSelect?: (params: SelectParam) => void; 8 | } 9 | 10 | class Sider extends Component { 11 | render() { 12 | const { onSelect } = this.props; 13 | return ( 14 | 15 | 22 | 23 | 24 | {i18next.t('widget.series.title')} 25 | 26 | 27 | {i18next.t('widget.xaxis.title')} 28 | 29 | 30 | {i18next.t('widget.yaxis.title')} 31 | 32 | 33 | {i18next.t('widget.grid.title')} 34 | 35 | 36 | {i18next.t('widget.tooltip.title')} 37 | 38 | 39 | 40 | 41 | {i18next.t('widget.series.title')} 42 | 43 | 44 | {i18next.t('widget.xaxis.title')} 45 | 46 | 47 | {i18next.t('widget.yaxis.title')} 48 | 49 | 50 | {i18next.t('widget.grid.title')} 51 | 52 | 53 | 54 | 55 | {i18next.t('widget.series.title')} 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | } 63 | 64 | export default Sider; 65 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | 3 | export { default as Sider } from './Sider'; 4 | 5 | export { default as Content } from './Content'; 6 | 7 | export { default as Menus } from './Menus'; 8 | -------------------------------------------------------------------------------- /src/components/panel/ChartPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactEcharts from 'echarts-for-react'; 3 | 4 | import { IChartContext, ChartContext } from '../../containers/ChartContainer'; 5 | 6 | class ChartPanel extends Component { 7 | private chartRef: any; 8 | private echarts: echarts.ECharts; 9 | static contextType = ChartContext; 10 | context: IChartContext; 11 | 12 | componentDidMount() { 13 | this.echarts = this.chartRef.getEchartsInstance(); 14 | } 15 | 16 | componentDidCatch(error: Error) { 17 | console.error(error); 18 | } 19 | 20 | getOption = (): echarts.EChartOption => { 21 | const { structure, style } = this.context; 22 | const { series, xAxis, yAxis, grid, tooltip } = structure; 23 | const tooltipOption = tooltip; 24 | const gridOption = Object.keys(grid).map(key => { 25 | return { 26 | id: key, 27 | ...grid[key], 28 | ...style.grid[key], 29 | }; 30 | }); 31 | const xAxisOption = Object.keys(xAxis).map(key => { 32 | const { grid: xAxisGrid, ...other } = xAxis[key]; 33 | const gridIndex = gridOption.findIndex(value => value.id === xAxisGrid); 34 | return { 35 | id: key, 36 | gridIndex: gridIndex >= 0 ? gridIndex : 0, 37 | ...other, 38 | }; 39 | }); 40 | const yAxisOption = Object.keys(yAxis).map(key => { 41 | const { grid: yAxisGrid, ...other } = yAxis[key]; 42 | const gridIndex = gridOption.findIndex(value => value.id === yAxisGrid); 43 | return { 44 | id: key, 45 | gridIndex: gridIndex >= 0 ? gridIndex : 0, 46 | ...other, 47 | }; 48 | }); 49 | const seriesOption = Object.keys(series).map(key => { 50 | const { type: seriesType, xAxis: seriesXAxis, yAxis: seriesYAxis, ...other } = series[key]; 51 | const isArea = seriesType === 'area'; 52 | const xAxisIndex = xAxisOption.findIndex(value => value.id === seriesXAxis); 53 | const yAxisIndex = yAxisOption.findIndex(value => value.id === seriesYAxis); 54 | return { 55 | id: key, 56 | type: isArea ? 'line' : seriesType, 57 | data: series[key].data, 58 | areaStyle: isArea ? {} : null, 59 | xAxisIndex: xAxisIndex >= 0 ? xAxisIndex : 0, 60 | yAxisIndex: yAxisIndex >= 0 ? yAxisIndex : 0, 61 | ...other, 62 | }; 63 | }); 64 | return { 65 | tooltip: tooltipOption, 66 | xAxis: xAxisOption, 67 | yAxis: yAxisOption, 68 | grid: gridOption, 69 | series: seriesOption, 70 | }; 71 | }; 72 | 73 | render() { 74 | console.log(this.getOption()); 75 | return ( 76 | { 78 | this.chartRef = c; 79 | }} 80 | notMerge={true} 81 | option={this.getOption()} 82 | style={{ height: '100%', width: '100%' }} 83 | /> 84 | ); 85 | } 86 | } 87 | 88 | export default ChartPanel; 89 | -------------------------------------------------------------------------------- /src/components/panel/DataGridPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDataGrid from 'react-data-grid'; 3 | 4 | import { Resizer } from '../resizer'; 5 | 6 | const columns = [ 7 | { key: "id", name: "ID", editable: true }, 8 | { key: "title", name: "Title", editable: true }, 9 | { key: "complete", name: "Complete", editable: true } 10 | ]; 11 | 12 | const rows = [ 13 | { id: 0, title: "Task 1", complete: 20 }, 14 | { id: 1, title: "Task 2", complete: 40 }, 15 | { id: 2, title: "Task 3", complete: 60 } 16 | ]; 17 | 18 | class DataGridPanel extends Component { 19 | render() { 20 | return ( 21 | 22 | {(width, height) => ( 23 | rows[i]} 28 | rowsCount={3} 29 | /> 30 | )} 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default DataGridPanel; 37 | -------------------------------------------------------------------------------- /src/components/panel/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | const Panel = React.forwardRef>((props, ref) => { 5 | const { children, className, ...other } = props; 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | }); 12 | 13 | export default Panel; 14 | -------------------------------------------------------------------------------- /src/components/panel/StructurePanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { XAxisPanel, YAxisPanel, SeriesPanel, GridPanel, TooltipPanel } from './structure'; 4 | 5 | const panels: { [key: string]: any } = { 6 | series: SeriesPanel, 7 | xAxis: XAxisPanel, 8 | yAxis: YAxisPanel, 9 | grid: GridPanel, 10 | tooltip: TooltipPanel, 11 | } 12 | 13 | interface IProps { 14 | panelKey: string; 15 | } 16 | 17 | class StructurePanel extends Component { 18 | render() { 19 | const { panelKey } = this.props; 20 | const PanelComponent = panels[panelKey]; 21 | return ; 22 | } 23 | } 24 | 25 | export default StructurePanel; 26 | -------------------------------------------------------------------------------- /src/components/panel/StylePanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { SeriesPanel, GridPanel, XAxisPanel, YAxisPanel } from './style'; 4 | 5 | const panels: Record = { 6 | series: SeriesPanel, 7 | grid: GridPanel, 8 | xAxis: XAxisPanel, 9 | yAxis: YAxisPanel, 10 | } 11 | 12 | interface IProps { 13 | panelKey: string; 14 | } 15 | 16 | class StylePanel extends Component { 17 | render() { 18 | const { panelKey } = this.props; 19 | const PanelComponent = panels[panelKey]; 20 | return ; 21 | } 22 | } 23 | 24 | export default StylePanel; 25 | -------------------------------------------------------------------------------- /src/components/panel/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Panel } from './Panel'; 2 | 3 | export { default as ChartPanel } from './ChartPanel'; 4 | 5 | export { default as DataGridPanel } from './DataGridPanel'; 6 | 7 | export { default as StructurePanel } from './StructurePanel'; 8 | 9 | export { default as StylePanel } from './StylePanel'; 10 | -------------------------------------------------------------------------------- /src/components/panel/structure/GridPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | import uuid from 'uuid'; 5 | 6 | import { DynamicForm } from '../../form'; 7 | import { IStructureContext, StructureContext } from '../../../containers/StructureContainer'; 8 | 9 | interface IState { 10 | collapsed: boolean; 11 | activeKey: string[]; 12 | grid: { [key: string]: any }; 13 | } 14 | 15 | class GridPanel extends Component<{}, IState> { 16 | static contextType = StructureContext; 17 | context: IStructureContext; 18 | 19 | constructor(props: {}, context: IStructureContext) { 20 | super(props, context); 21 | this.state = { 22 | grid: context.grid, 23 | activeKey: context.gridActiveKey, 24 | collapsed: false, 25 | } 26 | } 27 | 28 | handleChangeGrid = (grid: { [key: string]: any }) => { 29 | this.setState({ 30 | grid: Object.assign({}, grid), 31 | }, () => { 32 | this.context.onChangeGrid(this.state.grid); 33 | }); 34 | } 35 | 36 | handleChangeActiveKey = (activeKey: string[]) => { 37 | this.setState({ 38 | activeKey, 39 | }); 40 | this.context.onChangeGridActiveKey(activeKey); 41 | } 42 | 43 | handleAddGrid = () => { 44 | const id = uuid(); 45 | this.setState({ 46 | grid: Object.assign({}, this.state.grid, { 47 | [id]: { 48 | show: false, 49 | }, 50 | }), 51 | activeKey: this.state.activeKey.concat(id), 52 | }, () => { 53 | this.context.onChangeGrid(this.state.grid); 54 | }); 55 | } 56 | 57 | handleCollapse = () => { 58 | const collapsed = !this.state.collapsed; 59 | const activeKey = collapsed ? [] : Object.keys(this.state.grid); 60 | this.setState({ 61 | collapsed, 62 | activeKey, 63 | }); 64 | this.context.onChangeGridActiveKey(activeKey); 65 | } 66 | 67 | render() { 68 | const { grid, gridActiveKey } = this.context; 69 | const { collapsed } = this.state; 70 | return ( 71 |
72 |
73 | 74 | 75 |
76 |
77 | 104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | export default GridPanel; 111 | -------------------------------------------------------------------------------- /src/components/panel/structure/SeriesPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | import uuid from 'uuid'; 5 | 6 | import { DynamicForm } from '../../form'; 7 | import { StructureContext, IStructureContext } from '../../../containers/StructureContainer'; 8 | 9 | interface IState { 10 | series: { [key: string]: any }; 11 | activeKey: string[]; 12 | collapsed: boolean; 13 | } 14 | 15 | class SeriesPanel extends Component<{}, IState> { 16 | static contextType = StructureContext; 17 | context: IStructureContext; 18 | 19 | constructor(props: {}, context: IStructureContext) { 20 | super(props, context); 21 | this.state = { 22 | series: context.series, 23 | activeKey: context.seriesActiveKey, 24 | collapsed: false, 25 | }; 26 | } 27 | 28 | handleChangeSeries = (series: { [key: string]: any }) => { 29 | this.setState( 30 | { 31 | series: Object.assign({}, series), 32 | }, 33 | () => { 34 | this.context.onChangeSeries(this.state.series); 35 | }, 36 | ); 37 | }; 38 | 39 | handleChangeActiveKey = (activeKey: string[]) => { 40 | this.setState({ 41 | activeKey, 42 | }); 43 | this.context.onChangeSeriesActiveKey(activeKey); 44 | }; 45 | 46 | handleAddSeries = () => { 47 | const id = uuid(); 48 | this.setState( 49 | { 50 | series: Object.assign({}, this.state.series, { 51 | [id]: { 52 | type: 'line', 53 | data: Array.from({ length: 12 }, () => Math.random() * 1000 + 100), 54 | }, 55 | }), 56 | activeKey: this.state.activeKey.concat(id), 57 | }, 58 | () => { 59 | this.context.onChangeSeries(this.state.series); 60 | }, 61 | ); 62 | }; 63 | 64 | handleCollapse = () => { 65 | const collapsed = !this.state.collapsed; 66 | const activeKey = collapsed ? [] : Object.keys(this.state.series); 67 | this.setState({ 68 | collapsed, 69 | activeKey, 70 | }); 71 | this.context.onChangeSeriesActiveKey(activeKey); 72 | }; 73 | 74 | render() { 75 | const { series, seriesActiveKey, xAxis, yAxis } = this.context; 76 | const { collapsed } = this.state; 77 | return ( 78 |
79 |
80 | 83 | 86 |
87 |
88 | { 141 | const { name } = xAxis[key]; 142 | return { 143 | label: name && name.length ? name : `x${index}`, 144 | value: key, 145 | }; 146 | }), 147 | }, 148 | yAxis: { 149 | label: i18next.t('widget.yaxis.title'), 150 | type: 'select', 151 | style: { width: '100%' }, 152 | span: 12, 153 | items: Object.keys(yAxis).map((key, index) => { 154 | const { name } = yAxis[key]; 155 | return { 156 | label: name && name.length ? name : `y${index}`, 157 | value: key, 158 | }; 159 | }), 160 | }, 161 | }} 162 | onChange={this.handleChangeSeries} 163 | onChangeActiveKey={this.handleChangeActiveKey} 164 | /> 165 |
166 |
167 | ); 168 | } 169 | } 170 | 171 | export default SeriesPanel; 172 | -------------------------------------------------------------------------------- /src/components/panel/structure/TooltipPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import i18next from 'i18next'; 3 | import { Button } from 'antd'; 4 | import debounce from 'lodash/debounce'; 5 | 6 | import { Form } from '../../form'; 7 | import { StructureContext, IStructureContext } from '../../../containers/StructureContainer'; 8 | 9 | interface IState { 10 | tooltip: Record; 11 | } 12 | 13 | class TooltipPanel extends Component<{}, IState> { 14 | static contextType = StructureContext; 15 | context: IStructureContext; 16 | 17 | constructor(props: {}, context: IStructureContext) { 18 | super(props, context); 19 | this.state = { 20 | tooltip: context.tooltip, 21 | } 22 | } 23 | 24 | handleValuesChange = (changedValues: any, allValues: any) => { 25 | console.log(changedValues, allValues); 26 | this.context.onChangeTooltip(allValues); 27 | } 28 | 29 | render() { 30 | const { tooltip } = this.context; 31 | return ( 32 |
33 |
34 | 35 |
36 |
37 | 173 |
174 |
175 | ); 176 | } 177 | } 178 | 179 | export default TooltipPanel; 180 | -------------------------------------------------------------------------------- /src/components/panel/structure/XAxisPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | import uuid from 'uuid'; 5 | 6 | import { DynamicForm } from '../../form'; 7 | import { IStructureContext, StructureContext } from '../../../containers/StructureContainer'; 8 | 9 | interface IState { 10 | xAxis: { [key: string]: any }; 11 | activeKey: string[]; 12 | collapsed: boolean; 13 | } 14 | 15 | class XAxisPanel extends Component<{}, IState> { 16 | static contextType = StructureContext; 17 | context: IStructureContext; 18 | 19 | constructor(props: {}, context: IStructureContext) { 20 | super(props, context); 21 | this.state = { 22 | xAxis: context.xAxis, 23 | activeKey: context.xAxisActiveKey, 24 | collapsed: false, 25 | } 26 | } 27 | 28 | handleChangeXAxis = (xAxis: { [key: string]: any }) => { 29 | this.setState({ 30 | xAxis: Object.assign({}, xAxis), 31 | }, () => { 32 | this.context.onChangeXAxis(this.state.xAxis); 33 | }); 34 | } 35 | 36 | handleChangeActiveKey = (activeKey: string[]) => { 37 | this.setState({ 38 | activeKey, 39 | }); 40 | this.context.onChangeXAxisActiveKey(activeKey); 41 | } 42 | 43 | handleAddXAxis = () => { 44 | const id = uuid(); 45 | this.setState({ 46 | xAxis: Object.assign({}, this.state.xAxis, { 47 | [id]: { 48 | type: 'category', 49 | show: true, 50 | }, 51 | }), 52 | activeKey: this.state.activeKey.concat(id), 53 | }, () => { 54 | this.context.onChangeXAxis(this.state.xAxis); 55 | }); 56 | } 57 | 58 | handleCollapse = () => { 59 | const collapsed = !this.state.collapsed; 60 | const activeKey = collapsed ? [] : Object.keys(this.state.xAxis); 61 | this.setState({ 62 | collapsed, 63 | activeKey, 64 | }); 65 | this.context.onChangeXAxisActiveKey(activeKey); 66 | } 67 | 68 | render() { 69 | const { xAxis, xAxisActiveKey, grid } = this.context; 70 | const { collapsed } = this.state; 71 | return ( 72 |
73 |
74 | 75 | 76 |
77 |
78 | { 121 | const { name } = grid[key]; 122 | return { 123 | label: name && name.length ? name : `grid${index}`, 124 | value: key, 125 | }; 126 | }), 127 | }, 128 | show: { 129 | label: i18next.t('common.visible'), 130 | type: 'boolean', 131 | initialValue: true, 132 | span: 12, 133 | }, 134 | inverse: { 135 | label: i18next.t('common.inverse'), 136 | type: 'boolean', 137 | initialValue: false, 138 | span: 12, 139 | }, 140 | scale: { 141 | label: i18next.t('widget.scale'), 142 | type: 'boolean', 143 | initialValue: false, 144 | span: 12, 145 | }, 146 | silent: { 147 | label: i18next.t('widget.silent'), 148 | type: 'boolean', 149 | initialValue: false, 150 | span: 12, 151 | }, 152 | name: { 153 | label: i18next.t('common.name'), 154 | }, 155 | interval: { 156 | label: i18next.t('widget.interval'), 157 | type: 'number', 158 | span: 8, 159 | min: 0, 160 | }, 161 | minInterval: { 162 | label: i18next.t('widget.min-interval'), 163 | type: 'number', 164 | span: 8, 165 | initialValue: 0, 166 | min: 0, 167 | }, 168 | maxInterval: { 169 | label: i18next.t('widget.max-interval'), 170 | type: 'number', 171 | span: 8, 172 | min: 0, 173 | }, 174 | splitNumber: { 175 | label: i18next.t('widget.split-number'), 176 | type: 'number', 177 | initialValue: 5, 178 | span: 8, 179 | min: 0, 180 | }, 181 | min: { 182 | label: i18next.t('common.min'), 183 | type: 'number', 184 | initialValue: null, 185 | span: 8, 186 | }, 187 | max: { 188 | label: i18next.t('common.max'), 189 | type: 'number', 190 | initialValue: null, 191 | span: 8, 192 | }, 193 | boundaryGap: { 194 | label: i18next.t('widget.boundary-gap'), 195 | type: 'boolean', 196 | initialValue: true, 197 | span: 12, 198 | }, 199 | zLevel: { 200 | label: i18next.t('widget.z-level'), 201 | type: 'number', 202 | initialValue: 0, 203 | min: 0, 204 | max: 1000, 205 | span: 12, 206 | }, 207 | }} 208 | onChange={this.handleChangeXAxis} 209 | onChangeActiveKey={this.handleChangeActiveKey} 210 | /> 211 |
212 |
213 | ); 214 | } 215 | } 216 | 217 | export default XAxisPanel; 218 | -------------------------------------------------------------------------------- /src/components/panel/structure/YAxisPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | import uuid from 'uuid'; 5 | 6 | import { DynamicForm } from '../../form'; 7 | import { StructureContext, IStructureContext } from '../../../containers/StructureContainer'; 8 | 9 | interface IState { 10 | yAxis: { [key: string]: any }; 11 | activeKey: string[]; 12 | collapsed: boolean; 13 | } 14 | 15 | class YAxisPanel extends Component<{}, IState> { 16 | static contextType = StructureContext; 17 | context: IStructureContext; 18 | 19 | constructor(props: {}, context: IStructureContext) { 20 | super(props, context); 21 | this.state = { 22 | yAxis: context.yAxis, 23 | activeKey: context.yAxisActiveKey, 24 | collapsed: false, 25 | } 26 | } 27 | 28 | handleChangeYAxis = (yAxis: { [key: string]: any }) => { 29 | this.setState({ 30 | yAxis: Object.assign({}, yAxis), 31 | }, () => { 32 | this.context.onChangeYAxis(this.state.yAxis); 33 | }); 34 | } 35 | 36 | handleChangeActiveKey = (activeKey: string[]) => { 37 | this.setState({ 38 | activeKey, 39 | }); 40 | this.context.onChangeYAxisActiveKey(activeKey); 41 | } 42 | 43 | handleAddYAxis = () => { 44 | const id = uuid(); 45 | this.setState({ 46 | yAxis: Object.assign({}, this.state.yAxis, { 47 | [id]: { 48 | type: 'value', 49 | show: true, 50 | }, 51 | }), 52 | activeKey: this.state.activeKey.concat(id), 53 | }, () => { 54 | this.context.onChangeYAxis(this.state.yAxis); 55 | }); 56 | } 57 | 58 | handleCollapse = () => { 59 | const collapsed = !this.state.collapsed; 60 | const activeKey = collapsed ? [] : Object.keys(this.state.yAxis); 61 | this.setState({ 62 | collapsed, 63 | activeKey, 64 | }); 65 | this.context.onChangeYAxisActiveKey(activeKey); 66 | } 67 | 68 | render() { 69 | const { yAxis, yAxisActiveKey, grid } = this.context; 70 | const { collapsed } = this.state; 71 | return ( 72 |
73 |
74 | 75 | 76 |
77 |
78 | { 121 | const { name } = grid[key]; 122 | return { 123 | label: name && name.length ? name : `grid${index}`, 124 | value: key, 125 | }; 126 | }), 127 | }, 128 | show: { 129 | label: i18next.t('common.visible'), 130 | type: 'boolean', 131 | initialValue: true, 132 | span: 12, 133 | }, 134 | inverse: { 135 | label: i18next.t('common.inverse'), 136 | type: 'boolean', 137 | initialValue: false, 138 | span: 12, 139 | }, 140 | scale: { 141 | label: i18next.t('widget.scale'), 142 | type: 'boolean', 143 | initialValue: false, 144 | span: 12, 145 | }, 146 | silent: { 147 | label: i18next.t('widget.silent'), 148 | type: 'boolean', 149 | initialValue: false, 150 | span: 12, 151 | }, 152 | name: { 153 | label: i18next.t('common.name'), 154 | }, 155 | interval: { 156 | label: i18next.t('widget.interval'), 157 | type: 'number', 158 | span: 8, 159 | min: 0, 160 | }, 161 | minInterval: { 162 | label: i18next.t('widget.min-interval'), 163 | type: 'number', 164 | span: 8, 165 | initialValue: 0, 166 | min: 0, 167 | }, 168 | maxInterval: { 169 | label: i18next.t('widget.max-interval'), 170 | type: 'number', 171 | span: 8, 172 | min: 0, 173 | }, 174 | splitNumber: { 175 | label: i18next.t('widget.split-number'), 176 | type: 'number', 177 | initialValue: 5, 178 | span: 8, 179 | min: 0, 180 | }, 181 | min: { 182 | label: i18next.t('common.min'), 183 | type: 'number', 184 | initialValue: null, 185 | span: 8, 186 | }, 187 | max: { 188 | label: i18next.t('common.max'), 189 | type: 'number', 190 | initialValue: null, 191 | span: 8, 192 | }, 193 | boundaryGap: { 194 | label: i18next.t('widget.boundary-gap'), 195 | type: 'boolean', 196 | initialValue: true, 197 | span: 12, 198 | }, 199 | zLevel: { 200 | label: i18next.t('widget.z-level'), 201 | type: 'number', 202 | initialValue: 0, 203 | min: 0, 204 | max: 1000, 205 | span: 12, 206 | }, 207 | }} 208 | onChange={this.handleChangeYAxis} 209 | onChangeActiveKey={this.handleChangeActiveKey} 210 | /> 211 |
212 |
213 | ); 214 | } 215 | } 216 | 217 | export default YAxisPanel; 218 | -------------------------------------------------------------------------------- /src/components/panel/structure/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SeriesPanel } from './SeriesPanel'; 2 | 3 | export { default as XAxisPanel } from './XAxisPanel'; 4 | 5 | export { default as YAxisPanel } from './YAxisPanel'; 6 | 7 | export { default as GridPanel } from './GridPanel'; 8 | 9 | export { default as TooltipPanel } from './TooltipPanel'; 10 | -------------------------------------------------------------------------------- /src/components/panel/style/GridPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import i18next from 'i18next'; 4 | import { IStyleContext, StyleContext } from '../../../containers/StyleContainer'; 5 | import { DynamicForm } from '../../form'; 6 | 7 | interface IState { 8 | collapsed: boolean 9 | grid: Record; 10 | activeKey: string[]; 11 | } 12 | 13 | class GridPanel extends Component<{}, IState> { 14 | static contextType = StyleContext; 15 | context: IStyleContext; 16 | 17 | constructor(props: {}, context: IStyleContext) { 18 | super(props, context); 19 | this.state = { 20 | grid: context.grid, 21 | activeKey: context.gridActiveKey, 22 | collapsed: false, 23 | } 24 | } 25 | 26 | handleCollapse = () => { 27 | const collapsed = !this.state.collapsed; 28 | const activeKey = collapsed ? [] : Object.keys(this.state.grid); 29 | this.setState({ 30 | collapsed, 31 | activeKey, 32 | }); 33 | this.context.onChangeGridActiveKey(activeKey); 34 | } 35 | 36 | handleChangeGrid = (grid: { [key: string]: any }) => { 37 | this.setState({ 38 | grid: Object.assign({}, grid), 39 | }, () => { 40 | this.context.onChangeGrid(this.state.grid); 41 | }); 42 | } 43 | 44 | handleChangeActiveKey = (activeKey: string[]) => { 45 | this.setState({ 46 | activeKey, 47 | }); 48 | this.context.onChangeGridActiveKey(activeKey); 49 | } 50 | 51 | render() { 52 | const { grid, gridActiveKey } = this.context; 53 | const { collapsed } = this.state; 54 | return ( 55 |
56 |
57 | 63 |
64 |
65 | 149 |
150 |
151 | ); 152 | } 153 | } 154 | 155 | export default GridPanel; 156 | -------------------------------------------------------------------------------- /src/components/panel/style/SeriesPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class SeriesPanel extends Component { 4 | render() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default SeriesPanel; 14 | -------------------------------------------------------------------------------- /src/components/panel/style/XAxisPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class XAxisPanel extends Component { 4 | render() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default XAxisPanel; 14 | -------------------------------------------------------------------------------- /src/components/panel/style/YAxisPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class YAxisPanel extends Component { 4 | render() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default YAxisPanel; 14 | -------------------------------------------------------------------------------- /src/components/panel/style/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SeriesPanel } from './SeriesPanel'; 2 | 3 | export { default as GridPanel } from './GridPanel'; 4 | 5 | export { default as XAxisPanel } from './XAxisPanel'; 6 | 7 | export { default as YAxisPanel } from './YAxisPanel'; 8 | -------------------------------------------------------------------------------- /src/components/picker/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Dropdown, Button, Menu } from 'antd'; 3 | import { SketchPicker, ColorResult } from 'react-color'; 4 | import debounce from 'lodash/debounce'; 5 | 6 | export interface ColorPickerProps { 7 | value?: string; 8 | onChange?: (color: any) => void; 9 | } 10 | 11 | const ColorPicker: React.SFC = props => { 12 | const { value, onChange } = props; 13 | const [color, setColor] = useState('#fff'); 14 | useEffect(() => { 15 | setColor(value); 16 | }, [value]); 17 | const handleChange = (color: ColorResult) => { 18 | const { r, g, b, a } = color.rgb; 19 | const colorValue = `rgba(${r}, ${g}, ${b}, ${a})`; 20 | setColor(colorValue); 21 | if (onChange) { 22 | onChange(colorValue); 23 | } 24 | } 25 | const renderOverlay = () => { 26 | return ( 27 | 28 | 32 | 33 | ); 34 | } 35 | return ( 36 | 37 |
107 | 108 | 109 | ); 110 | }; 111 | 112 | export default VirtualizedTable; 113 | -------------------------------------------------------------------------------- /src/components/virtualized/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as VirtualizedTable } from './VirtualizedTable'; 2 | -------------------------------------------------------------------------------- /src/containers/ChartContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { IStyleContext, StyleContext } from './StyleContainer'; 3 | import { IStructureContext, StructureContext } from './StructureContainer'; 4 | 5 | export interface IChartContext { 6 | structure: IStructureContext; 7 | style: IStyleContext; 8 | } 9 | 10 | export const ChartContext = React.createContext(null); 11 | 12 | const ChartContainer: React.SFC = props => { 13 | const { children } = props; 14 | const structure = useContext(StructureContext); 15 | const style = useContext(StyleContext); 16 | return ( 17 | 23 | {children} 24 | 25 | ); 26 | } 27 | 28 | export default ChartContainer; 29 | -------------------------------------------------------------------------------- /src/containers/StructureContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import uuid from 'uuid'; 3 | import StyleContainer from './StyleContainer'; 4 | 5 | export interface IStructureContext { 6 | series: any; 7 | seriesActiveKey: string[]; 8 | onChangeSeries: (series: any) => void; 9 | onChangeSeriesActiveKey: (activeKey: string[]) => void; 10 | xAxis: any; 11 | xAxisActiveKey: string[]; 12 | onChangeXAxis: (xAxis: any) => void; 13 | onChangeXAxisActiveKey: (activeKey: string[]) => void; 14 | yAxis: any; 15 | yAxisActiveKey: string[]; 16 | onChangeYAxis: (yAxis: any) => void; 17 | onChangeYAxisActiveKey: (activeKey: string[]) => void; 18 | grid: any; 19 | gridActiveKey: string[]; 20 | onChangeGrid: (grid: any) => void; 21 | onChangeGridActiveKey: (activeKey: string[]) => void; 22 | tooltip: any; 23 | onChangeTooltip: (tooltip: any) => void; 24 | } 25 | 26 | export const StructureContext = React.createContext(null); 27 | 28 | const StructureContainer: React.SFC = props => { 29 | const { children } = props; 30 | const [series, setSeries] = useState({ 31 | [uuid()]: { 32 | type: 'line', 33 | data: Array.from({ length: 12 }, () => Math.random() * 1000 + 100), 34 | }, 35 | }); 36 | const [xAxis, setXAxis] = useState({ 37 | [uuid()]: { 38 | type: 'category', 39 | show: true, 40 | }, 41 | }); 42 | const [yAxis, setYAxis] = useState({ 43 | [uuid()]: { 44 | type: 'value', 45 | show: true, 46 | }, 47 | }); 48 | const [grid, setGrid] = useState({ 49 | [uuid()]: { 50 | show: false, 51 | }, 52 | }); 53 | const [tooltip, setTooltip] = useState({ 54 | show: true, 55 | showContent: true, 56 | alwaysShowContent: true, 57 | trigger: 'item', 58 | triggerOn: 'mousemove|click', 59 | showDelay: 0, 60 | hideDelay: 100, 61 | transitionDuration: 0.4, 62 | enterable: true, 63 | confine: false, 64 | renderMode: 'html', 65 | axisPointer: { 66 | show: true, 67 | }, 68 | }); 69 | const [xAxisActiveKey, setXAxisActiveKey] = useState([]); 70 | const [seriesActiveKey, setSeriesActiveKey] = useState([]); 71 | const [yAxisActiveKey, setYAxisActiveKey] = useState([]); 72 | const [gridActiveKey, setGridActiveKey] = useState([]); 73 | const handleChangeSeries = (series: any) => { 74 | setSeries(series); 75 | }; 76 | const handleChangeXAxis = (xAxis: any) => { 77 | setXAxis(xAxis); 78 | }; 79 | const handleChangeYAxis = (yAxis: any) => { 80 | setYAxis(yAxis); 81 | }; 82 | const handleChangeGrid = (grid: any) => { 83 | setGrid(grid); 84 | }; 85 | const handleChangeTooltip = (tooltip: any) => { 86 | setTooltip(tooltip); 87 | }; 88 | const handleChangeSeriesActiveKey = (activeKey: string[]) => { 89 | setSeriesActiveKey(activeKey); 90 | }; 91 | const handleChangeXAxisActiveKey = (activeKey: string[]) => { 92 | setXAxisActiveKey(activeKey); 93 | }; 94 | const handleChangeYAxisActiveKey = (activeKey: string[]) => { 95 | setYAxisActiveKey(activeKey); 96 | }; 97 | const handleChangeGridActiveKey = (activeKey: string[]) => { 98 | setGridActiveKey(activeKey); 99 | }; 100 | return ( 101 | 123 | {children} 124 | 125 | ); 126 | }; 127 | 128 | export default StructureContainer; 129 | -------------------------------------------------------------------------------- /src/containers/StyleContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | import { StructureContext } from './StructureContainer'; 3 | 4 | export interface IStyleContext { 5 | series: any; 6 | seriesActiveKey: string[]; 7 | onChangeSeries: (series: any) => void; 8 | onChangeSeriesActiveKey: (activeKey: string[]) => void; 9 | grid: any; 10 | gridActiveKey: string[]; 11 | onChangeGrid: (grid: any) => void; 12 | onChangeGridActiveKey: (activeKey: string[]) => void; 13 | xAxis: any; 14 | xAxisActiveKey: string[]; 15 | onChangeXAxis: (xAxis: any) => void; 16 | onChangeXAxisActiveKey: (activeKey: string[]) => void; 17 | yAxis: any; 18 | yAxisActiveKey: string[]; 19 | onChangeYAxis: (yAxis: any) => void; 20 | onChangeYAxisActiveKey: (activeKey: string[]) => void; 21 | } 22 | 23 | export const StyleContext = React.createContext(null); 24 | 25 | const StyleContainer: React.SFC = props => { 26 | const { children } = props; 27 | const structrue = useContext(StructureContext); 28 | const [series, setSeries] = useState(Object.keys(structrue.series).reduce((prev, curr) => { 29 | return Object.assign(prev, { [curr]: {} }); 30 | }, {})); 31 | const [xAxis, setXAxis] = useState(Object.keys(structrue.xAxis).reduce((prev, curr) => { 32 | return Object.assign(prev, { [curr]: {} }); 33 | }, {})); 34 | const [yAxis, setYAxis] = useState(Object.keys(structrue.yAxis).reduce((prev, curr) => { 35 | return Object.assign(prev, { [curr]: {} }); 36 | }, {})); 37 | const [grid, setGrid] = useState(Object.keys(structrue.grid).reduce((prev, curr) => { 38 | return Object.assign(prev, { [curr]: {} }); 39 | }, {} as Record)); 40 | const [xAxisActiveKey, setXAxisActiveKey] = useState([]); 41 | const [seriesActiveKey, setSeriesActiveKey] = useState([]); 42 | const [yAxisActiveKey, setYAxisActiveKey] = useState([]); 43 | const [gridActiveKey, setGridActiveKey] = useState([]); 44 | const handleChangeSeries = (series: any) => { 45 | setSeries(series); 46 | } 47 | const handleChangeXAxis = (xAxis: any) => { 48 | setXAxis(xAxis); 49 | } 50 | const handleChangeYAxis = (yAxis: any) => { 51 | setYAxis(yAxis); 52 | } 53 | const handleChangeGrid = (grid: any) => { 54 | setGrid(grid); 55 | } 56 | const handleChangeSeriesActiveKey = (activeKey: string[]) => { 57 | setSeriesActiveKey(activeKey); 58 | } 59 | const handleChangeXAxisActiveKey = (activeKey: string[]) => { 60 | setXAxisActiveKey(activeKey); 61 | } 62 | const handleChangeYAxisActiveKey = (activeKey: string[]) => { 63 | setYAxisActiveKey(activeKey); 64 | } 65 | const handleChangeGridActiveKey = (activeKey: string[]) => { 66 | setGridActiveKey(activeKey); 67 | } 68 | useEffect(() => { 69 | // console.log('structureSeries updated'); 70 | }, [structrue.series]); 71 | useEffect(() => { 72 | // console.log('structureXAxis updated'); 73 | }, [structrue.xAxis]); 74 | useEffect(() => { 75 | // console.log('structureYAxis updated'); 76 | }, [structrue.yAxis]); 77 | useEffect(() => { 78 | // console.log('structureGrid updated'); 79 | setGrid(Object.keys(structrue.grid).reduce((prev, curr) => { 80 | return Object.assign(prev, { [curr]: grid[curr] }); 81 | }, {})); 82 | }, [structrue.grid]); 83 | return ( 84 | 104 | {children} 105 | 106 | ); 107 | } 108 | 109 | export default StyleContainer; 110 | -------------------------------------------------------------------------------- /src/examples/Table.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { VirtualizedTable } from '../components/virtualized'; 3 | import { Button } from 'antd'; 4 | 5 | const Table = () => { 6 | const [data, setDatas] = useState([]); 7 | const columns = [ 8 | { title: 'A', dataIndex: 'key' }, 9 | { title: 'B', dataIndex: 'key' }, 10 | { title: 'C', dataIndex: 'key' }, 11 | { title: 'D', dataIndex: 'key', width: 200 }, 12 | { title: 'E', dataIndex: 'key', width: 200 }, 13 | { title: 'F', dataIndex: 'key', width: 200 }, 14 | ]; 15 | for (let i = 0; i < 100; i += 1) { 16 | data.push({ 17 | key: i, 18 | }); 19 | } 20 | // setDatas(datas); 21 | useEffect(() => { 22 | setTimeout(() => { 23 | for (let i = 0; i < 100; i += 1) { 24 | data.push({ 25 | key: i, 26 | }); 27 | } 28 | setDatas(data.concat(data)); 29 | }, 5000); 30 | }, []); 31 | return ( 32 |
33 |
34 |
test
35 |
36 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Table; 44 | -------------------------------------------------------------------------------- /src/examples/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Table } from './Table'; 2 | -------------------------------------------------------------------------------- /src/i18n/i18nClient.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | 4 | import { 5 | localeKO, 6 | localeEN, 7 | } from '../locales'; 8 | 9 | /** 10 | * Client Side Load 11 | */ 12 | const i18nClient = i18n 13 | .use(LanguageDetector) 14 | .init({ 15 | load: 'languageOnly', 16 | whitelist: ['en', 'en-US', 'ko', 'ko-KR'], 17 | nonExplicitWhitelist: false, 18 | fallbackLng: 'en-US', 19 | interpolation: { 20 | escapeValue: false, // not needed for react!! 21 | }, 22 | react: { 23 | wait: true, // set to true if you like to wait for loaded in every translated hoc 24 | nsMode: 'default', // set it to fallback to let passed namespaces to translated hoc act as fallbacks 25 | }, 26 | defaultNS: 'locale.constant', 27 | resources: { 28 | 'en': { 29 | 'locale.constant': localeEN, 30 | }, 31 | 'en-US': { 32 | 'locale.constant': localeEN, 33 | }, 34 | 'ko': { 35 | 'locale.constant': localeKO, 36 | }, 37 | 'ko-KR': { 38 | 'locale.constant': localeKO, 39 | }, 40 | }, 41 | }); 42 | 43 | export default () => i18nClient; 44 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export { default as i18nClient } from './i18nClient'; 2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | 5 | import { register } from './serviceWorker'; 6 | import App from './App'; 7 | 8 | const rootEl = document.createElement('div'); 9 | rootEl.id = 'root'; 10 | document.body.appendChild(rootEl); 11 | 12 | const render = (Component: any) => { 13 | const rootElement = document.getElementById('root'); 14 | ReactDOM.render( 15 | 16 | 17 | , 18 | rootElement, 19 | ); 20 | }; 21 | 22 | render(App); 23 | 24 | register(); 25 | 26 | if ((module as NodeModule).hot) { 27 | (module as NodeModule).hot.accept('./App', () => { 28 | render(App); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | export { default as localeEN } from './locale.constant.json'; 2 | 3 | export { default as localeKO } from './locale.constant-ko.json'; 4 | -------------------------------------------------------------------------------- /src/locales/locale.constant-ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "login": "로그인", 4 | "save": "저장", 5 | "close": "닫기", 6 | "cancel": "취소", 7 | "enabled": "사용 여부", 8 | "add": "추가", 9 | "more": "더 보기", 10 | "ok": "확인", 11 | "collapse": "축소", 12 | "expand": "확장", 13 | "clear": "초기화" 14 | }, 15 | "common": { 16 | "id": "아이디", 17 | "home": "홈", 18 | "title": "제목", 19 | "description": "설명", 20 | "general": "일반", 21 | "value": "값", 22 | "gauge": "게이지", 23 | "name": "이름", 24 | "type": "종류", 25 | "data": "데이터", 26 | "user-id": "사용자 아이디", 27 | "password": "비밀번호", 28 | "info": "정보", 29 | "warning": "경고", 30 | "critical": "심각", 31 | "location": "위치", 32 | "basic-info": "기본 정보", 33 | "category": "카테고리", 34 | "time": "시간", 35 | "log": "로그", 36 | "visible": "보기", 37 | "inverse": "역", 38 | "scale": "규모", 39 | "min": "최소", 40 | "max": "최대", 41 | "structure": "구조", 42 | "style": "스타일", 43 | "item": "아이템", 44 | "axis": "축", 45 | "none": "없음" 46 | }, 47 | "confirm": { 48 | "delete": "'{{arg}}'을(를) 삭제하시겠습니까?" 49 | }, 50 | "chart": { 51 | "line": "라인 차트", 52 | "bar": "바 차트", 53 | "area": "영역 차트", 54 | "scatter": "스캐터 차트", 55 | "pie": "파이 차트" 56 | }, 57 | "widget": { 58 | "modify": "위젯 수정", 59 | "delete": "위젯 삭제", 60 | "clone": "위젯 복제", 61 | "silent": "이벤트 취소", 62 | "scale": "차트 확대", 63 | "z-level": "우선 순위", 64 | "boundary-gap": "차트 공백", 65 | "interval": "간격", 66 | "min-interval": "최소 간격", 67 | "max-interval": "최대 간격", 68 | "split-number": "분할 개수", 69 | "log-base": "로그 베이스", 70 | "contain-label": "라벨 표시 여부", 71 | "card": { 72 | "title": "카드 위젯", 73 | "description": "카드 형태의 위젯을 추가합니다", 74 | "type": "카드 형태" 75 | }, 76 | "memo": { 77 | "title": "메모 위젯", 78 | "description": "HTML, 텍스트 등 다양한 내용을 메모할 수 있습니다", 79 | "html": "HTML 여부" 80 | }, 81 | "line-chart": { 82 | "title": "라인 차트 위젯", 83 | "description": "라인 형태의 차트 위젯을 추가합니다", 84 | "isArea": "영역 차트 여부" 85 | }, 86 | "pie-chart": { 87 | "title": "파이 차트 위젯", 88 | "description": "파이 형태의 차트 위젯을 추가합니다" 89 | }, 90 | "area-chart": { 91 | "title": "영역 차트 위젯", 92 | "description": "영역 형태의 차트 위젯을 추가합니다" 93 | }, 94 | "series": { 95 | "title": "시리즈", 96 | "description": "시리즈를 추가합니다" 97 | }, 98 | "xaxis": { 99 | "title": "X 축", 100 | "description": "X 축을 설정합니다", 101 | "data": "X 축 데이터" 102 | }, 103 | "yaxis": { 104 | "title": "Y 축", 105 | "description": "Y 축을 설정합니다", 106 | "data": "Y 축 데이터" 107 | }, 108 | "grid": { 109 | "title": "그리드", 110 | "description": "차트의 그리드를 설정합니다" 111 | }, 112 | "animation": { 113 | "title": "애니메이션" 114 | }, 115 | "tooltip": { 116 | "title": "툴팁", 117 | "show-content": "Show content" 118 | }, 119 | "trigger": { 120 | "title": "트리거" 121 | } 122 | }, 123 | "validate": { 124 | "enter-arg": "'{{arg}}'을(를) 입력해 주세요" 125 | }, 126 | "layout": { 127 | "left": "왼쪽", 128 | "right": "오른쪽", 129 | "top": "위", 130 | "bottom": "아래", 131 | "width": "넓이", 132 | "height": "높이", 133 | "background-color": "배경색", 134 | "border-color": "테두리 색", 135 | "border-width": "테두리 넓이", 136 | "shadow-color": "그림자 색", 137 | "shadow-blur": "그림자 번짐", 138 | "shadow-offset-x": "그림자 X 위치", 139 | "shadow-offset-y": "그림자 Y 위치" 140 | }, 141 | "event": { 142 | "mousemove": "Mousemove", 143 | "click": "Click", 144 | "mousemove-click": "Mousemove|Click", 145 | "none": "None" 146 | }, 147 | "dashboard": { 148 | "dashboard": "대시보드" 149 | } 150 | } -------------------------------------------------------------------------------- /src/locales/locale.constant.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "home": "Hoe" 4 | } 5 | } -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | const isLocalhost = Boolean( 2 | window.location.hostname === 'localhost' || 3 | // [::1] is the IPv6 localhost address. 4 | window.location.hostname === '[::1]' || 5 | // 127.0.0.1/8 is considered localhost for IPv4. 6 | window.location.hostname.match( 7 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 8 | ), 9 | ); 10 | 11 | export function register(config?: any) { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | // The URL constructor is available in all browsers that support SW. 14 | const publicUrl = new URL(PUBLIC_URL, window.location.href) 15 | if (publicUrl.origin !== window.location.origin) { 16 | // Our service worker won't work if PUBLIC_URL is on a different origin 17 | // from what our page is served on. This might happen if a CDN is used to 18 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 19 | return 20 | } 21 | 22 | window.addEventListener('load', () => { 23 | const swUrl = `${PUBLIC_URL}sw.js` 24 | if (isLocalhost) { 25 | // This is running on localhost. Let's check if a service worker still exists or not. 26 | checkValidServiceWorker(swUrl, config) 27 | 28 | // Add some additional logging to localhost, pointing developers to the 29 | // service worker/PWA documentation. 30 | navigator.serviceWorker.ready.then(() => { 31 | console.log( 32 | 'This web app is being served cache-first by a service ' + 33 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 34 | ) 35 | }) 36 | } else { 37 | // Is not localhost. Just register service worker 38 | registerValidSW(swUrl, config) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | function registerValidSW(swUrl: string, config: any) { 45 | navigator.serviceWorker 46 | .register(swUrl) 47 | .then(registration => { 48 | registration.onupdatefound = () => { 49 | const installingWorker = registration.installing 50 | if (installingWorker == null) { 51 | return 52 | } 53 | installingWorker.onstatechange = () => { 54 | if (installingWorker.state === 'installed') { 55 | if (navigator.serviceWorker.controller) { 56 | // At this point, the updated precached content has been fetched, 57 | // but the previous service worker will still serve the older 58 | // content until all client tabs are closed. 59 | console.log( 60 | 'New content is available and will be used when all ' + 61 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 62 | ) 63 | 64 | // Execute callback 65 | if (config && config.onUpdate) { 66 | config.onUpdate(registration) 67 | } 68 | } else { 69 | // At this point, everything has been precached. 70 | // It's the perfect time to display a 71 | // "Content is cached for offline use." message. 72 | console.log('Content is cached for offline use.') 73 | 74 | // Execute callback 75 | if (config && config.onSuccess) { 76 | config.onSuccess(registration) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | }) 83 | .catch(error => { 84 | console.error('Error during service worker registration:', error) 85 | }) 86 | } 87 | 88 | function checkValidServiceWorker(swUrl: string, config: any) { 89 | // Check if the service worker can be found. If it can't reload the page. 90 | fetch(swUrl) 91 | .then(response => { 92 | // Ensure service worker exists, and that we really are getting a JS file. 93 | const contentType = response.headers.get('content-type') 94 | if ( 95 | response.status === 404 || 96 | (contentType != null && contentType.indexOf('javascript') === -1) 97 | ) { 98 | // No service worker found. Probably a different app. Reload the page. 99 | navigator.serviceWorker.ready.then(registration => { 100 | registration.unregister().then(() => { 101 | window.location.reload() 102 | }) 103 | }) 104 | } else { 105 | // Service worker found. Proceed as normal. 106 | registerValidSW(swUrl, config) 107 | } 108 | }) 109 | .catch(() => { 110 | console.log( 111 | 'No internet connection found. App is running in offline mode.' 112 | ) 113 | }) 114 | } 115 | 116 | export function unregister() { 117 | if ('serviceWorker' in navigator) { 118 | navigator.serviceWorker.ready.then(registration => { 119 | registration.unregister() 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/styles/antd/form/form.less: -------------------------------------------------------------------------------- 1 | .ant-form { 2 | 3 | } 4 | 5 | .ant-form-item { 6 | margin-bottom: 16px; 7 | } 8 | 9 | .dynamic-form { 10 | display: flex; 11 | flex-direction: column; 12 | flex: 1; 13 | align-items: center; 14 | .ant-collapse { 15 | width: 100%; 16 | } 17 | button { 18 | margin: 16px 0 8px; 19 | } 20 | } -------------------------------------------------------------------------------- /src/styles/antd/form/index.less: -------------------------------------------------------------------------------- 1 | @import 'form'; 2 | -------------------------------------------------------------------------------- /src/styles/antd/index.less: -------------------------------------------------------------------------------- 1 | @import 'form/index'; -------------------------------------------------------------------------------- /src/styles/editor/editor.less: -------------------------------------------------------------------------------- 1 | .editor-container { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .editor-panel { 8 | width: 100%; 9 | height: 100%; 10 | flex: 1; 11 | } 12 | 13 | .editor-property { 14 | display: flex; 15 | flex-direction: column; 16 | height: 100%; 17 | &-header { 18 | flex: 0 0 48px; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | padding: 0 12px; 23 | } 24 | &-content { 25 | flex: 1; 26 | overflow-y: auto; 27 | } 28 | } 29 | 30 | .action-icon { 31 | transition: color .125s; 32 | cursor: pointer; 33 | // &:hover { 34 | // color: @primary-color; 35 | // } 36 | } -------------------------------------------------------------------------------- /src/styles/editor/index.less: -------------------------------------------------------------------------------- 1 | @import 'editor'; -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import 'normalize'; 2 | @import 'editor/index'; 3 | @import 'antd/index'; 4 | 5 | @import 'react-split-pane/index'; 6 | @import 'react-data-grid/index'; 7 | @import 'virtualized/index'; 8 | -------------------------------------------------------------------------------- /src/styles/normalize.less: -------------------------------------------------------------------------------- 1 | body, #root, .container { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | /* Customize website's scrollbar like Mac OS 7 | Not supports in Firefox and IE */ 8 | 9 | /* total width */ 10 | ::-webkit-scrollbar { 11 | width: 6px; 12 | height: 6px; 13 | } 14 | 15 | /* background of the scrollbar except button or resizer */ 16 | ::-webkit-scrollbar-track { 17 | background-color: transparent; 18 | } 19 | ::-webkit-scrollbar-track:hover { 20 | } 21 | 22 | /* scrollbar itself */ 23 | ::-webkit-scrollbar-thumb { 24 | background-color: #babac0; 25 | border-radius: 16px; 26 | border: 1px solid transparent; 27 | } 28 | ::-webkit-scrollbar-thumb:hover { 29 | background-color: #a0a0a5; 30 | border: 1px solid transparent; 31 | } 32 | 33 | /* set button(top and bottom of the scrollbar) */ 34 | ::-webkit-scrollbar-button { 35 | display: none; 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/react-data-grid/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salgum1114/react-analytics/519e4e79ba8dd1c9f4db818f91c214b04ab0e4cf/src/styles/react-data-grid/index.less -------------------------------------------------------------------------------- /src/styles/react-split-pane/index.less: -------------------------------------------------------------------------------- 1 | // .Resizer { 2 | // background: #000; 3 | // opacity: 0.2; 4 | // z-index: 1; 5 | // -moz-box-sizing: border-box; 6 | // -webkit-box-sizing: border-box; 7 | // box-sizing: border-box; 8 | // -moz-background-clip: padding; 9 | // -webkit-background-clip: padding; 10 | // background-clip: padding-box; 11 | // } 12 | 13 | // .Resizer:hover { 14 | // -webkit-transition: all 2s ease; 15 | // transition: all 2s ease; 16 | // } 17 | 18 | // .Resizer.horizontal { 19 | // height: 11px; 20 | // margin: -5px 0; 21 | // border-top: 5px solid rgba(255, 255, 255, 0); 22 | // border-bottom: 5px solid rgba(255, 255, 255, 0); 23 | // cursor: row-resize; 24 | // width: 100%; 25 | // } 26 | 27 | // .Resizer.horizontal:hover { 28 | // border-top: 5px solid rgba(0, 0, 0, 0.5); 29 | // border-bottom: 5px solid rgba(0, 0, 0, 0.5); 30 | // } 31 | 32 | // .Resizer.vertical { 33 | // width: 11px; 34 | // margin: 0 -5px; 35 | // border-left: 5px solid rgba(255, 255, 255, 0); 36 | // border-right: 5px solid rgba(255, 255, 255, 0); 37 | // cursor: col-resize; 38 | // } 39 | 40 | // .Resizer.vertical:hover { 41 | // border-left: 5px solid rgba(0, 0, 0, 0.5); 42 | // border-right: 5px solid rgba(0, 0, 0, 0.5); 43 | // } 44 | // .Resizer.disabled { 45 | // cursor: not-allowed; 46 | // } 47 | 48 | // .Resizer.disabled:hover { 49 | // border-color: transparent; 50 | // } 51 | 52 | .Resizer { 53 | background: #000; 54 | opacity: .1; 55 | z-index: 1; 56 | -moz-box-sizing: border-box; 57 | -webkit-box-sizing: border-box; 58 | box-sizing: border-box; 59 | -moz-background-clip: padding; 60 | -webkit-background-clip: padding; 61 | background-clip: padding-box; 62 | cursor: ew-resize; 63 | } 64 | 65 | .Resizer:hover { 66 | -webkit-transition: all 2s ease; 67 | transition: all 2s ease; 68 | } 69 | 70 | .Resizer.horizontal { 71 | height: 16px; 72 | margin: -5px 0; 73 | border-top: 5px solid rgba(255, 255, 255, 0); 74 | border-bottom: 5px solid rgba(255, 255, 255, 0); 75 | cursor: row-resize; 76 | width: 100%; 77 | background-repeat: no-repeat; 78 | background-position: center; 79 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=); 80 | } 81 | 82 | .Resizer.horizontal:hover { 83 | border-top: 5px solid rgba(0, 0, 0, 0.5); 84 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 85 | } 86 | 87 | .Resizer.vertical { 88 | width: 16px; 89 | margin: 0 -5px; 90 | border-left: 5px solid rgba(255, 255, 255, 0); 91 | border-right: 5px solid rgba(255, 255, 255, 0); 92 | cursor: col-resize; 93 | background-repeat: no-repeat; 94 | background-position: center; 95 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==); 96 | } 97 | 98 | .Resizer.vertical:hover { 99 | border-left: 5px solid rgba(0, 0, 0, 0.5); 100 | border-right: 5px solid rgba(0, 0, 0, 0.5); 101 | } 102 | 103 | .Resizer.disabled { 104 | cursor: not-allowed; 105 | } 106 | 107 | .Resizer.disabled:hover { 108 | border-color: transparent; 109 | } 110 | 111 | .gutter { 112 | background: #000; 113 | opacity: .1; 114 | z-index: 1; 115 | -moz-box-sizing: border-box; 116 | -webkit-box-sizing: border-box; 117 | box-sizing: border-box; 118 | -moz-background-clip: padding; 119 | -webkit-background-clip: padding; 120 | background-clip: padding-box; 121 | cursor: ew-resize; 122 | &.gutter-horizontal { 123 | width: 8px !important; 124 | cursor: col-resize; 125 | background-repeat: no-repeat; 126 | background-position: center; 127 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==); 128 | } 129 | &.gutter-vertical { 130 | height: 8px !important; 131 | cursor: row-resize; 132 | width: 100%; 133 | background-repeat: no-repeat; 134 | background-position: center; 135 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=); 136 | } 137 | &:hover { 138 | -webkit-transition: all 1s ease; 139 | transition: all 1s ease; 140 | opacity: 0.2; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/styles/virtualized/index.less: -------------------------------------------------------------------------------- 1 | .virtual-table .ant-table-container:before, 2 | .virtual-table .ant-table-container:after { 3 | display: none; 4 | } 5 | .virtual-table-cell { 6 | box-sizing: border-box; 7 | padding: 16px; 8 | border-bottom: 1px solid #e8e8e8; 9 | background: #fff; 10 | } 11 | [data-theme='dark'] .virtual-table-cell { 12 | box-sizing: border-box; 13 | padding: 16px; 14 | border-bottom: 1px solid #303030; 15 | background: #141414; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noUnusedLocals": true, 4 | "noUnusedParameters": true, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "strictNullChecks": false, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "declaration": true, 16 | "jsx": "react", 17 | "module": "commonjs", 18 | "moduleResolution": "node", 19 | "target": "esnext", 20 | "outDir": "lib", 21 | "lib": [ 22 | "dom", 23 | "esnext" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*", 28 | "types/*" 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "lib" 34 | ] 35 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react"], 3 | "rules": { 4 | "interface-name": false, 5 | "quotemark": [true, "single", "jsx-double"], 6 | "import-sources-order": false, 7 | "object-literal-sort-keys": false, 8 | "ordered-imports": false, 9 | "max-line-length": false, 10 | "indent": false, 11 | "prefer-for-of": false, 12 | "no-console": false, 13 | "member-access": false, 14 | "member-ordering": false, 15 | "jsx-no-multiline-js": false, 16 | "semicolon": false, 17 | "arrow-parens": [true, "ban-single-arg-parens"], 18 | "no-shadowed-variable": false, 19 | "jsx-no-lambda": false, 20 | "jsx-wrap-multiline": false, 21 | "array-type": false, 22 | "variable-name": false, 23 | "object-literal-key-quotes": false 24 | }, 25 | "linterOptions": { 26 | "exclude": ["**/node_modules/**"] 27 | } 28 | } -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const PUBLIC_URL: string; 2 | 3 | declare namespace echarts { 4 | namespace EChartOption { 5 | /** 6 | * Line style 7 | */ 8 | interface LineStyle { 9 | color?: string | string[] | any; 10 | width?: number; 11 | type?: 'solid' | 'dashed' | 'dotted'; 12 | shadowBlur?: number; 13 | shadowColor?: string; 14 | shadowOffsetX?: number; 15 | shadowOffsetY?: number; 16 | opacity?: number; 17 | } 18 | } 19 | } 20 | 21 | declare interface Window { 22 | less: any; 23 | } 24 | 25 | declare module 'react-split' { 26 | interface SplitProps { 27 | direction?: 'vertical' | 'horizontal'; 28 | cursor?: string; 29 | sizes?: number[]; 30 | minSize?: number | number[]; 31 | expandToMin?: boolean; 32 | gutterSize?: number; 33 | gutterAlign?: 'center' | 'start' | 'end'; 34 | snapOffset?: number; 35 | dragInterval?: number; 36 | gutter?: (index, direction, pairElement) => HTMLElement; 37 | elementStyle?: (dimension, elementSize, gutterSize, index) => Object; 38 | gutterStyle?: (dimension, gutterSize, index) => Object; 39 | onDrag?: (sizes: number[]) => void; 40 | onDragEnd?: (sizes: number[]) => void; 41 | style?: React.CSSProperties; 42 | } 43 | export default class Split extends React.Component {} 44 | } -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const path = require('path'); 4 | 5 | const publicURL = process.env.PUBLIC_URL; 6 | const isProduction = process.env.NODE_ENV === 'production'; 7 | 8 | module.exports = { 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(js|jsx|tsx|ts)$/, 13 | loader: 'babel-loader?cacheDirectory', 14 | include: path.resolve(__dirname, 'src'), 15 | options: { 16 | presets: [ 17 | ['@babel/preset-env', { modules: false }], 18 | '@babel/preset-react', 19 | '@babel/preset-typescript', 20 | ], 21 | plugins: [ 22 | '@babel/plugin-transform-runtime', 23 | '@babel/plugin-syntax-dynamic-import', 24 | ['@babel/plugin-proposal-decorators', { legacy: true }], 25 | '@babel/plugin-syntax-async-generators', 26 | ['@babel/plugin-proposal-class-properties', { loose: false }], 27 | '@babel/plugin-proposal-object-rest-spread', 28 | 'react-hot-loader/babel', 29 | 'dynamic-import-webpack', 30 | ['import', { libraryName: 'antd', style: 'css' }], 31 | ], 32 | }, 33 | exclude: /node_modules/, 34 | }, 35 | { 36 | test: /\.(js|jsx|tsx|ts)?$/, 37 | include: /node_modules/, 38 | use: ['react-hot-loader/webpack'], 39 | }, 40 | { 41 | test: /\.(css|less)$/, 42 | use: [ 43 | 'style-loader', 44 | 'css-loader', 45 | { 46 | loader: 'less-loader', 47 | options: { 48 | javascriptEnabled: true, 49 | }, 50 | }, 51 | ], 52 | }, 53 | { 54 | test: /\.(ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 55 | loader: 'url-loader', 56 | options: { 57 | publicPath: './', 58 | name: 'fonts/[hash].[ext]', 59 | limit: 10000, 60 | }, 61 | }, 62 | ], 63 | }, 64 | plugins: [ 65 | new webpack.DefinePlugin({ 66 | PUBLIC_URL: isProduction ? JSON.stringify(publicURL) : JSON.stringify('/'), 67 | }), 68 | new HtmlWebpackPlugin({ 69 | filename: 'index.html', 70 | title: 'React Analytics', 71 | meta: { 72 | description: `Data visualization analysis editor developed with react, antd, echarts`, 73 | }, 74 | }), 75 | ], 76 | optimization: { 77 | splitChunks: { 78 | cacheGroups: { 79 | vendor: { 80 | test: /node_modules/, 81 | chunks: 'initial', 82 | name: 'vendor', 83 | enforce: true, 84 | }, 85 | }, 86 | }, 87 | noEmitOnErrors: true, 88 | }, 89 | resolve: { 90 | // Add `.ts` and `.tsx` as a resolvable extension. 91 | extensions: ['.ts', '.tsx', '.js', 'jsx', '.less'], 92 | }, 93 | node: { 94 | net: 'empty', 95 | fs: 'empty', 96 | tls: 'empty', 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | const path = require('path'); 6 | 7 | const baseConfig = require('./webpack.common.js'); 8 | 9 | const devPort = process.env.DEV_PORT; 10 | const host = process.env.DEV_HOST; 11 | 12 | const proxyHTTP = process.env.DEV_PROXY_HTTP; 13 | const proxyWS = process.env.DEV_PROXY_WS; 14 | 15 | module.exports = merge(baseConfig, { 16 | mode: 'development', 17 | devtool: 'inline-source-map', 18 | entry: { 19 | bundle: [ 20 | '@babel/polyfill', 21 | 'react-hot-loader/patch', 22 | `webpack-dev-server/client?http://${host}:${devPort}`, 23 | 'webpack/hot/only-dev-server', 24 | path.resolve(__dirname, 'src/index.tsx'), 25 | ], 26 | }, 27 | output: { 28 | path: path.resolve(__dirname, 'public'), 29 | publicPath: '/', 30 | filename: '[name].[hash:16].js', 31 | chunkFilename: '[id].[hash:16].js', 32 | }, 33 | devServer: { 34 | inline: true, 35 | port: devPort, 36 | contentBase: path.resolve(__dirname, 'public'), 37 | hot: true, 38 | publicPath: '/', 39 | historyApiFallback: true, 40 | host, 41 | proxy: { 42 | '/api': { 43 | target: proxyHTTP, 44 | }, 45 | '/api/ws': { 46 | target: proxyWS, 47 | ws: true, 48 | }, 49 | }, 50 | headers: { 51 | 'X-Frame-Options': 'sameorigin', // used iframe 52 | }, 53 | }, 54 | plugins: [ 55 | new webpack.HotModuleReplacementPlugin(), // HMR을 사용하기 위한 플러그인 56 | ], 57 | }); 58 | -------------------------------------------------------------------------------- /webpack.lib.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | 5 | const pkg = require('./package.json'); 6 | 7 | const plugins = [ 8 | // 로더들에게 옵션을 넣어주는 플러그인 9 | new webpack.LoaderOptionsPlugin({ 10 | minimize: true, 11 | }), 12 | ]; 13 | module.exports = { 14 | mode: 'production', 15 | entry: { 16 | [pkg.name]: ['@babel/polyfill', path.resolve(__dirname, 'src/components/editor/index.tsx')], 17 | [`${pkg.name}.min`]: ['@babel/polyfill', path.resolve(__dirname, 'src/components/editor/index.tsx')], 18 | }, 19 | output: { 20 | // entry에 존재하는 app.js, vendor.js로 뽑혀 나온다. 21 | path: path.resolve(__dirname, 'dist'), 22 | filename: '[name].js', 23 | library: `${pkg.name}.js`, 24 | libraryTarget: 'umd', 25 | umdNamedDefine: true, 26 | publicPath: './', 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.(js|jsx|tsx|ts)$/, 32 | loader: 'babel-loader?cacheDirectory', 33 | include: path.resolve(__dirname, 'src'), 34 | options: { 35 | presets: [ 36 | ['@babel/preset-env', { modules: false }], 37 | '@babel/preset-react', 38 | '@babel/preset-typescript', 39 | ], 40 | plugins: [ 41 | '@babel/plugin-transform-runtime', 42 | '@babel/plugin-syntax-dynamic-import', 43 | ['@babel/plugin-proposal-decorators', { legacy: true }], 44 | '@babel/plugin-syntax-async-generators', 45 | ['@babel/plugin-proposal-class-properties', { loose: false }], 46 | '@babel/plugin-proposal-object-rest-spread', 47 | 'dynamic-import-webpack', 48 | ], 49 | }, 50 | exclude: /node_modules/, 51 | }, 52 | { 53 | test: /\.(css|less)$/, 54 | use: ['style-loader', 'css-loader', 'less-loader'], 55 | }, 56 | { 57 | test: /\.(ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 58 | loader: 'url-loader', 59 | options: { 60 | publicPath: './', 61 | name: 'fonts/[hash].[ext]', 62 | limit: 10000, 63 | }, 64 | }, 65 | ], 66 | }, 67 | resolve: { 68 | // Add `.ts` and `.tsx` as a resolvable extension. 69 | extensions: ['.ts', '.tsx', '.js', 'jsx'], 70 | }, 71 | optimization: { 72 | minimizer: [ 73 | // we specify a custom UglifyJsPlugin here to get source maps in production 74 | new TerserPlugin({ 75 | include: /\.min\.js$/, 76 | cache: true, 77 | parallel: true, 78 | terserOptions: { 79 | warnings: false, 80 | compress: { 81 | warnings: false, 82 | unused: true, // tree shaking(export된 모듈 중 사용하지 않는 모듈은 포함하지않음) 83 | }, 84 | ecma: 6, 85 | mangle: true, 86 | unused: true, 87 | }, 88 | sourceMap: true, 89 | }), 90 | ], 91 | }, 92 | plugins, 93 | }; 94 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const merge = require('webpack-merge'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const WorkboxPlugin = require('workbox-webpack-plugin'); 8 | 9 | const baseConfig = require('./webpack.common.js'); 10 | 11 | const plugins = [ 12 | // 로더들에게 옵션을 넣어주는 플러그인 13 | new webpack.LoaderOptionsPlugin({ 14 | minimize: true, 15 | }), 16 | // index.html 로 의존성 파일들 inject해주는 플러그인 17 | new WorkboxPlugin.GenerateSW({ 18 | swDest: 'sw.js', 19 | skipWaiting: true, 20 | clientsClaim: true, 21 | }), 22 | ]; 23 | module.exports = merge(baseConfig, { 24 | mode: 'production', 25 | entry: { 26 | vendor: [ 27 | 'react', 28 | 'react-dom', 29 | 'lodash', 30 | 'antd', 31 | ], 32 | app: ['@babel/polyfill', path.resolve(__dirname, 'src/index.tsx')], 33 | }, 34 | output: { 35 | // entry에 존재하는 app.js, vendor.js로 뽑혀 나온다. 36 | path: path.resolve(__dirname, 'docs'), 37 | filename: 'js/[name].[chunkhash:16].js', 38 | chunkFilename: 'js/[id].[chunkhash:16].js', 39 | publicPath: process.env.PUBLIC_URL, 40 | }, 41 | optimization: { 42 | minimizer: [ 43 | // we specify a custom UglifyJsPlugin here to get source maps in production 44 | new TerserPlugin({ 45 | cache: true, 46 | parallel: true, 47 | terserOptions: { 48 | warnings: false, 49 | compress: { 50 | warnings: false, 51 | unused: true, // tree shaking(export된 모듈 중 사용하지 않는 모듈은 포함하지않음) 52 | }, 53 | ecma: 6, 54 | mangle: true, 55 | unused: true, 56 | }, 57 | sourceMap: true, 58 | }), 59 | ], 60 | }, 61 | plugins, 62 | }); 63 | --------------------------------------------------------------------------------