├── .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 |
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 = ;
193 | break;
194 | case 'text':
195 | component = (
196 |
205 | );
206 | break;
207 | case 'password':
208 | component = (
209 |
218 | );
219 | break;
220 | case 'textarea':
221 | component = ;
222 | break;
223 | case 'number':
224 | component = (
225 |
232 | );
233 | break;
234 | case 'boolean':
235 | component = ;
236 | if (typeof value === 'undefined') {
237 | value = true;
238 | }
239 | // TODO... When value render form
240 | break;
241 | case 'select':
242 | value = selectedValues[key as string] || value;
243 | component = (
244 |
265 | );
266 | break;
267 | case 'tags':
268 | component = (
269 |
283 | );
284 | break;
285 | case 'dynamic':
286 | component = ;
287 | break;
288 | case 'color':
289 | component = ;
290 | break;
291 | case 'custom':
292 | component = render ? (
293 | render(form, values, disabled, handleValidate)
294 | ) : formConfig.component ? (
295 |
304 | ) : null;
305 | break;
306 | case 'form':
307 | component = (
308 |
309 | {label}
310 | {Object.keys(forms).map(formKey =>
311 | createFormItem(Array.isArray(key) ? key.map(k => k).concat(formKey) : [key, formKey], (forms as MultipleFormConfig)[formKey]),
312 | )}
313 |
314 | );
315 | break;
316 | case 'slider':
317 | component = ;
318 | break;
319 | default:
320 | component = (
321 |
330 | );
331 | }
332 | const newLabel = description ? (
333 | <>
334 | {icon || null}
335 | {label}
336 |
337 |
338 |
339 |
340 |
341 | >
342 | ) : (
343 | <>
344 | {icon || null}
345 | {label}
346 | >
347 | );
348 | let newValuePropName = typeof value === 'boolean' ? 'checked' : valuePropName || 'value';
349 | if (type === 'dynamic') {
350 | newValuePropName = 'values';
351 | }
352 | return (
353 |
354 |
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 |
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 |
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 |
33 | );
34 | }
35 | return (
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | export default ColorPicker;
43 |
--------------------------------------------------------------------------------
/src/components/picker/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as ColorPicker } from './ColorPicker';
2 |
--------------------------------------------------------------------------------
/src/components/resizer/Resizer.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ResizeObserver from 'resize-observer-polyfill';
3 |
4 | interface IProps {
5 | style?: React.CSSProperties;
6 | className?: string;
7 | children?: (width: number, height: number) => React.ReactNode;
8 | }
9 |
10 | interface IState {
11 | width: number;
12 | height: number;
13 | }
14 |
15 | class Resizer extends Component {
16 | private container = React.createRef();
17 | private resizeObserver: ResizeObserver;
18 |
19 | state: IState = {
20 | width: 0,
21 | height: 0,
22 | }
23 |
24 | componentDidMount() {
25 | this.createObserver();
26 | }
27 |
28 | componentWillUnmount() {
29 | this.cancelObserver();
30 | }
31 |
32 | createObserver = () => {
33 | this.resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
34 | const { width = 0, height = 0 } = entries[0] && entries[0].contentRect || {};
35 | this.setState({
36 | width,
37 | height,
38 | });
39 | });
40 | this.resizeObserver.observe(this.container.current);
41 | }
42 |
43 | cancelObserver = () => {
44 | if (this.resizeObserver) {
45 | this.resizeObserver.disconnect();
46 | }
47 | }
48 |
49 | render() {
50 | const { children, style, className } = this.props;
51 | const { width, height } = this.state;
52 | return (
53 |
54 | {children(width, height)}
55 |
56 | );
57 | }
58 | }
59 |
60 | export default Resizer;
61 |
--------------------------------------------------------------------------------
/src/components/resizer/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Resizer } from './Resizer';
2 |
--------------------------------------------------------------------------------
/src/components/virtualized/VirtualizedTable.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { VariableSizeGrid as Grid } from 'react-window';
3 | import { Table } from 'antd';
4 | import classnames from 'classnames';
5 | import { TableProps } from 'antd/lib/table';
6 | import ResizeObserver from 'rc-resize-observer';
7 | import Scrollbars from 'react-custom-scrollbars';
8 |
9 | export type VirtualizedTableProps = TableProps & {
10 | headerHeight?: number;
11 | };
12 |
13 | const CustomScrollbars = React.forwardRef((props, ref) => {
14 | return ;
15 | });
16 |
17 | const VirtualizedTable: React.FC = props => {
18 | const { className, columns, headerHeight = 55 } = props;
19 | const gridRef = useRef();
20 | const [tableWidth, setTableWidth] = useState(0);
21 | const [tableHeight, setTableHeight] = useState(0);
22 | const [connectObject] = useState(() => {
23 | const obj = {};
24 | Object.defineProperty(obj, 'scrollLeft', {
25 | get: () => null,
26 | set: (scrollLeft: number) => {
27 | if (gridRef.current) {
28 | gridRef.current.scrollTo({ scrollLeft });
29 | }
30 | },
31 | });
32 |
33 | return obj;
34 | });
35 | useEffect(() => resetVirtualGrid, []);
36 | useEffect(() => resetVirtualGrid, [tableWidth]);
37 | const resetVirtualGrid = () => {
38 | gridRef.current.resetAfterIndices({
39 | columnIndex: 0,
40 | shouldForceUpdate: false,
41 | });
42 | };
43 | const widthColumnCount = columns.filter(({ width }) => !width).length;
44 | const mergedColumns = columns.map(column => {
45 | if (column.width) {
46 | return column;
47 | }
48 | return {
49 | ...column,
50 | width: Math.floor(tableWidth / widthColumnCount),
51 | };
52 | });
53 | const handleResize = (size: { width: number; height: number }) => {
54 | setTableHeight(size.height);
55 | setTableWidth(size.width);
56 | };
57 | const renderVirtualGrid = (rawData: object[], { scrollbarSize, ref, onScroll }: any) => {
58 | ref.current = connectObject;
59 | return (
60 | {
66 | const { width } = mergedColumns[index];
67 | return index === mergedColumns.length - 1
68 | ? (width as number) - scrollbarSize - 1
69 | : (width as number);
70 | }}
71 | height={tableHeight - headerHeight}
72 | rowCount={rawData.length}
73 | rowHeight={() => 54}
74 | width={tableWidth}
75 | outerElementType={CustomScrollbars}
76 | onScroll={({ scrollLeft }) => {
77 | onScroll({ scrollLeft });
78 | }}
79 | >
80 | {({ columnIndex, rowIndex, style }) => (
81 |
87 | {rawData[rowIndex][mergedColumns[columnIndex].dataIndex]}
88 |
89 | )}
90 |
91 | );
92 | };
93 | return (
94 |
95 |
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 |
--------------------------------------------------------------------------------