diff --git a/.babelrc b/.babelrc index 4e907cb9..bb194fd3 100644 --- a/.babelrc +++ b/.babelrc @@ -10,7 +10,8 @@ } } ], - "@babel/preset-react" + "@babel/preset-react", + "@babel/preset-typescript" ], "plugins": [ "react-hot-loader/babel", @@ -28,12 +29,8 @@ "legacy": true } ], - ["@babel/plugin-proposal-class-properties", - { - "loose": true - } - ], + "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", - "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-syntax-dynamic-import" ] } diff --git a/.eslintrc.js b/.eslintrc.js index 6d9c20c2..55f930cd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,49 +1,293 @@ module.exports = { - extends: ['airbnb', 'prettier', 'prettier/react'], - parser: 'babel-eslint', - root: true, env: { browser: true, - es6: true + es6: true, + node: true + }, + extends: [ + 'plugin:react/recommended', + 'prettier', + 'prettier/@typescript-eslint', + ], + ignorePatterns: [ + 'coverage', + 'coverage', + 'coverage' + ], + parser: '@typescript-eslint/parser', + parserOptions: { + 'project': 'tsconfig.json', + 'sourceType': 'module' + }, + plugins: [ + 'eslint-plugin-react', + 'eslint-plugin-import', + 'eslint-plugin-jsdoc', + 'eslint-plugin-prefer-arrow', + '@typescript-eslint', + 'prettier', + ], + settings: { + react: { + version: 'detect' + } }, - plugins: ['react', 'import', 'prettier'], rules: { - 'jsx-a11y/anchor-is-valid': [ - 'error', - { - components: ['Link'], - specialLink: ['to'] - } - ], // 允许正常使用 Link - 'jsx-a11y/interactive-supports-focus': 0, - 'jsx-a11y/click-events-have-key-events': 0, - 'no-static-element-interactions': 0, - 'react/jsx-filename-extension': [1, { - extensions: ['.js', '.jsx'] - }], //允许在 .js 后缀文件中写 jsx - 'react/destructuring-assignment': 0, // 不强制对 state props 使用解构赋值 - 'react/forbid-prop-types': 0, // 不禁止使用一些指定的 propTypes - 'react/no-multi-comp': 0, // 可以在一个文件里写多个 react component - 'prefer-destructuring': ['error', { - object: true, - array: false - }], // 不强制要求使用数组解构赋值 - 'no-console': 0, //可以 console - semi: 0, //禁止在语句末尾使用分号 - 'no-unused-expressions': 0, // 支持 func && func() 的写法 - 'no-param-reassign': 0, // 允许修改函数参数 - 'no-plusplus': ['error', { - allowForLoopAfterthoughts: true - }], //允许在循环中使用 i++ / i-- - 'comma-dangle': ['error', 'only-multiline'], // 对象的最后一个元素后不需要逗号 - 'import/extensions': ['off', 'never'], // import 的时候可以不带文件后缀 - 'import/no-unresolved': 0, //import 路径 - 'import/no-extraneous-dependencies': ['error', { - packageDir: './' + 'arrow-spacing': [ + 'error', + { + before: true, + after: true, + } + ], + 'comma-dangle': ['error', 'only-multiline'], + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/array-type': [ + 'error', + { + default: 'array' + } + ], + 'react/display-name': 'off', + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + Object: { + message: 'Avoid using the `Object` type. Did you mean `object`?' + }, + Function: { + message: 'Avoid using the `Function` type. Prefer a specific function type, like `() => void`.' + }, + Boolean: { + message: 'Avoid using the `Boolean` type. Did you mean `boolean`?' + }, + Number: { + message: 'Avoid using the `Number` type. Did you mean `number`?' + }, + String: { + message: 'Avoid using the `String` type. Did you mean `string`?' + }, + Symbol: { + message: 'Avoid using the `Symbol` type. Did you mean `symbol`?' + }, + object: false + } + } + ], + '@typescript-eslint/consistent-type-assertions': 'off', + '@typescript-eslint/dot-notation': 'error', + '@typescript-eslint/explicit-member-accessibility': [ + 'off', + { + accessibility: 'explicit' + } + ], + '@typescript-eslint/indent': [ + 'error', + 2, + { + SwitchCase: 1, + ArrayExpression: 1, + CallExpression: { 'arguments': 1 }, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + } + ], + '@typescript-eslint/member-delimiter-style': [ + 'off', + { + multiline: { + delimiter: 'none', + requireLast: true + }, + singleline: { + delimiter: 'semi', + requireLast: false + } + } + ], + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-parameter-properties': 'off', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/prefer-function-type': 'error', + '@typescript-eslint/prefer-namespace-keyword': 'error', + '@typescript-eslint/quotes': [ + 'error', + 'single', + { allowTemplateLiterals: true } + ], + '@typescript-eslint/semi': [ + 'off', + null + ], + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + types: 'prefer-import', + lib: 'always' + } + ], + '@typescript-eslint/unified-signatures': 'error', + 'arrow-parens': [ + 'off', + 'always' + ], + 'brace-style': [ + 'error', + '1tbs' + ], + complexity: 'off', + 'constructor-super': 'error', + eqeqeq: [ + 'error', + 'smart' + ], + 'id-blacklist': 'error', + 'id-match': 'error', + 'import/no-extraneous-dependencies': 'error', + 'import/no-internal-modules': 'off', + 'import/order': 'error', + 'jsdoc/check-alignment': 'error', + 'jsdoc/newline-after-description': 'error', + 'max-classes-per-file': [ + 'error', + 10 + ], + 'no-bitwise': 'off', + 'no-caller': 'error', + 'no-cond-assign': 'error', + 'no-console': [ + 'off', + { + allow: [ + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'assert', + 'clear', + 'count', + 'countReset', + 'group', + 'groupEnd', + 'table', + 'debug', + 'info', + 'dirxml', + 'groupCollapsed', + 'Console', + 'profile', + 'profileEnd', + 'timeStamp', + 'context' + ] + } + ], + 'no-debugger': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': 'error', + 'no-eval': 'error', + 'no-extra-bind': 'error', + 'no-fallthrough': 'off', + 'no-invalid-this': 'off', + 'no-irregular-whitespace': 'off', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-redeclare': 'error', + 'no-return-await': 'error', + 'no-sequences': 'off', + 'no-shadow': [ + 'off', + { + hoist: 'all' + } + ], + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'one-var': [ + 'error', + 'never' + ], + 'no-multi-spaces': ['error', { ignoreEOLComments: true }], + 'comma-spacing': ['error'], + 'prefer-const': 'error', + 'prefer-object-spread': 'error', + 'react/display-name': 'error', + 'react/jsx-curly-spacing': 'off', + 'react/jsx-equals-spacing': 'off', + 'react/jsx-key': 'error', + 'react/jsx-no-bind': [ + 'error', + { + allowArrowFunctions: true + } + ], + 'react/jsx-no-comment-textnodes': 'error', + 'react/jsx-no-duplicate-props': 'error', + 'react/jsx-no-target-blank': 'error', + 'react/jsx-no-undef': 'error', + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error', + 'react/jsx-wrap-multilines': 'off', + 'react/no-children-prop': 'error', + 'react/no-danger-with-children': 'error', + 'react/no-deprecated': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'error', + 'react/no-is-mounted': 'error', + 'react/no-render-return-value': 'error', + 'react/no-string-refs': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unknown-property': 'error', + 'react/no-unsafe': 'off', + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'error', + 'react/require-render-return': 'error', + 'react/self-closing-comp': ['error'], + 'key-spacing': ['error', { + 'beforeColon': false, + 'afterColon': true + }], + 'space-in-parens': [ + 'error', + 'never' + ], + 'spaced-comment': [ + 'error', + 'always', + { + markers: [ + '/' + ] + } + ], + 'space-infix-ops': 'error', + semi: 1, + 'block-spacing': 'error', + 'space-before-blocks': 'error', + 'space-before-function-paren': ['error', { + 'anonymous': 'never', + 'named': 'never', + 'asyncArrow': 'always' }], - 'prettier/prettier': ['error', { - singleQuote: true, - semi: false - }] + 'object-curly-spacing': ['error', 'always'], + 'use-isnan': 'error', + 'valid-typeof': 'off', + 'jsx-quotes': ['error', 'prefer-double'] } -} +}; diff --git a/.gitignore b/.gitignore index f5cf4588..a5f89fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,9 @@ node_modules/ .vscode *.swp *.lock -*.js *.map .github/workflows/nodejs.yml -app/**/*.js -config/**/*.js app/**/*.map config/**/*.map dist/ diff --git a/.stylelintrc.json b/.stylelintrc.json index ab66a496..32a70920 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -8,6 +8,6 @@ "except": ["value"] }], "max-empty-lines": 2, - "unit-whitelist": ["em", "rem", "%", "s", "px", "deg"] + "unit-allowed-list": ["em", "rem", "%", "s", "px", "deg", "vw", "vh"] } } \ No newline at end of file diff --git a/README.md b/README.md index 821ceaf0..f803eee3 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,18 @@ Nebula Graph Studio (Studio for short) is a web-based visualization tool for Neb ## Development Quick Start -### Set up nebula-graph-studio +### set up studio and server at the same time +``` +$ npm install +$ npm run dev-all +``` + +### Set up nebula-graph-studio only ``` $ npm install $ npm run dev ``` -### Set up go-server +### Set up go-server only ``` $ cd server $ go build -o server @@ -23,20 +29,20 @@ $ nohup ./server & ### 1. Build Web ``` -$ npm run install +$ npm install $ npm run build +$ mv dist server/assets ``` -### 1. Build Web +### 2. Build Server ``` -// remove default port 7001 in config/example-config.yaml first $ cd server +// update default port 9000 to 7001 in config/example-config.yaml first $ go build -o server ``` ### 3. Start ``` -$ mv dist server/assets $ nohup ./server & ``` diff --git a/app/App.less b/app/App.less deleted file mode 100644 index bb7b2a96..00000000 --- a/app/App.less +++ /dev/null @@ -1,185 +0,0 @@ -@import '~#app/common.less'; - -html body { - height: 100%; - padding: 0; - margin: 0; -} - -p { - margin: 0; - padding: 0; -} - -.menu-icon { - width: 16px; - height: 16px; - vertical-align: -0.15em; - overflow: hidden; - fill: #fff; - margin-right: 10px; -} - -.ant-dropdown-menu-item i { - margin-right: 4px; -} - -.ant-modal-close-x { - width: 40px; - height: 40px; - line-height: 40px; -} - -.btns { - text-align: center; - margin-bottom: 20px; - - button > a > i { - margin-right: 8px; - } -} - -#app { - height: 100%; - min-width: 1440px; - - > .ant-spin-nested-loading { - height: 100%; - - > .ant-spin-container { - height: 100%; - } - } - - > .ant-layout { - height: 100%; - } -} - -.nebula-graph-studio { - background: #d2d5da; - - .github-star { - height: @navHeight; - display: flex; - align-items: center; - margin-right: 8px; - - > span { - height: 48px; - } - } - - .setting, - .version { - width: 100px; - text-align: center; - color: #fff; - } - - .lang-select { - > span { - display: inline-block; - padding-right: 16px; - text-align: right; - width: 100px; - color: #fff; - } - - > .ant-select { - width: 80px; - } - } - - .help { - width: 80px; - text-align: center; - color: #fff; - - i { - margin-right: 10px; - } - } - - .ant-layout-header { - display: flex; - z-index: 9; - padding-left: 18px; - height: @navHeight; - - .studio-logo img { - width: 126px; - height: 41px; - } - - > ul { - flex: 1; - height: 100%; - line-height: 64px; - font-size: 20px; - background: #00152a; - border: none; - padding-left: 15px; - - a, - span, - i { - color: #fff; - font-size: 16px; - } - - a:hover, - i:hover { - color: #efefef; - } - - .ant-menu-item { - top: 0; - border-bottom: none; - - .nebula-cloud-icon { - margin-right: 10px; - } - - &:hover { - color: #00152a; - border-bottom: 000; - } - - &.ant-menu-item-selected { - background: #fff; - - a, - span, - i { - color: #00152a; - } - - .icon { - fill: #00152a; - } - } - } - } - } - - .ant-layout-content { - height: 100%; - overflow: auto; - - .padding-page { - padding: 12px 4% 24px 4%; - } - } -} - -.header-title { - font-family: PingFangSC-Medium, serif; - font-size: 20px; - line-height: 18px; - color: #333; - letter-spacing: 1.85px; - border-left: 4px solid #1d9bf6; - padding-left: 12px; -} - diff --git a/app/App.tsx b/app/App.tsx deleted file mode 100644 index 7a807737..00000000 --- a/app/App.tsx +++ /dev/null @@ -1,379 +0,0 @@ -import { Dropdown, Icon, Layout, Menu, Select, Spin } from 'antd'; -import cookies from 'js-cookie'; -import React from 'react'; -import { hot } from 'react-hot-loader/root'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; -import { - Link, - Redirect, - Route, - RouteComponentProps, - Switch, - withRouter, -} from 'react-router-dom'; - -import IconFont from '#app/components/Icon'; -import { INTL_LOCALE_SELECT, INTL_LOCALES } from '#app/config'; -import service from '#app/config/service'; -import { LanguageContext } from '#app/context'; -import Console from '#app/modules/Console'; -import Explore from '#app/modules/Explore'; -import Import from '#app/modules/Import'; -import Schema from '#app/modules/Schema'; -import CreateSpace from '#app/modules/Schema/CreateSpace'; -import SpaceConfig from '#app/modules/Schema/SpaceConfig'; -import '#app/static/fonts/iconfont.css'; -import logo from '#app/static/images/studio-logo.png'; -import { IDispatch, IRootState } from '#app/store'; -import { updateQueryStringParameter } from '#app/utils'; - -import './App.less'; -import ConfigServer from './modules/ConfigServer'; -import PrivateRoute from './PrivateRoute'; -import { handleTrackEvent, trackEvent, trackPageView } from './utils/stat'; - -const { Header, Content } = Layout; -const { Option } = Select; - -interface IState { - loading: boolean; - activeMenu: string; -} - -const mapDispatch = (dispatch: IDispatch) => ({ - asyncClearConfigServer: dispatch.nebula.asyncClearConfigServer, - asyncSwitchSpace: dispatch.nebula.asyncSwitchSpace, -}); - -const mapState = (state: IRootState) => ({ - appVersion: state.app.version, -}); - -interface IProps - extends RouteComponentProps, - ReturnType, - ReturnType {} - -class App extends React.Component { - currentLocale; - constructor(props: IProps) { - super(props); - - const regx = /lang=(\w+)/g; - const match = regx.exec(props.history.location.search); - - if (match) { - cookies.set('locale', match[1].toUpperCase()); - } else { - cookies.set('locale', 'ZH_CN'); - } - - this.currentLocale = cookies.get('locale'); - this.state = { - loading: true, - activeMenu: '', - }; - } - - toggleLanguage = (locale: string) => { - cookies.set('locale', locale); - trackEvent('navigation', 'change_language', locale); - window.location.href = updateQueryStringParameter( - window.location.href, - 'lang', - locale, - ); - }; - - loadIntlLocale = () => { - intl - .init({ - currentLocale: this.currentLocale, - locales: INTL_LOCALES, - }) - .then(() => { - this.setState({ - loading: false, - }); - }); - }; - - handleMenuClick = ({ key }) => { - if (key === 'newRelease') { - return; - } - this.setState({ - activeMenu: key, - }); - }; - - componentWillMount() { - // Initialize the import task - service.handleImportAction({ taskAction: 'actionStopAll' }); - } - - componentDidMount() { - this.loadIntlLocale(); - this.renderMenu(); - const space = sessionStorage.getItem('currentSpace'); - if (space) { - this.props.asyncSwitchSpace(space); - } - document.addEventListener('click', handleTrackEvent); - } - - componentDidUpdate(prevProps: IProps) { - if (prevProps.location.pathname !== this.props.location.pathname) { - this.renderMenu(); - } - } - - componentWillUnmount() { - document.removeEventListener('click', handleTrackEvent); - } - renderMenu = () => { - const path = this.props.location.pathname.split('/')[1] || ''; - this.setState({ - activeMenu: path === 'space' ? 'schema' : path, - }); - }; - - handleClear = () => { - this.props.asyncClearConfigServer(); - }; - - render() { - const { appVersion } = this.props; - const { loading, activeMenu } = this.state; - const locale = cookies.get('locale'); - const nGQLHref = - locale === 'ZH_CN' - ? 'https://docs.nebula-graph.com.cn/2.5.0/3.ngql-guide/1.nGQL-overview/1.overview/' - : 'https://docs.nebula-graph.io/2.5.0/3.ngql-guide/1.nGQL-overview/1.overview/'; - const mannualHref = - locale === 'ZH_CN' - ? 'https://docs.nebula-graph.com.cn/2.5.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/' - : 'https://docs.nebula-graph.io/2.5.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/'; - const versionLogHref = - locale === 'ZH_CN' - ? 'https://docs.nebula-graph.com.cn/2.5.0/nebula-studio/about-studio/st-ug-release-note/' - : 'https://docs.nebula-graph.io/2.5.0/nebula-studio/about-studio/st-ug-release-note/'; - return ( - <> - - {loading ? ( - - ) : ( - -
-
- -
- - - - - {intl.get('common.schema')} - - - - - - {intl.get('common.import')} - - - - - - {intl.get('common.explore')} - - - - - - {intl.get('common.console')} - - - -
- {intl.get('common.languageSelect')}: - -
- - - - - {intl.get('configServer.clear')} - - - - } - > - - {intl.get('common.setting')} - - - - trackPageView('/user-mannual')}> - - - {intl.get('common.use')} - - - trackPageView('/nebula-doc')}> - - - nGQL - - - - - - {intl.get('common.forum')} - - - - } - > - - {intl.get('common.help')} - - - - {appVersion && ( - - - - - {intl.get('common.release')} - - - - } - > - - v{appVersion} - - - - )} -
- - - - - - - - - - - - -
- )} -
- - ); - } -} - -export default withRouter(connect(mapState, mapDispatch)(hot(App))); diff --git a/app/AuthorizedRoute.tsx b/app/AuthorizedRoute.tsx new file mode 100644 index 00000000..39eba8d4 --- /dev/null +++ b/app/AuthorizedRoute.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import { observer } from 'mobx-react-lite'; + +import { useStore } from '@app/stores'; +interface IProps { + component?: any; + render?: any +} + +const AuthorizedRoute = (props: IProps) => { + const { component: Component, render, ...rest } = props; + const { global: { host, username } } = useStore(); + if (host && username) { + return Component ? ( + } /> + ) : ( + + ); + } else { + return ; + } +}; + +export default observer(AuthorizedRoute); diff --git a/app/PrivateRoute.tsx b/app/PrivateRoute.tsx deleted file mode 100644 index 616be22f..00000000 --- a/app/PrivateRoute.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Redirect, Route } from 'react-router-dom'; - -import { IRootState } from './store'; - -const mapState = (state: IRootState) => ({ - host: state.nebula.host, - username: state.nebula.username, -}); - -const mapDispatch = () => ({}); - -const PrivateRoute = ({ component: Component, render, ...rest }) => { - if (rest.host && rest.username) { - return Component ? ( - } /> - ) : ( - - ); - } else { - return ; - } -}; - -export default connect(mapState, mapDispatch)(PrivateRoute); diff --git a/app/app.less b/app/app.less new file mode 100644 index 00000000..9e47f266 --- /dev/null +++ b/app/app.less @@ -0,0 +1,202 @@ +@import './common.less'; + +@font-face { + font-family: 'Roboto-Black'; + src: url(~@app/static/fonts/Roboto-Black.ttf); +} + +@font-face { + font-family: 'Roboto-Bold'; + src: url(~@app/static/fonts/Roboto-Bold.ttf); +} + +@font-face { + font-family: 'Roboto-Light'; + src: url(~@app/static/fonts/Roboto-Light.ttf); +} + +@font-face { + font-family: 'Roboto-Medium'; + src: url(~@app/static/fonts/Roboto-Medium.ttf); +} + +@font-face { + font-family: 'Roboto-Regular'; + src: url(~@app/static/fonts/Roboto-Regular.ttf); +} + +#studio-app { + font-family: Roboto-Regular, sans-serif; +} + +.center-layout { + width: @containerWidth; + margin: 0 auto; +} + +.studio-tab-header { + display: flex; + justify-content: center; + padding-bottom: 16px; + border-bottom: 1px solid @gray; +} + +.ant-radio-group.nebula-tab-group { + background: @lightGray; + border-radius: 20px; + padding: 4px; + min-width: 400px; + display: flex; + justify-content: center; + + .ant-radio-button-wrapper { + border-radius: 20px; + flex: 1; + text-align: center; + border: none; + white-space: nowrap; + color: @darkBlue; + + &.ant-radio-button-wrapper-checked { + color: #fff; + } + + &:not(.ant-radio-button-wrapper-checked) { + background: none; + } + + &::before { + width: 0; + } + } +} + +.ant-btn { + font-family: Roboto-Regular, sans-serif; +} + +.ant-btn.warning-btn { + color: @red; + border-color: @red; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 3px; + + svg { + width: 20px; + height: 20px; + } + + &.ant-btn-link { + border: none; + } + + &:hover { + color: @red; + border-color: @red; + } +} + +.ant-btn.primary-btn { + color: @blue; + border-color: @blue; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 3px; + + svg { + width: 20px; + height: 20px; + } + + &.ant-btn-link { + border: none; + } + + &:hover { + color: @blue; + border-color: @blue; + } +} + +.ant-btn.cancel-btn { + color: @darkGray; + border-color: @darkGray; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 3px; + + svg { + width: 20px; + height: 20px; + } + + &.ant-btn-link { + border: none; + } + + &:hover { + color: @darkGray; + border-color: @darkGray; + } +} + +.ant-btn.studio-add-btn { + border-radius: 3px; + display: inline-flex; + align-items: center; + padding: 0 29px; + + a { + display: flex; + align-items: center; + } + + .studio-add-btn-icon { + display: inline-flex; + margin-right: 10px; + height: 22px; + + > svg { + width: 22px; + height: 22px; + } + + & ~ span { + margin-left: 0; + } + } +} + +.studio-form-footer { + position: fixed; + left: 0; + bottom: 0; + z-index: 10; + width: 100%; + height: 98px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + box-shadow: 0 -4px 4px rgba(0, 0, 0, 0.1); + + button { + width: 236px; + + &:not(:last-child) { + margin-right: 50px; + } + } +} + +.ant-form-item-label label { + font-family: Roboto-Bold, sans-serif; +} + +.ant-table .ant-table-row { + background: #fff; +} diff --git a/app/common.less b/app/common.less index d01831c2..ce08fcf6 100644 --- a/app/common.less +++ b/app/common.less @@ -1,2 +1,10 @@ -@navHeight: 64px; -@controlHeight: 65px; +@gray: #D5DDEB; +@lightWhite: #F8F8F8; +@containerWidth: 1180px; +@darkGray: #8697B0; +@red: #EB5757; +@blue: #2F80ED; +@lightGray: #E9EDEF; +@lightBlue: #F3F6F9; +@darkBlue: #465B7A; +@lightBlack: #172F52; diff --git a/app/components/Avatar/index.tsx b/app/components/Avatar/index.tsx new file mode 100644 index 00000000..dd499af5 --- /dev/null +++ b/app/components/Avatar/index.tsx @@ -0,0 +1,33 @@ +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { Avatar as AntAvatar } from 'antd'; + +interface IProps { + username?: string, + size: 'small'|'large'|'default' +} + +const getColorIndex = (value: string) => { + const index = value.toLowerCase().charCodeAt(0) - 96; + return Math.floor(index / Math.floor(26 / RANDOM_COLOR_PICKER.length + 1)); +}; + +const RANDOM_COLOR_PICKER = ['#345EDA', '#0C89BE', '#1D9E96', '#219A1F', '#D4A600', '#B36235', '#C54262']; + +const Avatar = (props: IProps) => { + const { username, size } = props; + + const [avatarColor, setAvatarColor] = useState(RANDOM_COLOR_PICKER[0]); + useEffect(() => { + if(username) { + const colorIndex = getColorIndex(username[0]); + setAvatarColor(RANDOM_COLOR_PICKER[colorIndex]); + } + }, [username] + ); + return ( + {username && username[0]?.toUpperCase()} + ); +}; + +export default Avatar; \ No newline at end of file diff --git a/app/components/Breadcrumb/index.less b/app/components/Breadcrumb/index.less new file mode 100644 index 00000000..84b294b0 --- /dev/null +++ b/app/components/Breadcrumb/index.less @@ -0,0 +1,52 @@ +@import '~@app/common.less'; +.nebula-breadcrumb { + height: 60px; + background-color: #fff; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + font-size: 18px; + align-items: center; + display: flex; + padding-left: 0; + padding-right: 0; + .arrow-icon > svg { + width: 23px; + height: 23px; + margin-right: 15px; + } + .breadcrumb-container { + display: flex; + justify-content: space-between; + align-items: center; + } + .ant-breadcrumb { + flex: 1; + display: inline-flex; + align-items: center; + font-size: 18px; + margin-right: 15px; + & > span { + display: inline-flex; + align-items: center; + .ant-breadcrumb-link, a { + display: inline-flex; + align-items: center; + } + a, .ant-breadcrumb-link span { + word-break: break-all; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 500px; + } + } + & > span:last-child { + a, span { + color: @darkBlue; + font-weight: 600; + } + } + } + &.has-breadcrumb { + padding: 0; + } +} diff --git a/app/components/Breadcrumb/index.tsx b/app/components/Breadcrumb/index.tsx new file mode 100644 index 00000000..9f13e73b --- /dev/null +++ b/app/components/Breadcrumb/index.tsx @@ -0,0 +1,50 @@ +import { Breadcrumb, PageHeader } from 'antd'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import Icon from '@app/components/Icon'; + +import './index.less'; + +interface IProps { + routes: { + path: string; + breadcrumbName: string; + }[]; + extraNode?: JSX.Element; +} + +const itemRender = (route, _params, routes, _paths) => { + const final = routes.indexOf(route) === routes.length - 1; + const first = routes.indexOf(route) === 0; + return final ? ( + {route.breadcrumbName} + ) : first ? <> + + {route.breadcrumbName} + : ( + + {route.breadcrumbName} + + ); +}; + +const NebulaBreadcrumb: React.FC = (props: IProps) => { + const { routes, extraNode } = props; + return ( + { + return
+ + {extraNode} +
; + }} + /> + ); +}; + +export default NebulaBreadcrumb; diff --git a/app/components/Button/index.less b/app/components/Button/index.less index dfa40507..8f81312c 100644 --- a/app/components/Button/index.less +++ b/app/components/Button/index.less @@ -1,27 +1,18 @@ -.panel-btn-item, -.menu-color { - display: inline-flex; +@import '@app/common.less'; + +svg.btn-icon { + fill: #262626; cursor: pointer; } -.panel-menu-icon { - svg { - width: 30px; - height: 30px; - } - - margin-right: 10px; +svg.btn-actived { + fill: @blue; } -.panel-disabled { +svg.btn-disabled { cursor: not-allowed; - color: #d9d9d9; - - svg { - fill: #d9d9d9; - } } -.panel-actived { - color: #0091ff; +svg.rotate-btn { + transform: rotate(180deg); } diff --git a/app/components/Button/index.tsx b/app/components/Button/index.tsx index b02c3a64..5dc381ce 100644 --- a/app/components/Button/index.tsx +++ b/app/components/Button/index.tsx @@ -1,18 +1,17 @@ -import { Icon, Tooltip } from 'antd'; import classnames from 'classnames'; import React from 'react'; -import IconFont from '#app/components/Icon'; - +import Icon from '@app/components/Icon'; import './index.less'; interface IBtnProps { disabled?: boolean; action?: () => void; + mouseDownAction?: () => void; + mouseUpAction?: () => void; icon?: string; - iconfont?: string; title?: string; className?: string; - active?: boolean; + actived?: boolean; component?: any; trackCategory?: string; trackAction?: string; @@ -21,124 +20,40 @@ interface IBtnProps { interface IMenuButton extends IBtnProps { tips?: string; + id?:string; } -const CustomizeButton = (props: IBtnProps) => { - const { - icon, - iconfont, - action, - disabled, - title, - active, - component, - className, - trackCategory, - trackAction, - trackLabel, - } = props; - return ( -
- {icon && ( - - )} - {iconfont && ( - - )} - {component} - {title && {title}} -
- ); -}; - -// antd Tooltip can't wrap custom component -const CustomizeTooltipBtn = (props: IMenuButton) => { - const { - icon, - iconfont, - action, - disabled, - active, - tips, - component, - trackAction, - trackCategory, - trackLabel, - } = props; - return ( - - {icon ? ( - - ) : iconfont ? ( - - ) : ( -
- {component} -
- )} -
- ); +const MenuButton: React.FC = (props: IMenuButton) => { + const { icon, action, disabled, title, actived, component, className, trackCategory, trackAction, trackLabel } = + props; + return
{ + e.preventDefault(); + if (!disabled && action) { + action(); + } + }} + data-track-category={trackCategory} + data-track-action={trackAction} + data-track-label={trackLabel} + > + {icon && ( + + )} + {component} + {title && {title}} +
; }; - -class MenuButton extends React.PureComponent { - render() { - const { tips, ...rest } = this.props; - if (tips) { - return ; - } else { - return ; - } - } -} export default MenuButton; diff --git a/app/components/CSVPreviewLink.less b/app/components/CSVPreviewLink.less deleted file mode 100644 index 9db52e4f..00000000 --- a/app/components/CSVPreviewLink.less +++ /dev/null @@ -1,25 +0,0 @@ -.csv-preview { - padding: 16px 8px; - overflow: auto; - - table { - td, - th { - text-align: center; - } - } - - > .operation { - padding: 16px; - text-align: center; - } - - .csv-select-index { - margin-right: 10px; - } - - .anticon { - font-size: 16px; - } -} - diff --git a/app/components/CSVPreviewLink.tsx b/app/components/CSVPreviewLink.tsx deleted file mode 100644 index 9bc91154..00000000 --- a/app/components/CSVPreviewLink.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Button, Icon, Table, Tooltip } from 'antd'; -import React from 'react'; -import intl from 'react-intl-universal'; - -import { Modal } from '.'; -import './CSVPreviewLink.less'; - -interface IProps { - file: any; - children: string; - onMapping?: (index) => void; - prop?: string; -} - -class CSVPreviewLink extends React.PureComponent { - modalHandler; - handleLinkClick = () => { - if (this.modalHandler) { - this.modalHandler.show(); - } - }; - - handleMapping = index => { - if (this.props.onMapping) { - this.props.onMapping(index); - this.modalHandler.hide(); - } - }; - render() { - const { onMapping, prop } = this.props; - const { content } = this.props.file; - const columns = content.length - ? content[0].map((_, index) => { - const textIndex = index; - return { - title: onMapping ? ( - <> - - - - - - ) : ( - `column ${textIndex}` - ), - dataIndex: index, - }; - }) - : []; - - return ( - <> - - { - this.modalHandler = handler; - }} - footer={false} - width={1000} - > -
- index.toString()} - /> -
- {onMapping && ( - - )} -
- - - - ); - } -} - -export default CSVPreviewLink; diff --git a/app/components/CSVPreviewLink/index.less b/app/components/CSVPreviewLink/index.less new file mode 100644 index 00000000..cdfcfe9a --- /dev/null +++ b/app/components/CSVPreviewLink/index.less @@ -0,0 +1,56 @@ +@import '~@app/common.less'; +.popover-preview { + max-width: 80%; +} +.csv-preview { + max-width: 600px; + padding: 16px 8px 50px; + overflow: auto; + + table { + td, + th { + text-align: center; + } + .ant-table-tbody { + tr { + background-color: @lightBlue; + } + } + .limit-width { + max-width: 180px; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre-wrap; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + } + .noBackground { + .ant-table-thead > tr > th { + background-color: #fff; + &::before { + content: none !important; + } + } + } + + > .operation { + padding-bottom: 25px; + text-align: center; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + } + + .csv-select-index { + margin-right: 10px; + } + + .anticon { + font-size: 16px; + } +} + diff --git a/app/components/CSVPreviewLink/index.tsx b/app/components/CSVPreviewLink/index.tsx new file mode 100644 index 00000000..3b112e29 --- /dev/null +++ b/app/components/CSVPreviewLink/index.tsx @@ -0,0 +1,79 @@ +import { Button, Popover, Table } from 'antd'; +import { } from 'antd/lib/button'; +import React, { useState } from 'react'; +import intl from 'react-intl-universal'; +import { v4 as uuidv4 } from 'uuid'; +import './index.less'; +import classNames from 'classnames'; + +interface IProps { + file: any; + children: any; + onMapping?: (index) => void; + btnType?: string + selected?: boolean +} + +const CSVPreviewLink = (props: IProps) => { + const { onMapping, file: { content }, children, btnType, selected } = props; + const [visible, setVisible] = useState(false); + const handleLinkClick = e => { + e.stopPropagation(); + setVisible(true); + }; + + const handleMapping = index => { + onMapping && onMapping(index); + setVisible(false); + }; + const columns = content.length + ? content[0].map((_, index) => { + const textIndex = index; + return { + title: onMapping ? ( + + ) : ( + `Column ${textIndex}` + ), + dataIndex: index, + render: value => {value}, + }; + }) + : []; + return ( + setVisible(visible)} + content={
+
uuidv4()} + /> +
+ {onMapping && ( + + )} +
+ } + > + + + ); +}; + +export default CSVPreviewLink; diff --git a/app/components/Codemirror.less b/app/components/CodeMirror/index.less similarity index 100% rename from app/components/Codemirror.less rename to app/components/CodeMirror/index.less diff --git a/app/components/CodeMirror.tsx b/app/components/CodeMirror/index.tsx similarity index 94% rename from app/components/CodeMirror.tsx rename to app/components/CodeMirror/index.tsx index 4a030d79..c46bfbf1 100644 --- a/app/components/CodeMirror.tsx +++ b/app/components/CodeMirror/index.tsx @@ -10,9 +10,9 @@ import 'codemirror/mode/meta'; import 'codemirror/theme/monokai.css'; import React from 'react'; -import { ban, keyWords, maxLineNum, operators } from '#app/config/nebulaQL'; +import { ban, keyWords, maxLineNum, operators } from '@app/config/nebulaQL'; -import './Codemirror.less'; +import './index.less'; interface IProps { options?: object; @@ -51,10 +51,6 @@ export default class ReactCodeMirror extends React.PureComponent { } stream.next(); }, - // blockCommentStart: '/*', - // blockCommentEnd: '*/', - // lineComment: '//' ? '#' : '--', - // closeBrackets: '()[]{}\'\'""``', }; }); @@ -147,7 +143,7 @@ export default class ReactCodeMirror extends React.PureComponent { } }; - async componentWillReceiveProps(nextProps) { + async UNSAFE_componentWillReceiveProps(nextProps) { const { options, value } = nextProps; await this.setOptions(options); if (value !== this.editor.getValue()) { diff --git a/app/components/ColorPicker/index.less b/app/components/ColorPicker/index.less index 4578ca27..dd2d4f24 100644 --- a/app/components/ColorPicker/index.less +++ b/app/components/ColorPicker/index.less @@ -1,20 +1,14 @@ -.popover-color { - .ant-popover-inner-content { - padding: 15px; - } - - .custom-picker { - border: none !important; - box-shadow: initial !important; - border-radius: 0 !important; +.custom-picker { + border: none !important; + box-shadow: initial !important; + border-radius: 0 !important; - > div { - padding: 0 !important; + > div { + padding: 0 !important; - > span > div { - width: 24px !important; - height: 24px !important; - } + > span > div { + width: 24px !important; + height: 24px !important; } } } diff --git a/app/components/ColorPicker/index.tsx b/app/components/ColorPicker/index.tsx index 0210b911..a2da4854 100644 --- a/app/components/ColorPicker/index.tsx +++ b/app/components/ColorPicker/index.tsx @@ -1,57 +1,39 @@ import React from 'react'; import { TwitterPicker } from 'react-color'; -import { COLOR_PICK_LIST } from '#app/config/explore'; +import { COLOR_PICK_LIST } from '@app/config/explore'; import './index.less'; interface IProps { - children?: any; - handleChangeColorComplete?: (color: string) => void; - handleChange?: (color: string) => void; + onChangeComplete?: (color: string) => void; + onChange?: (color: string) => void; } -interface IState { - visible: boolean; -} - -class ColorPicker extends React.Component { - constructor(props: IProps) { - super(props); - this.state = { - visible: false, - }; - } - - handleChange = color => { - if (this.props.handleChange) { - const { hex: _color } = color; - this.props.handleChange(_color); +const ColorPicker: React.FC = (props: IProps) => { + const { onChange, onChangeComplete } = props; + const handleChange = color => { + if (onChange) { + onChange(color); } }; - handleChangeComplete = (color, _event) => { - if (this.props.handleChangeColorComplete) { - const { hex: _color } = color; - this.props.handleChangeColorComplete(_color); + const handleChangeComplete = (color, _event) => { + if (onChangeComplete) { + onChangeComplete(color); } }; - render() { - return ( -
- - {this.props.children} -
- ); - } -} + return ( + + ); +}; export default ColorPicker; diff --git a/app/components/ConfigServerForm/index.less b/app/components/ConfigServerForm/index.less deleted file mode 100644 index b40052d8..00000000 --- a/app/components/ConfigServerForm/index.less +++ /dev/null @@ -1,7 +0,0 @@ -.config-server-form { - margin: auto; - width: 50%; - padding: 16px; - text-align: center; - background: #fff; -} diff --git a/app/components/ConfigServerForm/index.tsx b/app/components/ConfigServerForm/index.tsx deleted file mode 100644 index 69f380aa..00000000 --- a/app/components/ConfigServerForm/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Button, Form, Input } from 'antd'; -import { FormComponentProps } from 'antd/lib/form'; -import { WrappedFormUtils } from 'antd/lib/form/Form'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import { - hostRulesFn, - passwordRulesFn, - usernameRulesFn, -} from '#app/config/rules'; -import { IRootState } from '#app/store'; - -import './index.less'; - -const FormItem = Form.Item; - -const fomrItemLayout = { - labelCol: { span: 6 }, - wrapperCol: { span: 14 }, -}; -const mapState = (state: IRootState) => ({ - loading: state.loading.effects.nebula.asyncConfigServer, -}); -interface IProps extends ReturnType, FormComponentProps { - onConfig: (form: WrappedFormUtils) => void; -} - -const ConfigServerForm = Form.create()((props: IProps) => { - const { onConfig, loading } = props; - const { getFieldDecorator } = props.form; - return ( -
- - {getFieldDecorator('host', { - rules: hostRulesFn(intl), - })()} - - - {getFieldDecorator('username', { - rules: usernameRulesFn(intl), - })()} - - - {getFieldDecorator('password', { - rules: passwordRulesFn(intl), - })()} - - - - ); -}); - -export default connect(mapState)(ConfigServerForm); diff --git a/app/components/DisplayPanel/ExpandForm/index.less b/app/components/DisplayPanel/ExpandForm/index.less deleted file mode 100644 index 04b4f9e7..00000000 --- a/app/components/DisplayPanel/ExpandForm/index.less +++ /dev/null @@ -1,103 +0,0 @@ -.display-expand { - height: 100%; - - .footer { - position: absolute; - bottom: 0; - height: 72px; - width: 100%; - border-top: 1px solid #d4d4d4; - padding: 20px 14px; - - > span { - position: absolute; - top: 50%; - right: 12px; - transform: translateY(-50%); - cursor: pointer; - - svg { - width: 18px; - height: 18px; - fill: #0091ff; - } - } - } - - .header { - position: relative; - - > .ant-tabs { - > .ant-tabs-bar { - margin-bottom: 0; - - .ant-tabs-nav .ant-tabs-tab { - margin-right: 0; - } - } - } - - .btn-view, - .btn-disabled { - position: absolute; - right: 10px; - top: 54%; - transform: translateY(-50%); - width: 24px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - - i svg { - fill: rgba(0, 145, 255, 1); - width: 17px; - height: 17px; - } - } - - .btn-disabled i svg { - fill: gainsboro; - } - - .btn-view:hover { - background-color: #f0f5ff; - border-radius: 5px; - } - } - - .content { - height: calc(100% - 116px); - overflow: auto; - position: relative; - - .row { - min-height: 40px; - display: flex; - align-items: center; - } - - .empty-tip { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - - i svg { - height: 92px; - width: 92px; - fill: #9acfff; - } - - span { - display: block; - font-size: 18px; - color: rgba(0, 0, 0, 0.25); - letter-spacing: 0; - text-align: center; - font-weight: 400; - } - } - } -} diff --git a/app/components/DisplayPanel/ExpandForm/index.tsx b/app/components/DisplayPanel/ExpandForm/index.tsx deleted file mode 100644 index 41bc0381..00000000 --- a/app/components/DisplayPanel/ExpandForm/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { Button, Tabs, Tooltip } from 'antd'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import IconFont from '#app/components/Icon'; -import { exportDataToCSV } from '#app/config/explore'; -import { IRootState } from '#app/store'; -import { INode, IPath } from '#app/utils/interface'; - -import ExpandItem from '../ExpandItem'; -import SelectedGraphDetailShowModal from '../SelectedGraphDetailShowModal'; -import './index.less'; -const TabPane = Tabs.TabPane; - -const mapState = (state: IRootState) => ({ - selectVertexes: state.explore.selectVertexes, - selectEdges: state.explore.selectEdges, - spaceVidType: state.nebula.spaceVidType, -}); - -interface IProps extends ReturnType { - close: () => void; -} - -interface IState { - tagType: 'vertex' | 'edge'; -} - -class DisplayComponent extends React.PureComponent { - SelectedGraphDetailShowModal; - constructor(props: IProps) { - super(props); - this.state = { - tagType: 'vertex', - }; - } - showModal = () => { - const { tagType } = this.state; - this.SelectedGraphDetailShowModal.show(tagType); - }; - - handleChangeType = async (key: string) => { - this.setState({ - tagType: key, - } as Pick); - }; - - exportDataToCSV = () => { - const { selectVertexes, selectEdges } = this.props; - const { tagType } = this.state; - const data = tagType === 'vertex' ? selectVertexes : selectEdges; - exportDataToCSV(data, tagType); - }; - - flattenProps = data => { - if (data.nodeProp && data.nodeProp.properties) { - return this.flattenVertex(data); - } else if (data.edgeProp && data.edgeProp.properties) { - return this.flattenEdge(data); - } - }; - - flattenVertex = data => { - const _data = [ - { - key: 'vid', - value: data.name, - vidType: this.props.spaceVidType, - }, - ] as any; - const properties = data.nodeProp.properties; - Object.keys(properties).forEach(property => { - const valueObj = properties[property]; - Object.keys(valueObj).forEach(field => { - _data.push({ - key: `${property}.${field}`, - value: valueObj[field], - }); - }); - }); - return _data; - }; - - flattenEdge = data => { - const _data = [ - { - key: 'id', - value: data.id, - }, - ]; - const name = data.type; - const properties = data.edgeProp.properties; - Object.keys(properties).forEach(property => { - const value = properties[property]; - _data.push({ - key: `${name}.${property}`, - value, - }); - }); - return _data; - }; - - render() { - const { selectVertexes, selectEdges, close } = this.props; - const { tagType } = this.state; - const data = tagType === 'vertex' ? selectVertexes : selectEdges; - return ( -
-
- - - - -
0 ? 'btn-view' : 'btn-disabled'} - onClick={data.length > 0 ? this.showModal : undefined} - data-track-category="explore" - data-track-action="select_info_modal_view" - > - - - -
-
-
- {data.length > 0 && - data.map((item: INode | IPath, index) => ( - - ))} - {data.length === 0 && ( -
- - {intl.get('common.noSelectedData')} -
- )} -
-
- - -
- (this.SelectedGraphDetailShowModal = modal)} - vertexes={selectVertexes} - edges={selectEdges} - /> -
- ); - } -} - -export default connect(mapState)(DisplayComponent); diff --git a/app/components/DisplayPanel/ExpandItem/index.less b/app/components/DisplayPanel/ExpandItem/index.less deleted file mode 100644 index 2e9f18a6..00000000 --- a/app/components/DisplayPanel/ExpandItem/index.less +++ /dev/null @@ -1,60 +0,0 @@ -.display-row-item { - background: #e6f7ff; - - .item-header { - cursor: pointer; - padding: 0 30px; - background: #bae7ff; - - .display-header-title { - margin-left: 5px; - font-size: 12px; - } - } - - .active { - border-right: 3px solid rgba(0, 145, 255, 1); - } - - .item-content { - padding: 0 50px; - background: #e6f7ff; - min-height: 40px; - display: flex; - - .item-key { - max-width: 100px; - font-size: 12px; - margin-right: 5px; - } - - .item-value { - font-size: 12px; - color: #000; - overflow: hidden; - } - } - - div:nth-child(2) { - padding-top: 18px; - margin-bottom: 13px; - } - - .item-operation { - cursor: pointer; - background: #ddf4ff; - justify-content: center; - font-size: 12px; - color: #0091ff; - - i svg { - fill: #0091ff; - width: 12px; - height: 12px; - } - - span { - margin-left: 5px; - } - } -} diff --git a/app/components/DisplayPanel/ExpandItem/index.tsx b/app/components/DisplayPanel/ExpandItem/index.tsx deleted file mode 100644 index 57e4981c..00000000 --- a/app/components/DisplayPanel/ExpandItem/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Icon } from 'antd'; -import classnames from 'classnames'; -import React from 'react'; -import intl from 'react-intl-universal'; - -import IconFont from '#app/components/Icon'; -import { convertBigNumberToString } from '#app/utils/function'; - -import './index.less'; - -interface IProps { - data: any; - title: string; - index: number; -} - -interface IState { - expandedAll: boolean; - expandedRestInfo: boolean; - needExpandRest: boolean; -} - -const EXPAND_NUM = 3; - -class RowItem extends React.PureComponent { - constructor(props: IProps) { - super(props); - this.state = { - expandedAll: false, - expandedRestInfo: false, - needExpandRest: false, - }; - } - componentDidMount() { - const { data, index } = this.props; - if (data.length > EXPAND_NUM) { - this.setState({ needExpandRest: true }); - } - if (index === 0) { - this.setState({ expandedAll: true }); - } - } - - toggleExpandAll = () => { - const { expandedAll } = this.state; - this.setState({ - expandedAll: !expandedAll, - }); - }; - toggleExpandRest = () => { - const { expandedRestInfo } = this.state; - this.setState({ - expandedRestInfo: !expandedRestInfo, - }); - }; - - handleShowValue = data => { - const { key, value } = data; - if (typeof value === 'string') { - if (key === 'vid' && data.vidType === 'INT64') { - return value; - } else { - return JSON.stringify(value, (_, v) => { - return v.replace(/\u0000+$/, ''); - }); - } - } else if (typeof value === 'boolean') { - return value.toString(); - } else { - return convertBigNumberToString(value); // TODO: bigint in props does not be convert - } - }; - - render() { - const { expandedAll, needExpandRest, expandedRestInfo } = this.state; - const { data, title } = this.props; - return ( -
-
- {expandedAll ? : } - {title} -
- {expandedAll && ( - <> - {data.slice(0, EXPAND_NUM).map(item => ( -
- {item.key} : - {this.handleShowValue(item)} -
- ))} - {needExpandRest && ( - <> - {expandedRestInfo ? ( - <> - {data.slice(EXPAND_NUM).map(item => ( -
- {item.key} - - {this.handleShowValue(item)} - -
- ))} -
- - {intl.get('explore.collapseItem')} -
- - ) : ( -
- - {intl.get('explore.expandItem')} -
- )} - - )} - - )} -
- ); - } -} - -export default RowItem; diff --git a/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.less b/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.less deleted file mode 100644 index 2d22c7bc..00000000 --- a/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.less +++ /dev/null @@ -1,36 +0,0 @@ -.modal-show-selected { - .ant-modal-header { - padding: 0; - - .ant-tabs { - padding-left: 32px; - - .ant-tabs-bar { - margin: 0; - border-bottom: none; - } - } - } - - .operation { - display: flex; - justify-content: space-between; - - .btns { - display: flex; - align-items: center; - - button { - margin-right: 15px; - } - - .icon-instruction svg { - margin-left: 5px; - } - } - - span:first-child { - font-weight: bold; - } - } -} diff --git a/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.tsx b/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.tsx deleted file mode 100644 index 2a44849b..00000000 --- a/app/components/DisplayPanel/SelectedGraphDetailShowModal/index.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import { Button, Input, message, Table, Tabs, Tooltip } from 'antd'; -import JSONBigint from 'json-bigint'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; - -import { Instruction, Modal } from '#app/components'; -import { downloadCSVFiles, parseData } from '#app/config/explore'; -import { INode, IPath } from '#app/utils/interface'; - -import './index.less'; -const TabPane = Tabs.TabPane; - -interface IProps { - vertexes: INode[]; - edges: IPath[]; - showType?: 'vertex' | 'edge'; - handlerRef?: (handler) => void; -} - -interface IState { - tagType: string; - _vertexes: any; - _edges: any; - originVertexes: any; - originEdges: any; - searchValue: string; -} -class SelectedGraphDetailShowModal extends React.PureComponent { - modalHandler; - constructor(props: IProps) { - super(props); - this.state = { - tagType: 'vertex', - _vertexes: [], - _edges: [], - originVertexes: [], - originEdges: [], - searchValue: '', - }; - } - - componentDidMount() { - if (this.props.handlerRef) { - this.props.handlerRef({ - show: this.handleOpenModal, - }); - } - } - - handleOpenModal = async type => { - if (this.modalHandler) { - this.handleChangeType(type); - this.modalHandler.show(); - } - }; - - handleChangeType = async (key: string) => { - const { vertexes, edges } = this.props; - this.setState({ - tagType: key, - _vertexes: parseData(vertexes, 'vertex').tables, - originVertexes: parseData(vertexes, 'vertex').tables, - _edges: parseData(edges, 'edge').tables, - originEdges: parseData(edges, 'edge').tables, - searchValue: '', - }); - }; - - handleExportToCSV = () => { - const { tagType, _vertexes, _edges } = this.state; - const data = tagType === 'vertex' ? _vertexes : _edges; - const headers = Object.keys(data[0]); - downloadCSVFiles({ headers, tables: data, title: tagType }); - }; - - handleUpdateValue = e => { - this.setState({ searchValue: e.target.value }); - }; - handleSearch = value => { - const { tagType, originVertexes, originEdges } = this.state; - if (value) { - const data = tagType === 'vertex' ? originVertexes : originEdges; - const reg = /([a-zA-Z0-9_.]*)\s*([\=\>\<\!]+)\s*(.*)/g; - const searchInfo = reg.exec(value) || []; - if (searchInfo.length > 0) { - const key = searchInfo[1]; - const compare = searchInfo[2]; - const result = searchInfo[3]; - const filters = data.filter(item => { - const { attributes, ...rest } = item; - const flattenData = { ...rest, ...JSON.parse(attributes) }; - let checked = false; - if (flattenData[key]) { - const value = flattenData[key].toString(); - const strReg = /^['|"].*['|"]$/; - switch (compare) { - case '=': - if (strReg.test(result)) { - checked = value === result.slice(1, result.length - 1); - } else { - checked = value === result; - } - break; - case '>': - checked = value > result; - break; - case '<': - checked = value < result; - break; - case '<=': - checked = value <= result; - break; - case '>=': - checked = value >= result; - break; - case '<>': - case '!=': - if (strReg.test(result)) { - checked = value !== result.slice(1, result.length - 1); - } else { - checked = value !== result; - } - break; - default: - break; - } - } - return checked; - }); - if (tagType === 'vertex') { - this.setState({ _vertexes: filters }); - } else { - this.setState({ _edges: filters }); - } - } else { - return message.warning(intl.get('explore.expressionError')); - } - } else { - if (tagType === 'vertex') { - this.setState({ - _vertexes: originVertexes, - }); - } else { - this.setState({ - _edges: originEdges, - }); - } - } - }; - - render() { - const { tagType, _vertexes, _edges, searchValue } = this.state; - const data = tagType === 'vertex' ? _vertexes : _edges; - const columns = - tagType === 'vertex' - ? [ - { - title: 'vid', - dataIndex: 'vid', - align: 'center' as const, - }, - { - title: 'attributes', - dataIndex: 'attributes', - ellipsis: true, - align: 'center' as const, - render: record => { - return ( - - {JSONBigint.stringify( - JSONBigint.parse(record), - (_, value) => { - if (typeof value === 'string') { - return value.replace(/\u0000+$/, ''); - } - return value; - }, - 2, - )} - - } - > - {record} - - ); - }, - }, - ] - : [ - { - title: 'type', - dataIndex: 'type', - align: 'center' as const, - }, - { - title: 'rank', - dataIndex: 'rank', - align: 'center' as const, - }, - { - title: 'srcId', - dataIndex: 'srcId', - align: 'center' as const, - }, - { - title: 'dstId', - dataIndex: 'dstId', - align: 'center' as const, - }, - { - title: 'attributes', - dataIndex: 'attributes', - ellipsis: true, - align: 'center' as const, - render: record => { - return ( - - {JSONBigint.stringify( - JSONBigint.parse(record), - null, - 2, - )} - - } - > - {record} - - ); - }, - }, - ]; - return ( - (this.modalHandler = handler)} - title={ - - - - - } - footer={null} - > -
- - {intl.get('common.total')}: {data.length} - -
- - - -
-
-
index.toString()} - /> - - ); - } -} -export default SelectedGraphDetailShowModal; diff --git a/app/components/DisplayPanel/index.less b/app/components/DisplayPanel/index.less deleted file mode 100644 index 80c2b439..00000000 --- a/app/components/DisplayPanel/index.less +++ /dev/null @@ -1,89 +0,0 @@ -@import '~#app/common.less'; - -.display-drawer { - position: absolute; - top: 0; - height: 100%; - z-index: 0; - - .ant-drawer-content-wrapper { - border-right: 1px solid #d9d9d9; - box-shadow: none !important; - } - - .ant-drawer-header { - padding: 14px 14px 0; - border-bottom: none; - - .ant-drawer-title { - border-left: 3px solid #0091ff; - padding-left: 9px; - } - } - - .ant-drawer-body { - padding: 0; - height: 100%; - } -} - -.display-sider { - position: absolute; - top: 0; - left: 0; - width: 55px; - height: 100%; - border-right: 1px solid #eee; - text-align: center; - background: white; - - - .display-label { - width: 45px; - height: 56px; - background: #f5f5f5; - border-radius: 8px; - margin-top: 10px; - display: inline-flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - - i svg { - width: 16px; - height: 16px; - fill: #0091ff; - } - - span { - font-size: 16px; - color: #000; - letter-spacing: 0; - font-weight: 500; - margin-left: 5px; - } - } - - .sider-footer { - width: 100%; - height: 75px; - border-top: 1px solid #d4d4d4; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - bottom: 0; - - .icon-collapse { - cursor: pointer; - - svg { - fill: #0091ff; - width: 17px; - height: 17px; - } - } - } -} diff --git a/app/components/DisplayPanel/index.tsx b/app/components/DisplayPanel/index.tsx deleted file mode 100644 index 31e84908..00000000 --- a/app/components/DisplayPanel/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Drawer } from 'antd'; -import _ from 'lodash'; -import React from 'react'; -import { connect } from 'react-redux'; - -import IconFont from '#app/components/Icon'; -import { IDispatch, IRootState } from '#app/store'; - -import Expand from './ExpandForm'; -import './index.less'; - -const mapState = (state: IRootState) => ({ - selectVertexes: state.explore.selectVertexes, - selectEdges: state.explore.selectEdges, - showDisplayPanel: state.d3Graph.showDisplayPanel, - currentSpace: state.nebula.currentSpace, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - toggleExpand: data => dispatch.d3Graph.update(data), -}); - -interface IProps - extends ReturnType, - ReturnType { - showTitle?: boolean; -} - -class DisplayPanel extends React.PureComponent { - handleClose = () => { - this.props.toggleExpand({ - showDisplayPanel: false, - }); - }; - - handleOpen = () => { - this.props.toggleExpand({ - showDisplayPanel: true, - }); - }; - - render() { - const { - currentSpace, - selectVertexes, - selectEdges, - showDisplayPanel, - } = this.props; - if (currentSpace) { - return ( - <> - - - - {!showDisplayPanel && ( -
-
- - {selectVertexes.length} -
-
- - {selectEdges.length} -
-
- -
-
- )} - - ); - } - return null; - } -} -export default connect(mapState, mapDispatch)(DisplayPanel); diff --git a/app/components/Expand/AddFilterForm/index.less b/app/components/Expand/AddFilterForm/index.less deleted file mode 100644 index 638401cd..00000000 --- a/app/components/Expand/AddFilterForm/index.less +++ /dev/null @@ -1,26 +0,0 @@ - -.form-add-filter { - .ant-form-item { - margin-bottom: 3px; - - .ant-form-item-label { - line-height: initial; - margin-bottom: 3px; - } - } - - input { - width: 230px; - display: block; - } - - .popover-footer { - text-align: center; - margin-top: 16px; - - button { - height: 24px; - margin-right: 10px; - } - } -} diff --git a/app/components/Expand/AddFilterForm/index.tsx b/app/components/Expand/AddFilterForm/index.tsx deleted file mode 100644 index 72c263c1..00000000 --- a/app/components/Expand/AddFilterForm/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Button, Form, Input } from 'antd'; -import { FormComponentProps } from 'antd/lib/form'; -import React from 'react'; -import intl from 'react-intl-universal'; - -import './index.less'; - -interface IProps extends FormComponentProps { - onConfirm: (values) => void; - onCancel: () => void; -} - -class AddFilterForm extends React.PureComponent { - handleAddFilters = () => { - this.props.form.validateFields((err, values) => { - if (!err) { - this.props.onConfirm(values); - } - }); - }; - render() { - const { getFieldDecorator } = this.props.form; - return ( -
-
- - {getFieldDecorator(`field`, { - rules: [ - { - required: true, - }, - ], - })()} - - - {getFieldDecorator(`operator`, { - rules: [ - { - required: true, - }, - ], - })()} - - - {getFieldDecorator(`value`, { - rules: [ - { - required: true, - }, - ], - })()} - - -
- - -
-
- ); - } -} - -export default Form.create({ - mapPropsToFields(_props: IProps) { - return { - onConfirm: Form.createFormField({ - onConfirm: _props.onConfirm, - }), - onCancel: Form.createFormField({ - onCancel: _props.onCancel, - }), - }; - }, -})(AddFilterForm); diff --git a/app/components/Expand/ExpandForm/index.less b/app/components/Expand/ExpandForm/index.less deleted file mode 100644 index 2ffdf75e..00000000 --- a/app/components/Expand/ExpandForm/index.less +++ /dev/null @@ -1,168 +0,0 @@ -.graph-expand { - height: 100%; - - .expand-config { - padding: 12px 12px 0; - height: calc(100% - 72px); - overflow: auto; - - .menu-color { - position: absolute; - bottom: 3px; - right: 0; - } - } - - .expand-footer { - position: absolute; - bottom: 0; - height: 72px; - width: 100%; - border-top: 1px solid #d4d4d4; - padding: 20px 14px; - text-align: center; - - .btn-collapse { - position: absolute; - top: 50%; - left: 12px; - transform: translateY(-50%); - - svg { - width: 18px; - height: 18px; - fill: #0091ff; - } - } - - .icon-instruction { - position: absolute; - top: 50%; - transform: translateY(-50%); - margin-left: 6px; - } - - button { - width: 155px; - border-radius: 2px; - } - } - - .filter-component { - text-align: center; - - .filter-header { - display: flex; - justify-content: space-between; - margin-bottom: 10px; - color: rgba(0, 0, 0, 0.85); - - .btn-reset { - fill: rgba(0, 145, 255, 1); - color: rgba(0, 145, 255, 1); - cursor: pointer; - } - } - - .ant-form-item { - margin-bottom: 0; - - .ant-form-item-control { - line-height: 34px; - } - } - - .select-relation { - width: 70px; - - .ant-select-selection { - border: none; - border-radius: 2px; - background: #f5f5f5; - } - } - - .tag-expression { - margin-right: 0; - width: 100%; - min-height: 40px; - display: flex; - align-items: center; - justify-content: space-between; - padding-left: 15px; - font-size: 14px; - white-space: pre-line; - word-break: break-all; - } - - .btn-add-filter { - i svg { - width: 12px; - height: 12px; - } - } - } - - - .ant-form { - padding-bottom: 24px; - border-bottom: 1px solid #d4d4d4; - - .ant-form-item { - margin-bottom: 12px; - - .ant-form-item-label { - line-height: initial; - } - - .ant-radio-group { - padding-top: 5px; - margin-left: 15px; - position: relative; - - .ant-radio-wrapper { - display: block; - margin-bottom: 4px; - } - - .btn-color { - position: absolute; - right: 0; - bottom: -12px; - } - } - } - - .select-step-type { - margin-bottom: 0; - } - - .input-step { - padding-left: 25px; - display: flex; - align-items: center; - - .ant-form-item { - display: inline-block; - margin-bottom: 0; - padding: 0 5px; - } - } - } - - .btn-gql { - width: 100%; - color: black; - border-radius: 2px; - margin-top: 17px; - } -} - -.select-relation-dropdown { - border-radius: 2px; - background: #f5f5f5; - - .ant-select-dropdown-menu-item-selected { - background: #f5f5f5; - } -} diff --git a/app/components/Expand/ExpandForm/index.tsx b/app/components/Expand/ExpandForm/index.tsx deleted file mode 100644 index 2385f4b7..00000000 --- a/app/components/Expand/ExpandForm/index.tsx +++ /dev/null @@ -1,568 +0,0 @@ -import { - Button, - Form, - Input, - message, - Popover, - Radio, - Select, - Tag, -} from 'antd'; -import { FormComponentProps } from 'antd/lib/form'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import { Instruction } from '#app/components'; -import GQLModal from '#app/components/GQLModal'; -import IconFont from '#app/components/Icon'; -import VertexStyleSet from '#app/components/VertexStyleSet'; -import { DEFAULT_COLOR_PICKER } from '#app/config/explore'; -import { IDispatch, IRootState } from '#app/store'; -import { RELATION_OPERATORS } from '#app/utils/constant'; -import { getExploreMatchGQL } from '#app/utils/gql'; -import { trackEvent } from '#app/utils/stat'; - -import AddFilterForm from '../AddFilterForm'; -import './index.less'; - -const Option = Select.Option; - -const mapState = (state: IRootState) => ({ - edgeTypes: state.nebula.edgeTypes, - edgesFields: state.nebula.edgesFields, - spaceVidType: state.nebula.spaceVidType, - selectVertexes: state.explore.selectVertexes, - exploreRules: state.explore.exploreRules, - getExpandLoading: state.loading.effects.explore.asyncGetExpand, -}); -const mapDispatch = (dispatch: IDispatch) => ({ - asyncGetEdgesAndFields: dispatch.nebula.asyncGetEdgesAndFields, - asyncGetExpand: dispatch.explore.asyncGetExpand, - updateExploreRules: rules => - dispatch.explore.update({ - exploreRules: rules, - }), -}); - -interface IProps - extends FormComponentProps, - ReturnType, - ReturnType { - close: () => void; -} - -interface IFilter { - expression: string; - relation?: string; -} - -interface IState { - filters: IFilter[]; - visible: boolean; - customColor: string; - customIcon: string; -} - -class Expand extends React.Component { - gqlRef; - constructor(props: IProps) { - super(props); - this.state = { - filters: [], - visible: false, - customColor: DEFAULT_COLOR_PICKER, - customIcon: '', - }; - this.gqlRef = React.createRef(); - } - - componentDidMount() { - this.props.asyncGetEdgesAndFields(); - const { - exploreRules: { filters, customIcon }, - } = this.props; - if (filters) { - this.setState({ filters }); - } - if (customIcon) { - this.setState({ customIcon }); - } - } - - renderFilters = () => { - const { filters } = this.state; - const formItems = filters.map((item, index) => ( -
- {index > 0 && ( - - - - )} - - this.handleDeleteFilter(index)} - > - {item.expression} - - -
- )); - return formItems; - }; - - handleUpdateFilter = (value, index) => { - const { filters } = this.state; - filters[index].relation = value; - this.setState({ - filters, - }); - }; - - handleDeleteFilter = index => { - const { filters } = this.state; - this.setState({ - filters: filters.filter((_, i) => i !== index), - }); - }; - - handleExpand = () => { - const { selectVertexes, edgesFields } = this.props; - const { getFieldsValue } = this.props.form; - const { filters, customColor, customIcon } = this.state; - this.props.form.validateFields(async err => { - if (err) { - return; - } - const { - edgeTypes, - edgeDirection, - stepsType, - step, - minStep, - maxStep, - vertexStyle, - quantityLimit, - } = getFieldsValue(); - (this.props.asyncGetExpand({ - filters, - selectVertexes, - edgeTypes, - edgesFields, - edgeDirection, - vertexStyle, - quantityLimit, - stepsType, - step, - minStep, - maxStep, - customColor, - customIcon, - }) as any).then( - async () => { - message.success(intl.get('common.success')); - trackEvent('explore', 'expand', 'ajax success'); - }, - (e: any) => { - trackEvent('explore', 'expand', 'ajax fail'); - if (e.message) { - message.error(e.message); - } else { - message.info(intl.get('common.noData')); - } - }, - ); - }); - }; - - handleAddFilter = data => { - const { field, operator, value } = data; - const { filters } = this.state; - const expression = `${field} ${operator} ${value}`; - const newFilter = - filters.length === 0 - ? { expression } - : { - relation: 'AND', - expression, - }; - this.setState( - { - filters: [...filters, newFilter], - visible: false, - }, - this.handleUpdateRules, - ); - }; - - handleResetFilters = () => { - this.setState({ filters: [] }, this.handleUpdateRules); - }; - handleVisibleChange = visible => { - this.setState({ visible }); - }; - - hide = () => { - this.setState({ - visible: false, - }); - }; - - handleViewGQL = () => { - if (this.gqlRef) { - this.gqlRef.show(); - } - }; - - handleCustomColor = color => { - this.setState( - { - customColor: color, - }, - this.handleUpdateRules, - ); - }; - - handleCustomIcon = icon => { - this.setState( - { - customIcon: icon.content ? icon.type : '', - }, - this.handleUpdateRules, - ); - }; - - handleUpdateRules = () => { - const { getFieldsValue } = this.props.form; - const { filters, customColor, customIcon } = this.state; - setTimeout(() => { - const { - edgeTypes, - edgeDirection, - stepsType, - step, - minStep, - maxStep, - vertexStyle, - quantityLimit, - } = getFieldsValue(); - this.props.updateExploreRules({ - edgeTypes, - edgeDirection, - vertexStyle, - quantityLimit, - stepsType, - step, - minStep, - maxStep, - customColor, - customIcon, - filters, - }); - }, 100); - }; - render() { - const { - edgeTypes, - exploreRules: rules, - selectVertexes, - getExpandLoading, - spaceVidType, - close, - } = this.props; - const { getFieldDecorator, getFieldsValue } = this.props.form; - const { filters, customColor, customIcon } = this.state; - const { - edgeTypes: selectEdgeTypes, - edgeDirection, - stepsType, - step, - minStep, - maxStep, - quantityLimit, - } = getFieldsValue(); - const currentGQL = - selectEdgeTypes && selectEdgeTypes.length - ? getExploreMatchGQL({ - selectVertexes, - edgeTypes: selectEdgeTypes, - filters, - edgeDirection, - quantityLimit, - spaceVidType, - stepsType, - step, - minStep, - maxStep, - }) - : ''; - const fieldTable = this.renderFilters(); - return ( -
-
-
- - {getFieldDecorator('edgeTypes', { - initialValue: - rules.edgeTypes && rules.edgeTypes.length > 0 - ? rules.edgeTypes - : edgeTypes, - rules: [ - { - required: true, - message: 'Edge Type is required', - }, - ], - })( - , - )} - - - {getFieldDecorator('edgeDirection', { - initialValue: rules.edgeDirection || 'outgoing', - rules: [ - { - required: true, - }, - ], - })( - , - )} - - - {getFieldDecorator('stepsType', { - initialValue: rules.stepsType || 'single', - rules: [ - { - required: true, - }, - ], - })( - - {intl.get('explore.singleStep')} - {intl.get('explore.rangeStep')} - , - )} - - {stepsType === 'single' && ( - - {getFieldDecorator('step', { - initialValue: rules.step || '1', - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - { - required: true, - }, - ], - })()} - - )} - {stepsType === 'range' && ( -
- - {getFieldDecorator('minStep', { - initialValue: rules.minStep || '', - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - { - required: true, - }, - ], - })()} - - - - - {getFieldDecorator('maxStep', { - initialValue: rules.maxStep || '', - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - { - required: true, - }, - ], - })()} - -
- )} - - {getFieldDecorator('vertexStyle', { - initialValue: rules.vertexStyle || 'colorGroupByTag', - rules: [ - { - required: true, - }, - ], - })( - - - {intl.get('explore.colorGroupByTag')} - - - {intl.get('explore.customStyle')} - - - , - )} - - - {getFieldDecorator('quantityLimit', { - initialValue: rules.quantityLimit || 100, - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - ], - })()} - -
-
- {intl.get('explore.filter')} -
- - {intl.get('import.reset')} -
-
- {fieldTable} - - } - visible={this.state.visible} - onVisibleChange={this.handleVisibleChange} - trigger="click" - > - - -
- - { - this.gqlRef = handler; - }} - /> - -
-
- - - -
-
- ); - } -} - -export default connect(mapState, mapDispatch)(Form.create()(Expand)); diff --git a/app/components/Expand/index.less b/app/components/Expand/index.less deleted file mode 100644 index f3647210..00000000 --- a/app/components/Expand/index.less +++ /dev/null @@ -1,86 +0,0 @@ -@import '~#app/common.less'; - -.expand-drawer { - top: 0; - z-index: 0; - position: absolute; - - > .ant-drawer-content-wrapper { - border-left: 1px solid #d9d9d9; - box-shadow: none !important; - } - - .ant-drawer-header { - padding: 14px 14px 0; - border-bottom: none; - - .ant-drawer-title { - border-left: 3px solid #0091ff; - padding-left: 9px; - } - } - - .ant-drawer-body { - padding: 0; - height: calc(100% - 36px); - } -} - -.expand-sider { - position: absolute; - top: 0; - right: 0; - width: 55px; - height: 100%; - border-left: 1px solid #eee; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - background: white; - text-align: center; - - .btn-expand { - width: 45px; - height: 56px; - background: #f5f5f5; - border-radius: 8px; - margin-top: 10px; - display: inline-flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - } - - .icon-expand, - .icon-collapse { - cursor: pointer; - - svg { - fill: #0091ff; - } - } - - .icon-expand { - svg { - width: 19px; - height: 19px; - } - } - - .sider-footer { - width: 100%; - height: 75px; - border-top: 1px solid #d4d4d4; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - - .icon-collapse svg { - width: 17px; - height: 17px; - } - } -} diff --git a/app/components/Expand/index.tsx b/app/components/Expand/index.tsx deleted file mode 100644 index 5b54025a..00000000 --- a/app/components/Expand/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Drawer } from 'antd'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import IconFont from '#app/components/Icon'; -import { IDispatch, IRootState } from '#app/store'; - -import Expand from './ExpandForm'; -import './index.less'; - -const mapState = (state: IRootState) => ({ - selectVertexes: state.explore.selectVertexes, - showSider: state.d3Graph.showSider, - currentSpace: state.nebula.currentSpace, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - toggleExpand: data => dispatch.d3Graph.update(data), -}); - -interface IProps - extends ReturnType, - ReturnType { - showTitle?: boolean; -} - -class ExpandBtn extends React.PureComponent { - handleClose = () => { - this.props.toggleExpand({ - showSider: false, - }); - }; - - handleOpen = () => { - this.props.toggleExpand({ - showSider: true, - }); - }; - - render() { - const { currentSpace, showSider } = this.props; - if (currentSpace) { - return ( - <> - {intl.get('explore.expansionConditions')}} - visible={showSider} - className="expand-drawer" - width="300" - onClose={this.handleClose} - getContainer={false} - closable={false} - mask={false} - placement="right" - > - - - {!showSider && ( -
-
- - {intl.get('explore.expand')} -
-
- -
-
- )} - - ); - } - return null; - } -} -export default connect(mapState, mapDispatch)(ExpandBtn); diff --git a/app/components/ForceGraph/canvas-force-graph.js b/app/components/ForceGraph/canvas-force-graph.js new file mode 100644 index 00000000..f1493a49 --- /dev/null +++ b/app/components/ForceGraph/canvas-force-graph.js @@ -0,0 +1,545 @@ +import { + forceSimulation as d3ForceSimulation, + forceLink as d3ForceLink, + forceManyBody as d3ForceManyBody, + forceCenter as d3ForceCenter, + forceRadial as d3ForceRadial +} from 'd3-force-3d'; + +import { Bezier } from 'bezier-js'; + +import Kapsule from 'kapsule'; +import accessorFn from 'accessor-fn'; +import indexBy from 'index-array-by'; + +import { autoColorObjects } from './color-utils'; +import getDagDepths from './dagDepths'; + +// + +const DAG_LEVEL_NODE_RATIO = 2; + +// whenever styling props are changed that require a canvas redraw +const notifyRedraw = (_, state) => state.onNeedsRedraw && state.onNeedsRedraw(); + +export default Kapsule({ + + props: { + graphData: { + default: { + nodes: [], + links: [] + }, + onChange(_, state) { + state.engineRunning = false; + } // Pause simulation + }, + dagMode: { + onChange(dagMode, state) { // td, bu, lr, rl, radialin, radialout + !dagMode && (state.graphData.nodes || []).forEach(n => n.fx = n.fy = undefined); // unfix nodes when disabling dag mode + } + }, + dagLevelDistance: {}, + dagNodeFilter: { default: node => true }, + onDagError: { triggerUpdate: false }, + nodeRelSize: { default: 4, triggerUpdate: false, onChange: notifyRedraw }, // area per val unit + nodeId: { default: 'id' }, + nodeVal: { default: 'val', triggerUpdate: false, onChange: notifyRedraw }, + nodeColor: { default: 'color', triggerUpdate: false, onChange: notifyRedraw }, + nodeAutoColorBy: {}, + nodeCanvasObject: { triggerUpdate: false, onChange: notifyRedraw }, + nodeCanvasObjectMode: { default: () => 'replace', triggerUpdate: false, onChange: notifyRedraw }, + nodeVisibility: { default: true, triggerUpdate: false, onChange: notifyRedraw }, + linkSource: { default: 'source' }, + linkTarget: { default: 'target' }, + linkVisibility: { default: true, triggerUpdate: false, onChange: notifyRedraw }, + linkColor: { default: 'color', triggerUpdate: false, onChange: notifyRedraw }, + linkAutoColorBy: {}, + linkLineDash: { triggerUpdate: false, onChange: notifyRedraw }, + linkWidth: { default: 1, triggerUpdate: false, onChange: notifyRedraw }, + linkCurvature: { default: 0, triggerUpdate: false, onChange: notifyRedraw }, + linkCanvasObject: { triggerUpdate: false, onChange: notifyRedraw }, + linkCanvasObjectMode: { default: () => 'replace', triggerUpdate: false, onChange: notifyRedraw }, + linkDirectionalArrowLength: { default: 0, triggerUpdate: false, onChange: notifyRedraw }, + linkDirectionalArrowColor: { triggerUpdate: false, onChange: notifyRedraw }, + linkDirectionalArrowRelPos: { default: 0.5, triggerUpdate: false, onChange: notifyRedraw }, // value between 0<>1 indicating the relative pos along the (exposed) line + linkDirectionalParticles: { default: 0 }, // animate photons travelling in the link direction + linkDirectionalParticleSpeed: { default: 0.01, triggerUpdate: false }, // in link length ratio per frame + linkDirectionalParticleWidth: { default: 4, triggerUpdate: false }, + linkDirectionalParticleColor: { triggerUpdate: false }, + globalScale: { default: 1, triggerUpdate: false }, + d3AlphaMin: { default: 0, triggerUpdate: false }, + d3AlphaDecay: { default: 0.0228, triggerUpdate: false, onChange(alphaDecay, state) { state.forceLayout.alphaDecay(alphaDecay) } }, + d3AlphaTarget: { default: 0, triggerUpdate: false, onChange(alphaTarget, state) { state.forceLayout.alphaTarget(alphaTarget) } }, + d3VelocityDecay: { default: 0.4, triggerUpdate: false, onChange(velocityDecay, state) { state.forceLayout.velocityDecay(velocityDecay) } }, + warmupTicks: { default: 0, triggerUpdate: false }, // how many times to tick the force engine at init before starting to render + cooldownTicks: { default: Infinity, triggerUpdate: false }, + cooldownTime: { default: 15000, triggerUpdate: false }, // ms + onUpdate: { default: () => { }, triggerUpdate: false }, + onFinishUpdate: { default: () => { }, triggerUpdate: false }, + onEngineTick: { default: () => { }, triggerUpdate: false }, + onEngineStop: { default: () => { }, triggerUpdate: false }, + onNeedsRedraw: { triggerUpdate: false }, + isShadow: { default: false, triggerUpdate: false } + }, + + methods: { + // Expose d3 forces for external manipulation + d3Force: function (state, forceName, forceFn) { + if (forceFn === undefined) { + return state.forceLayout.force(forceName); // Force getter + } + state.forceLayout.force(forceName, forceFn); // Force setter + return this; + }, + d3ReheatSimulation: function (state) { + state.forceLayout.alpha(1); + this.resetCountdown(); + return this; + }, + // reset cooldown state + resetCountdown: function (state) { + state.cntTicks = 0; + state.startTickTime = new Date(); + state.engineRunning = true; + return this; + }, + isEngineRunning: state => !!state.engineRunning, + tickFrame: function (state) { + !state.isShadow && layoutTick(); + paintLinks(); + !state.isShadow && paintArrows(); + !state.isShadow && paintPhotons(); + paintNodes(); + + return this; + + // + + function layoutTick() { + if (state.engineRunning) { + if ( + ++state.cntTicks > state.cooldownTicks || + (new Date()) - state.startTickTime > state.cooldownTime || + (state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin) + ) { + state.engineRunning = false; // Stop ticking graph + state.onEngineStop(); + } else { + state.forceLayout.tick(); // Tick it + state.onEngineTick(); + } + } + } + + function paintNodes() { + const getVisibility = accessorFn(state.nodeVisibility); + const getVal = accessorFn(state.nodeVal); + const getColor = accessorFn(state.nodeColor); + const getNodeCanvasObjectMode = accessorFn(state.nodeCanvasObjectMode); + + const ctx = state.ctx; + + // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing) + const padAmount = state.isShadow / state.globalScale; + + const visibleNodes = state.graphData.nodes.filter(getVisibility); + + ctx.save(); + visibleNodes.forEach(node => { + const nodeCanvasObjectMode = getNodeCanvasObjectMode(node); + + if (state.nodeCanvasObject && (nodeCanvasObjectMode === 'before' || nodeCanvasObjectMode === 'replace')) { + // Custom node before/replace paint + state.nodeCanvasObject(node, ctx, state.globalScale, state.isShadow); + + if (nodeCanvasObjectMode === 'replace') { + ctx.restore(); + return; + } + } + + // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing) + const r = Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize + padAmount; + + ctx.beginPath(); + ctx.arc(node.x, node.y, r, 0, 2 * Math.PI, false); + ctx.fillStyle = getColor(node) || 'rgba(31, 120, 180, 0.92)'; + ctx.fill(); + + if (state.nodeCanvasObject && nodeCanvasObjectMode === 'after') { + // Custom node after paint + state.nodeCanvasObject(node, state.ctx, state.globalScale); + } + }); + ctx.restore(); + } + + function paintLinks() { + const getVisibility = accessorFn(state.linkVisibility); + const getColor = accessorFn(state.linkColor); + const getWidth = accessorFn(state.linkWidth); + const getLineDash = accessorFn(state.linkLineDash); + const getCurvature = accessorFn(state.linkCurvature); + const getLinkCanvasObjectMode = accessorFn(state.linkCanvasObjectMode); + + const ctx = state.ctx; + + // Draw wider lines by 2px on shadow canvas for more precise hovering (due to boundary anti-aliasing) + const padAmount = state.isShadow * 2; + + const visibleLinks = state.graphData.links.filter(getVisibility); + + visibleLinks.forEach(calcLinkControlPoints); // calculate curvature control points for all visible links + + let beforeCustomLinks = [], afterCustomLinks = [], defaultPaintLinks = visibleLinks; + if (state.linkCanvasObject) { + const replaceCustomLinks = [], otherCustomLinks = []; + + visibleLinks.forEach(d => + ({ + before: beforeCustomLinks, + after: afterCustomLinks, + replace: replaceCustomLinks + }[getLinkCanvasObjectMode(d)] || otherCustomLinks).push(d) + ); + defaultPaintLinks = [...beforeCustomLinks, ...afterCustomLinks, ...otherCustomLinks]; + beforeCustomLinks = beforeCustomLinks.concat(replaceCustomLinks); + } + + // Custom link before paints + ctx.save(); + beforeCustomLinks.forEach(link => state.linkCanvasObject(link, ctx, state.globalScale, state.isShadow)); + ctx.restore(); + + // Bundle strokes per unique color/width/dash for performance optimization + const linksPerColor = indexBy(defaultPaintLinks, [getColor, getWidth, getLineDash]); + + ctx.save(); + Object.entries(linksPerColor).forEach(([color, linksPerWidth]) => { + const lineColor = !color || color === 'undefined' ? 'rgba(0,0,0,0.15)' : color; + Object.entries(linksPerWidth).forEach(([width, linesPerLineDash]) => { + const lineWidth = (width || 1) / state.globalScale + padAmount; + Object.entries(linesPerLineDash).forEach(([dashSegments, links]) => { + const lineDashSegments = getLineDash(links[0]); + ctx.beginPath(); + links.forEach(link => { + const start = link.source; + const end = link.target; + if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link + + ctx.moveTo(start.x, start.y); + + const controlPoints = link.__controlPoints; + + if (!controlPoints) { // Straight line + ctx.lineTo(end.x, end.y); + } else { + // Use quadratic curves for regular lines and bezier for loops + ctx[controlPoints.length === 2 ? 'quadraticCurveTo' : 'bezierCurveTo'](...controlPoints, end.x, end.y); + } + }); + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth; + ctx.setLineDash(lineDashSegments || []); + ctx.stroke(); + }); + }); + }); + ctx.restore(); + + // Custom link after paints + ctx.save(); + afterCustomLinks.forEach(link => state.linkCanvasObject(link, ctx, state.globalScale)); + ctx.restore(); + + // + + function calcLinkControlPoints(link) { + const curvature = getCurvature(link); + + if (!curvature) { // straight line + link.__controlPoints = null; + return; + } + + const start = link.source; + const end = link.target; + if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link + + const l = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); // line length + + if (l > 0) { + const a = Math.atan2(end.y - start.y, end.x - start.x); // line angle + const d = l * curvature; // control point distance + + const cp = { // control point + x: (start.x + end.x) / 2 + d * Math.cos(a - Math.PI / 2), + y: (start.y + end.y) / 2 + d * Math.sin(a - Math.PI / 2) + }; + + link.__controlPoints = [cp.x, cp.y]; + } else { // Same point, draw a loop + const d = curvature * 70; + link.__controlPoints = [end.x, end.y - d, end.x + d, end.y]; + } + } + } + + function paintArrows() { + const ARROW_WH_RATIO = 1.6; + const ARROW_VLEN_RATIO = 0.2; + + const getLength = accessorFn(state.linkDirectionalArrowLength); + const getRelPos = accessorFn(state.linkDirectionalArrowRelPos); + const getVisibility = accessorFn(state.linkVisibility); + const getColor = accessorFn(state.linkDirectionalArrowColor || state.linkColor); + const getNodeVal = accessorFn(state.nodeVal); + const ctx = state.ctx; + + ctx.save(); + state.graphData.links.filter(getVisibility).forEach(link => { + const arrowLength = getLength(link); + if (!arrowLength || arrowLength < 0) return; + + const start = link.source; + const end = link.target; + + if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link + + const startR = Math.sqrt(Math.max(0, getNodeVal(start) || 1)) * state.nodeRelSize; + const endR = Math.sqrt(Math.max(0, getNodeVal(end) || 1)) * state.nodeRelSize; + + const arrowRelPos = Math.min(1, Math.max(0, getRelPos(link))); + const arrowColor = getColor(link) || 'rgba(0,0,0,0.28)'; + const arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2; + + // Construct bezier for curved lines + const bzLine = link.__controlPoints && new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y); + + const getCoordsAlongLine = bzLine + ? t => bzLine.get(t) // get position along bezier line + : t => ({ // straight line: interpolate linearly + x: start.x + (end.x - start.x) * t || 0, + y: start.y + (end.y - start.y) * t || 0 + }); + + const lineLen = bzLine + ? bzLine.length() + : Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); + + const posAlongLine = startR + arrowLength + (lineLen - startR - endR - arrowLength) * arrowRelPos; + + const arrowHead = getCoordsAlongLine(posAlongLine / lineLen); + const arrowTail = getCoordsAlongLine((posAlongLine - arrowLength) / lineLen); + const arrowTailVertex = getCoordsAlongLine((posAlongLine - arrowLength * (1 - ARROW_VLEN_RATIO)) / lineLen); + + const arrowTailAngle = Math.atan2(arrowHead.y - arrowTail.y, arrowHead.x - arrowTail.x) - Math.PI / 2; + + ctx.beginPath(); + + ctx.moveTo(arrowHead.x, arrowHead.y); + ctx.lineTo(arrowTail.x + arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y + arrowHalfWidth * Math.sin(arrowTailAngle)); + ctx.lineTo(arrowTailVertex.x, arrowTailVertex.y); + ctx.lineTo(arrowTail.x - arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y - arrowHalfWidth * Math.sin(arrowTailAngle)); + + ctx.fillStyle = arrowColor; + ctx.fill(); + }); + ctx.restore(); + } + + function paintPhotons() { + const getNumPhotons = accessorFn(state.linkDirectionalParticles); + const getSpeed = accessorFn(state.linkDirectionalParticleSpeed); + const getDiameter = accessorFn(state.linkDirectionalParticleWidth); + const getVisibility = accessorFn(state.linkVisibility); + const getColor = accessorFn(state.linkDirectionalParticleColor || state.linkColor); + const ctx = state.ctx; + + ctx.save(); + state.graphData.links.filter(getVisibility).forEach(link => { + const numCyclePhotons = getNumPhotons(link); + + if (!link.hasOwnProperty('__photons') || !link.__photons.length) return; + + const start = link.source; + const end = link.target; + + if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link + + const particleSpeed = getSpeed(link); + const photons = link.__photons || []; + const photonR = Math.max(0, getDiameter(link) / 2) / Math.sqrt(state.globalScale); + const photonColor = getColor(link) || 'rgba(0,0,0,0.28)'; + + ctx.fillStyle = photonColor; + + // Construct bezier for curved lines + const bzLine = link.__controlPoints + ? new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y) + : null; + + let cyclePhotonIdx = 0; + let needsCleanup = false; // whether some photons need to be removed from list + photons.forEach(photon => { + const singleHop = !!photon.__singleHop; + + if (!photon.hasOwnProperty('__progressRatio')) { + photon.__progressRatio = singleHop ? 0 : cyclePhotonIdx / numCyclePhotons; + } + + !singleHop && cyclePhotonIdx++; // increase regular photon index + + photon.__progressRatio += particleSpeed; + + if (photon.__progressRatio >= 1) { + if (!singleHop) { + photon.__progressRatio = photon.__progressRatio % 1; + } else { + needsCleanup = true; + return; + } + } + + const photonPosRatio = photon.__progressRatio; + + const coords = bzLine + ? bzLine.get(photonPosRatio) // get position along bezier line + : { // straight line: interpolate linearly + x: start.x + (end.x - start.x) * photonPosRatio || 0, + y: start.y + (end.y - start.y) * photonPosRatio || 0 + }; + + ctx.beginPath(); + ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false); + ctx.fill(); + }); + + if (needsCleanup) { + // remove expired single hop photons + link.__photons = link.__photons.filter(photon => !photon.__singleHop || photon.__progressRatio <= 1); + } + }); + ctx.restore(); + } + }, + emitParticle: function (state, link) { + if (link) { + !link.__photons && (link.__photons = []); + link.__photons.push({ __singleHop: true }); // add a single hop particle + } + + return this; + } + }, + + stateInit: () => ({ + forceLayout: d3ForceSimulation() + .force('link', d3ForceLink()) + .force('charge', d3ForceManyBody()) + .force('dagRadial', null) + .stop(), + engineRunning: false + }), + + init(canvasCtx, state) { + // Main canvas object to manipulate + state.ctx = canvasCtx; + }, + + update(state) { + state.engineRunning = false; // Pause simulation + state.onUpdate(); + + if (state.nodeAutoColorBy !== null) { + // Auto add color to uncolored nodes + autoColorObjects(state.graphData.nodes, accessorFn(state.nodeAutoColorBy), state.nodeColor); + } + if (state.linkAutoColorBy !== null) { + // Auto add color to uncolored links + autoColorObjects(state.graphData.links, accessorFn(state.linkAutoColorBy), state.linkColor); + } + + // parse links + state.graphData.links.forEach(link => { + link.source = link[state.linkSource]; + link.target = link[state.linkTarget]; + }); + + if (!state.isShadow) { + // Add photon particles + const linkParticlesAccessor = accessorFn(state.linkDirectionalParticles); + state.graphData.links.forEach(link => { + const numPhotons = Math.round(Math.abs(linkParticlesAccessor(link))); + if (numPhotons) { + link.__photons = [...Array(numPhotons)].map(() => ({})); + } else { + delete link.__photons; + } + }); + } + + // Feed data to force-directed layout + state.forceLayout + .stop() + .alpha(1) // re-heat the simulation + .nodes(state.graphData.nodes); + + // add links (if link force is still active) + const linkForce = state.forceLayout.force('link'); + if (linkForce) { + linkForce + .id(d => d[state.nodeId]) + .links(state.graphData.links); + } + + // setup dag force constraints + const nodeDepths = state.dagMode && getDagDepths( + state.graphData, + node => node[state.nodeId], + { + nodeFilter: state.dagNodeFilter, + onLoopError: state.onDagError || undefined + } + ); + const maxDepth = Math.max(...Object.values(nodeDepths || [])); + const dagLevelDistance = state.dagLevelDistance || ( + state.graphData.nodes.length / (maxDepth || 1) * DAG_LEVEL_NODE_RATIO + * (['radialin', 'radialout'].indexOf(state.dagMode) !== -1 ? 0.7 : 1) + ); + + // Fix nodes to x,y for dag mode + if (state.dagMode) { + const getFFn = (fix, invert) => node => !fix + ? undefined + : (nodeDepths[node[state.nodeId]] - maxDepth / 2) * dagLevelDistance * (invert ? -1 : 1); + + const fxFn = getFFn(['lr', 'rl'].indexOf(state.dagMode) !== -1, state.dagMode === 'rl'); + const fyFn = getFFn(['td', 'bu'].indexOf(state.dagMode) !== -1, state.dagMode === 'bu'); + + state.graphData.nodes.filter(state.dagNodeFilter).forEach(node => { + node.fx = fxFn(node); + node.fy = fyFn(node); + }); + } + + // Use radial force for radial dags + state.forceLayout.force('dagRadial', + ['radialin', 'radialout'].indexOf(state.dagMode) !== -1 + ? d3ForceRadial(node => { + const nodeDepth = nodeDepths[node[state.nodeId]] || -1; + return (state.dagMode === 'radialin' ? maxDepth - nodeDepth : nodeDepth) * dagLevelDistance; + }) + .strength(node => state.dagNodeFilter(node) ? 1 : 0) + : null + ); + + for (let i = 0; (i < state.warmupTicks) && !(state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin); i++) { + state.forceLayout.tick(); + } // Initial ticks before starting to render + + this.resetCountdown(); + state.onFinishUpdate(); + } +}); diff --git a/app/components/ForceGraph/color-tracker.js b/app/components/ForceGraph/color-tracker.js new file mode 100644 index 00000000..623ed5c7 --- /dev/null +++ b/app/components/ForceGraph/color-tracker.js @@ -0,0 +1,31 @@ +const int2HexColor = (num) => `#${num.toString(16).padStart(6, '0')}`; +const rgb2Int = (r, g, b) => r * 256 * 256 + g * 256 + b; +let name = 0; +class ColorTracker { + constructor() { + this.name = name++; + this.index = 0; + this.max = 255 * 255 * 255 - 1; + this.registry = {}; + } + + register(obj) { + this.index += 5; + const total = this.index; + const color = int2HexColor(total); + this.registry[total] = obj; + if (this.index >= this.max) { + this.index = 0; + } + return color; + } + + lookup(color) { + return this.registry[rgb2Int(...color)]; + } + clear() { + this.index = 0; + this.registry = {}; + } +} +export default ColorTracker; diff --git a/app/components/ForceGraph/color-utils.js b/app/components/ForceGraph/color-utils.js new file mode 100644 index 00000000..68a8b127 --- /dev/null +++ b/app/components/ForceGraph/color-utils.js @@ -0,0 +1,17 @@ +import { scaleOrdinal } from 'd3-scale'; +import { schemePaired } from 'd3-scale-chromatic'; + +const autoColorScale = scaleOrdinal(schemePaired); + +// Autoset attribute colorField by colorByAccessor property +// If an object has already a color, don't set it +// Objects can be nodes or links +function autoColorObjects(objects, colorByAccessor, colorField) { + if (!colorByAccessor || typeof colorField !== 'string') return; + + objects.filter(obj => !obj[colorField]).forEach(obj => { + obj[colorField] = autoColorScale(colorByAccessor(obj)); + }); +} + +export { autoColorObjects }; diff --git a/app/components/ForceGraph/dagDepths.js b/app/components/ForceGraph/dagDepths.js new file mode 100644 index 00000000..4e23b990 --- /dev/null +++ b/app/components/ForceGraph/dagDepths.js @@ -0,0 +1,51 @@ +export default function({ nodes, links }, idAccessor, { + nodeFilter = () => true, + onLoopError = loopIds => { throw `Invalid DAG structure! Found cycle in node path: ${loopIds.join(' -> ')}.` } +} = {}) { + // linked graph + const graph = {}; + + nodes.forEach(node => graph[idAccessor(node)] = { data: node, out : [], depth: -1, skip: !nodeFilter(node) }); + links.forEach(({ source, target }) => { + const sourceId = getNodeId(source); + const targetId = getNodeId(target); + if (!graph.hasOwnProperty(sourceId)) throw `Missing source node with id: ${sourceId}`; + if (!graph.hasOwnProperty(targetId)) throw `Missing target node with id: ${targetId}`; + const sourceNode = graph[sourceId]; + const targetNode = graph[targetId]; + + sourceNode.out.push(targetNode); + + function getNodeId(node) { + return typeof node === 'object' ? idAccessor(node) : node; + } + }); + + const foundLoops = []; + traverse(Object.values(graph)); + + const nodeDepths = Object.assign({}, ...Object.entries(graph) + .filter(([, node]) => !node.skip) + .map(([id, node]) => ({ [id]: node.depth })) + ); + + return nodeDepths; + + function traverse(nodes, nodeStack = [], currentDepth = 0) { + for (let i=0, l=nodes.length; i idAccessor(d.data)); + if (!foundLoops.some(foundLoop => foundLoop.length === loop.length && foundLoop.every((id, idx) => id === loop[idx]))) { + foundLoops.push(loop); + onLoopError(loop); + } + continue; + } + if (currentDepth > node.depth) { // Don't unnecessarily revisit chunks of the graph + node.depth = currentDepth; + traverse(node.out, [...nodeStack, node], currentDepth + (node.skip ? 0 : 1)); + } + } + } +} \ No newline at end of file diff --git a/app/components/ForceGraph/force-graph.css b/app/components/ForceGraph/force-graph.css new file mode 100644 index 00000000..8ea34c4a --- /dev/null +++ b/app/components/ForceGraph/force-graph.css @@ -0,0 +1,23 @@ +.force-graph-container canvas { + display: block; + user-select: none; + outline: none; + -webkit-tap-highlight-color: transparent; +} + +.force-graph-container .clickable { + cursor: pointer; +} + +.force-graph-container .grabbable { + cursor: move; + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} + +.force-graph-container .grabbable:active { + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; +} diff --git a/app/components/ForceGraph/force-graph.js b/app/components/ForceGraph/force-graph.js new file mode 100644 index 00000000..553d4379 --- /dev/null +++ b/app/components/ForceGraph/force-graph.js @@ -0,0 +1,702 @@ +import { select as d3Select } from 'd3-selection'; +import { zoom as d3Zoom, zoomTransform as d3ZoomTransform } from 'd3-zoom'; +import { drag as d3Drag } from 'd3-drag'; +import { max as d3Max, min as d3Min } from 'd3-array'; +import throttle from 'lodash.throttle'; +import TWEEN from '@tweenjs/tween.js'; +import Kapsule from 'kapsule'; +import accessorFn from 'accessor-fn'; +import ColorTracker from './color-tracker'; + +import CanvasForceGraph from './canvas-force-graph'; +import linkKapsule from './kapsule-link.js'; + +const HOVER_CANVAS_THROTTLE_DELAY = 800; // ms to throttle shadow canvas updates for perf improvement +const ZOOM2NODES_FACTOR = 4; + +// Expose config from forceGraph +const bindFG = linkKapsule('forceGraph', CanvasForceGraph); +const bindBoth = linkKapsule(['forceGraph', 'shadowGraph'], CanvasForceGraph); +const linkedProps = Object.assign( + ...[ + 'nodeColor', + 'nodeAutoColorBy', + 'nodeCanvasObject', + 'nodeCanvasObjectMode', + 'linkColor', + 'linkAutoColorBy', + 'linkLineDash', + 'linkWidth', + 'linkCanvasObject', + 'linkCanvasObjectMode', + 'linkDirectionalArrowLength', + 'linkDirectionalArrowColor', + 'linkDirectionalArrowRelPos', + 'linkDirectionalParticles', + 'linkDirectionalParticleSpeed', + 'linkDirectionalParticleWidth', + 'linkDirectionalParticleColor', + 'dagMode', + 'dagLevelDistance', + 'dagNodeFilter', + 'onDagError', + 'd3AlphaMin', + 'd3AlphaDecay', + 'd3VelocityDecay', + 'warmupTicks', + 'cooldownTicks', + 'cooldownTime', + 'onEngineTick', + 'onEngineStop', + ].map((p) => ({ [p]: bindFG.linkProp(p) })), + ...[ + 'nodeRelSize', + 'nodeId', + 'nodeVal', + 'nodeVisibility', + 'linkSource', + 'linkTarget', + 'linkVisibility', + 'linkCurvature', + ].map((p) => ({ [p]: bindBoth.linkProp(p) })) +); +const linkedMethods = Object.assign( + ...['d3Force', 'd3ReheatSimulation', 'emitParticle'].map((p) => ({ [p]: bindFG.linkMethod(p) })) +); + +function adjustCanvasSize(state) { + if (state.canvas) { + let curWidth = state.canvas.width; + let curHeight = state.canvas.height; + if (curWidth === 300 && curHeight === 150) { + // Default canvas dimensions + curWidth = curHeight = 0; + } + + const pxScale = window.devicePixelRatio; // 2 on retina displays + curWidth /= pxScale; + curHeight /= pxScale; + // Resize canvases + [state.canvas, state.shadowCanvas].forEach((canvas) => { + // Element size + canvas.style.width = `${state.width}px`; + canvas.style.height = `${state.height}px`; + + // Memory size (scaled to avoid blurriness) + canvas.width = state.width * pxScale; + canvas.height = state.height * pxScale; + + // Normalize coordinate system to use css pixels (on init only) + if (!curWidth && !curHeight) { + canvas.getContext('2d').scale(pxScale, pxScale); + } + }); + + // Relative center panning based on 0,0 + const k = d3ZoomTransform(state.canvas).k; + state.zoom.translateBy(state.zoom.__baseElem, (state.width - curWidth) / 2 / k, (state.height - curHeight) / 2 / k); + state.needsRedraw = true; + } +} + +function resetTransform(ctx) { + const pxRatio = window.devicePixelRatio; + ctx.setTransform(pxRatio, 0, 0, pxRatio, 0, 0); +} + +function clearCanvas(ctx, width, height) { + ctx.save(); + resetTransform(ctx); // reset transform + ctx.clearRect(0, 0, width, height); + ctx.restore(); //restore transforms +} + +// + +export default Kapsule({ + props: { + width: { default: 1100, onChange: (_, state) => adjustCanvasSize(state), triggerUpdate: false }, + height: { default: 400, onChange: (_, state) => adjustCanvasSize(state), triggerUpdate: false }, + graphData: { + default: { nodes: [], links: [] }, + onChange: (d, state) => { + if (state.enablePointerInteraction) { + state.colorTracker.clear(); + [ + { type: 'Node', objs: d.nodes }, + { type: 'Link', objs: d.links }, + ].forEach(hexIndex); + } + state.forceGraph.graphData(d); + state.shadowGraph.graphData(d); + + function hexIndex({ type, objs }) { + objs.forEach((d) => { + d.__indexColor = state.colorTracker.register({ type, d }); + }); + } + }, + triggerUpdate: false, + }, + backgroundColor: { + onChange(color, state) { + state.canvas && color && (state.canvas.style.background = color); + }, + triggerUpdate: false, + }, + nodeLabel: { default: 'name', triggerUpdate: false }, + nodePointerAreaPaint: { + onChange(paintFn, state) { + state.shadowGraph.nodeCanvasObject( + !paintFn ? null : (node, ctx, globalScale) => paintFn(node, node.__indexColor, ctx, globalScale) + ); + state.flushShadowCanvas && state.flushShadowCanvas(); + }, + triggerUpdate: false, + }, + linkPointerAreaPaint: { + onChange(paintFn, state) { + state.shadowGraph.linkCanvasObject( + !paintFn ? null : (link, ctx, globalScale) => paintFn(link, link.__indexColor, ctx, globalScale) + ); + state.flushShadowCanvas && state.flushShadowCanvas(); + }, + triggerUpdate: false, + }, + linkLabel: { default: 'name', triggerUpdate: false }, + linkHoverPrecision: { default: 4, triggerUpdate: false }, + minZoom: { + default: 0.01, + onChange(minZoom, state) { + state.zoom.scaleExtent([minZoom, state.zoom.scaleExtent()[1]]); + }, + triggerUpdate: false, + }, + maxZoom: { + default: 1000, + onChange(maxZoom, state) { + state.zoom.scaleExtent([state.zoom.scaleExtent()[0], maxZoom]); + }, + triggerUpdate: false, + }, + enableNodeDrag: { default: true, triggerUpdate: false }, + enableZoomInteraction: { default: true, triggerUpdate: false }, + enablePanInteraction: { default: true, triggerUpdate: false }, + enableZoomPanInteraction: { default: true, triggerUpdate: false }, // to be deprecated + enablePointerInteraction: { + default: true, + onChange(_, state) { + state.hoverObj = null; + }, + triggerUpdate: false, + }, + autoPauseRedraw: { default: true, triggerUpdate: false }, + onNodeDrag: { default: () => {}, triggerUpdate: false }, + onNodeDragEnd: { default: () => {}, triggerUpdate: false }, + onNodeClick: { triggerUpdate: false }, + onNodeRightClick: { triggerUpdate: false }, + onNodeHover: { triggerUpdate: false }, + onLinkClick: { triggerUpdate: false }, + onLinkRightClick: { triggerUpdate: false }, + onLinkHover: { triggerUpdate: false }, + onBackgroundClick: { triggerUpdate: false }, + onBackgroundRightClick: { triggerUpdate: false }, + onZoom: { triggerUpdate: false }, + onZoomEnd: { triggerUpdate: false }, + onRenderFramePre: { triggerUpdate: false }, + onRenderFramePost: { triggerUpdate: false }, + ...linkedProps, + }, + + aliases: { + // Prop names supported for backwards compatibility + stopAnimation: 'pauseAnimation', + }, + + methods: { + graph2ScreenCoords: function (state, x, y) { + const t = d3ZoomTransform(state.canvas); + return { x: x * t.k + t.x, y: y * t.k + t.y }; + }, + screen2GraphCoords: function (state, x, y) { + const t = d3ZoomTransform(state.canvas); + return { x: (x - t.x) / t.k, y: (y - t.y) / t.k }; + }, + centerAt: function (state, x, y, transitionDuration) { + if (!state.canvas) return null; // no canvas yet + + // setter + if (x !== undefined || y !== undefined) { + const finalPos = Object.assign({}, x !== undefined ? { x } : {}, y !== undefined ? { y } : {}); + if (!transitionDuration) { + // no animation + setCenter(finalPos); + } else { + new TWEEN.Tween(getCenter()) + .to(finalPos, transitionDuration) + .easing(TWEEN.Easing.Quadratic.Out) + .onUpdate(setCenter) + .start(); + } + return this; + } + + // getter + return getCenter(); + + // + + function getCenter() { + const t = d3ZoomTransform(state.canvas); + return { x: (state.width / 2 - t.x) / t.k, y: (state.height / 2 - t.y) / t.k }; + } + + function setCenter({ x, y }) { + state.zoom.translateTo( + state.zoom.__baseElem, + x === undefined ? getCenter().x : x, + y === undefined ? getCenter().y : y + ); + state.needsRedraw = true; + } + }, + zoom: function (state, k, transitionDuration) { + if (!state.canvas) return null; // no canvas yet + // setter + if (k !== undefined) { + if (!transitionDuration) { + // no animation + setZoom(k); + } else { + new TWEEN.Tween({ k: getZoom() }) + .to({ k }, transitionDuration) + .easing(TWEEN.Easing.Quadratic.Out) + .onUpdate(({ k }) => setZoom(k)) + .start(); + } + return this; + } + + // getter + return getZoom(); + + // + + function getZoom() { + return d3ZoomTransform(state.canvas).k; + } + + function setZoom(k) { + state.zoom.scaleTo(state.zoom.__baseElem, k); + state.needsRedraw = true; + } + }, + zoomToFit: function (state, transitionDuration = 0, padding = 10, ...bboxArgs) { + const bbox = this.getGraphBbox(...bboxArgs); + if (bbox) { + const center = { + x: (bbox.x[0] + bbox.x[1]) / 2, + y: (bbox.y[0] + bbox.y[1]) / 2, + }; + + const zoomK = Math.max( + 1e-12, + Math.min( + 1e12, + (state.width - padding * 2) / (bbox.x[1] - bbox.x[0]), + (state.height - padding * 2) / (bbox.y[1] - bbox.y[0]) + ) + ); + + this.centerAt(center.x, center.y, transitionDuration); + this.zoom(zoomK, transitionDuration); + } + + return this; + }, + getGraphBbox: function (state, nodeFilter = () => true) { + const getVal = accessorFn(state.nodeVal); + const getR = (node) => Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize; + + const nodesPos = state.graphData.nodes.filter(nodeFilter).map((node) => ({ + x: node.x, + y: node.y, + r: getR(node), + })); + return !nodesPos.length + ? null + : { + x: [d3Min(nodesPos, (node) => node.x - node.r), d3Max(nodesPos, (node) => node.x + node.r)], + y: [d3Min(nodesPos, (node) => node.y - node.r), d3Max(nodesPos, (node) => node.y + node.r)], + }; + }, + pauseAnimation: function (state) { + if (state.animationFrameRequestId) { + cancelAnimationFrame(state.animationFrameRequestId); + state.animationFrameRequestId = null; + } + return this; + }, + resumeAnimation: function (state) { + if (!state.animationFrameRequestId) { + this._animationCycle(); + } + return this; + }, + _destructor: function (state) { + this.pauseAnimation(); + this.graphData({ nodes: [], links: [] }); + }, + getState: (state) => { + return state; + }, + ...linkedMethods, + }, + + stateInit: () => ({ + lastSetZoom: 1, + zoom: d3Zoom(), + forceGraph: new CanvasForceGraph(), + shadowGraph: new CanvasForceGraph() + .cooldownTicks(0) + .nodeColor('__indexColor') + .linkColor('__indexColor') + .isShadow(true), + colorTracker: new ColorTracker(), // indexed objects for rgb lookup + }), + + init: function (domNode, state) { + // Wipe DOM + domNode.innerHTML = ''; + state.domNode = domNode; + // Container anchor for canvas and tooltip + const container = document.createElement('div'); + container.classList.add('force-graph-container'); + container.style.position = 'relative'; + domNode.appendChild(container); + + state.canvas = document.createElement('canvas'); + if (state.backgroundColor) state.canvas.style.background = state.backgroundColor; + container.appendChild(state.canvas); + + state.shadowCanvas = document.createElement('canvas'); + + // Show shadow canvas + state.shadowCanvas.style.position = 'absolute'; + state.shadowCanvas.style.top = '0'; + state.shadowCanvas.style.left = '0'; + state.shadowCanvas.style.pointerEvents = 'none'; + state.shadowCanvas.style.opacity = 0.5; + // container.appendChild(state.shadowCanvas); + + const ctx = state.canvas.getContext('2d'); + const shadowCtx = state.shadowCanvas.getContext('2d'); + + const pointerPos = { x: -1e12, y: -1e12 }; + const getObjUnderPointer = () => { + let obj = null; + const pxScale = window.devicePixelRatio; + const px = + pointerPos.x > 0 && pointerPos.y > 0 + ? shadowCtx.getImageData(pointerPos.x * pxScale, pointerPos.y * pxScale, 1, 1) + : null; + if (px && px.data && px.data[3] !== 0) { + } + // Lookup object per pixel color + px && (obj = state.colorTracker.lookup(px.data)); + // if (obj) { + // console.log(px.data, obj.d) + // } + return obj; + }; + + // Setup node drag interaction + d3Select(state.canvas).call( + d3Drag() + .subject(() => { + if (!state.enableNodeDrag) { + return null; + } + const obj = getObjUnderPointer(); + return obj && obj.type === 'Node' ? obj.d : null; // Only drag nodes + }) + .on('start', (ev) => { + const obj = ev.subject; + obj.__initialDragPos = { x: obj.x, y: obj.y, fx: obj.fx, fy: obj.fy }; + + // keep engine running at low intensity throughout drag + if (!ev.active) { + obj.fx = obj.x; + obj.fy = obj.y; // Fix points + } + + // drag cursor + state.canvas.classList.add('grabbable'); + }) + .on('drag', (ev) => { + const obj = ev.subject; + const initPos = obj.__initialDragPos; + const dragPos = ev; + + const k = d3ZoomTransform(state.canvas).k; + const translate = { + x: initPos.x + (dragPos.x - initPos.x) / k - obj.x, + y: initPos.y + (dragPos.y - initPos.y) / k - obj.y, + }; + + // Move fx/fy (and x/y) of nodes based on the scaled drag distance since the drag start + ['x', 'y'].forEach((c) => (obj[`f${c}`] = obj[c] = initPos[c] + (dragPos[c] - initPos[c]) / k)); + + // prevent freeze while dragging + state.forceGraph + .d3AlphaTarget(0.3) // keep engine running at low intensity throughout drag + .resetCountdown(); // prevent freeze while dragging + + state.isPointerDragging = true; + + obj.__dragged = true; + state.onNodeDrag(obj, translate); + }) + .on('end', (ev) => { + const obj = ev.subject; + const initPos = obj.__initialDragPos; + const translate = { x: obj.x - initPos.x, y: obj.y - initPos.y }; + + if (initPos.fx === undefined) { + obj.fx = undefined; + } + if (initPos.fy === undefined) { + obj.fy = undefined; + } + delete obj.__initialDragPos; + + if (state.forceGraph.d3AlphaTarget()) { + state.forceGraph + .d3AlphaTarget(0) // release engine low intensity + .resetCountdown(); // let the engine readjust after releasing fixed nodes + } + + // drag cursor + state.canvas.classList.remove('grabbable'); + + state.isPointerDragging = false; + + if (obj.__dragged) { + delete obj.__dragged; + state.onNodeDragEnd(obj, translate); + } + }) + ); + + // Setup zoom / pan interaction + state.zoom((state.zoom.__baseElem = d3Select(state.canvas))); // Attach controlling elem for easy access + + state.zoom.__baseElem.on('dblclick.zoom', null); // Disable double-click to zoom + + state.zoom + .filter( + (ev) => + // disable zoom interaction + !ev.button && + state.enableZoomPanInteraction && + (state.enableZoomInteraction || ev.type !== 'wheel') && + (state.enablePanInteraction || ev.type === 'wheel') + ) + .on('zoom', (ev) => { + const t = ev.transform; + [ctx, shadowCtx].forEach((c) => { + resetTransform(c); + c.translate(t.x, t.y); + c.scale(t.k, t.k); + }); + state.onZoom && state.onZoom({ ...t, ...this.centerAt() }); // report x,y coordinates relative to canvas center + state.needsRedraw = true; + }) + .on('end', (ev) => state.onZoomEnd && state.onZoomEnd({ ...ev.transform, ...this.centerAt() })); + + adjustCanvasSize(state); + + state.forceGraph + .onNeedsRedraw(() => (state.needsRedraw = true)) + .onFinishUpdate(() => { + // re-zoom, if still in default position (not user modified) + if (d3ZoomTransform(state.canvas).k === state.lastSetZoom && state.graphData.nodes.length) { + state.zoom.scaleTo( + state.zoom.__baseElem, + (state.lastSetZoom = ZOOM2NODES_FACTOR / Math.cbrt(state.graphData.nodes.length)) + ); + state.needsRedraw = true; + } + }); + + // Setup tooltip + const toolTipElem = document.createElement('div'); + // toolTipElem.classList.add('graph-tooltip'); + // container.appendChild(toolTipElem); + + // Capture pointer coords on move or touchstart + ['pointermove', 'pointerdown'].forEach((evType) => + container.addEventListener( + evType, + (ev) => { + if (evType === 'pointerdown') { + state.isPointerPressed = true; // track click state + state.pointerDownEvent = ev; + } + + // detect pointer drag on canvas pan + !state.isPointerDragging && + ev.type === 'pointermove' && + state.onBackgroundClick && // only bother detecting drags this way if background clicks are enabled (so they don't trigger accidentally on canvas panning) + (ev.pressure > 0 || state.isPointerPressed) && // ev.pressure always 0 on Safari, so we use the isPointerPressed tracker + (ev.pointerType !== 'touch' || + ev.movementX === undefined || + [ev.movementX, ev.movementY].some((m) => Math.abs(m) > 1)) && // relax drag trigger sensitivity on touch events + (state.isPointerDragging = true); + + // update the pointer pos + const offset = getOffset(container); + pointerPos.x = ev.pageX - offset.left; + pointerPos.y = ev.pageY - offset.top; + + // Move tooltip + toolTipElem.style.top = `${pointerPos.y}px`; + toolTipElem.style.left = `${pointerPos.x}px`; + + // + + function getOffset(el) { + const rect = el.getBoundingClientRect(), + scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, + scrollTop = window.pageYOffset || document.documentElement.scrollTop; + return { top: rect.top + scrollTop, left: rect.left + scrollLeft }; + } + }, + { passive: true } + ) + ); + + // Handle click/touch events on nodes/links + container.addEventListener( + 'pointerup', + (ev) => { + state.isPointerPressed = false; + if (state.isPointerDragging) { + state.isPointerDragging = false; + return; // don't trigger click events after pointer drag (pan / node drag functionality) + } + + const cbEvents = [ev, state.pointerDownEvent]; + requestAnimationFrame(() => { + // trigger click events asynchronously, to allow hoverObj to be set (on frame) + if (ev.button === 0) { + // mouse left-click or touch + if (state.hoverObj) { + const fn = state[`on${state.hoverObj.type}Click`]; + fn && fn(state.hoverObj.d, ...cbEvents); + } else { + state.onBackgroundClick && state.onBackgroundClick(...cbEvents); + } + } + + if (ev.button === 2) { + // mouse right-click + if (state.hoverObj) { + const fn = state[`on${state.hoverObj.type}RightClick`]; + fn && fn(state.hoverObj.d, ...cbEvents); + } else { + state.onBackgroundRightClick && state.onBackgroundRightClick(...cbEvents); + } + } + }); + }, + { passive: true } + ); + + container.addEventListener('contextmenu', (ev) => { + if (!state.onBackgroundRightClick && !state.onNodeRightClick && !state.onLinkRightClick) return true; // default contextmenu behavior + ev.preventDefault(); + return false; + }); + + state.forceGraph(ctx); + state.shadowGraph(shadowCtx); + state.ctx = ctx; + // + + const refreshShadowCanvas = throttle(() => { + // wipe canvas + clearCanvas(shadowCtx, state.width, state.height); + + // Adjust link hover area + state.shadowGraph.linkWidth((l) => accessorFn(state.linkWidth)(l) + state.linkHoverPrecision); + + // redraw + const t = d3ZoomTransform(state.canvas); + state.shadowGraph.globalScale(t.k).tickFrame(); + }, HOVER_CANVAS_THROTTLE_DELAY); + state.flushShadowCanvas = refreshShadowCanvas.flush; // hook to immediately invoke shadow canvas paint + + // Kick-off renderer + (this._animationCycle = function animate() { + // IIFE + const doRedraw = + !state.autoPauseRedraw || + !!state.needsRedraw || + state.forceGraph.isEngineRunning() || + state.graphData.links.some((d) => d.__photons && d.__photons.length); + state.needsRedraw = false; + + if (state.enablePointerInteraction) { + doRedraw && refreshShadowCanvas(); + + // Update tooltip and trigger onHover events + const obj = !state.isPointerDragging ? getObjUnderPointer() : null; // don't hover during drag + if (obj !== state.hoverObj) { + const prevObj = state.hoverObj; + const prevObjType = prevObj ? prevObj.type : null; + const objType = obj ? obj.type : null; + + if (prevObjType && prevObjType !== objType) { + // Hover out + const fn = state[`on${prevObjType}Hover`]; + fn && fn(null, prevObj.d); + } + if (objType) { + // Hover in + const fn = state[`on${objType}Hover`]; + fn && fn(obj.d, prevObjType === objType ? prevObj.d : null); + } + + const tooltipContent = obj ? accessorFn(state[`${obj.type.toLowerCase()}Label`])(obj.d) || '' : ''; + toolTipElem.style.visibility = tooltipContent ? 'visible' : 'hidden'; + toolTipElem.innerHTML = tooltipContent; + + // set pointer if hovered object is clickable + state.canvas.classList[ + (obj && state[`on${objType}Click`]) || (!obj && state.onBackgroundClick) ? 'add' : 'remove' + ]('clickable'); + + state.hoverObj = obj; + } + } + + if (doRedraw) { + // Wipe canvas + clearCanvas(ctx, state.width, state.height); + + // Frame cycle + const globalScale = d3ZoomTransform(state.canvas).k; + state.onRenderFramePre && state.onRenderFramePre(ctx, globalScale); + state.forceGraph.globalScale(globalScale).tickFrame(); + state.onRenderFramePost && state.onRenderFramePost(ctx, globalScale); + } + + TWEEN.update(); // update canvas animation tweens + + state.animationFrameRequestId = requestAnimationFrame(animate); + })(); + }, + + update: function updateFn(state) {}, +}); diff --git a/app/components/ForceGraph/index.js b/app/components/ForceGraph/index.js new file mode 100644 index 00000000..015068f1 --- /dev/null +++ b/app/components/ForceGraph/index.js @@ -0,0 +1,2 @@ +import './force-graph.css'; +export { default } from "./force-graph"; \ No newline at end of file diff --git a/app/components/ForceGraph/kapsule-link.js b/app/components/ForceGraph/kapsule-link.js new file mode 100644 index 00000000..41cff6e9 --- /dev/null +++ b/app/components/ForceGraph/kapsule-link.js @@ -0,0 +1,34 @@ +export default function(kapsulePropNames, kapsuleType) { + + const propNames = kapsulePropNames instanceof Array ? kapsulePropNames : [kapsulePropNames]; + + const dummyK = new kapsuleType(); // To extract defaults + + return { + linkProp: function(prop) { // link property config + return { + default: dummyK[prop](), + onChange(v, state) { propNames.forEach(propName => state[propName][prop](v)) }, + triggerUpdate: false + } + }, + linkMethod: function(method) { // link method pass-through + return function(state, ...args) { + const returnVals = []; + propNames.forEach(propName => { + const kapsuleInstance = state[propName]; + const returnVal = kapsuleInstance[method](...args); + + if (returnVal !== kapsuleInstance) { + returnVals.push(returnVal); + } + }); + + return returnVals.length + ? returnVals[0] + : this; // chain based on the parent object, not the inner kapsule + } + } + } + +} \ No newline at end of file diff --git a/app/components/GQLCodeMirror/index.less b/app/components/GQLCodeMirror/index.less index 7fce9361..76ddb274 100644 --- a/app/components/GQLCodeMirror/index.less +++ b/app/components/GQLCodeMirror/index.less @@ -1,6 +1,14 @@ +@import '~@app/common.less'; .export-gql { text-align: left; margin-top: 24px; + background: #FFFFFF; + border: 1px solid @gray; + .ant-collapse-item { + border-bottom: none; + } + box-sizing: border-box; + border-radius: 3px; } .export-gql .ant-collapse-content-box div { diff --git a/app/components/GQLCodeMirror/index.tsx b/app/components/GQLCodeMirror/index.tsx index 06694a08..282ecd40 100644 --- a/app/components/GQLCodeMirror/index.tsx +++ b/app/components/GQLCodeMirror/index.tsx @@ -2,12 +2,12 @@ import { Collapse } from 'antd'; import React from 'react'; import intl from 'react-intl-universal'; -import { CodeMirror } from '#app/components'; +import CodeMirror from '@app/components/CodeMirror'; import './index.less'; const Panel = Collapse.Panel; interface IOptions { - [propName: string]: string; + [propName: string]: any; } const GQLCodeMirror = (props: { currentGQL: string; option?: IOptions }) => { const options = { @@ -20,7 +20,7 @@ const GQLCodeMirror = (props: { currentGQL: string; option?: IOptions }) => { return ( - + ); diff --git a/app/components/GQLModal/index.less b/app/components/GQLModal/index.less deleted file mode 100644 index 80aabf65..00000000 --- a/app/components/GQLModal/index.less +++ /dev/null @@ -1,10 +0,0 @@ -.modal-gql { - .ant-modal-title { - font-size: 14px; - font-weight: 400; - } - - .footer { - text-align: center; - } -} diff --git a/app/components/GQLModal/index.tsx b/app/components/GQLModal/index.tsx deleted file mode 100644 index 6fbbbab8..00000000 --- a/app/components/GQLModal/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Button, message } from 'antd'; -import React from 'react'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; -import intl from 'react-intl-universal'; - -import { CodeMirror } from '#app/components'; - -import { Modal } from '..'; -import './index.less'; -interface IModalHandler { - show: (callback?: any) => void; -} - -interface IProps { - gql: string; - handlerRef?: (handler: IModalHandler) => void; -} - -class GQLModal extends React.PureComponent { - modalHandler; - componentDidMount() { - if (this.props.handlerRef) { - this.props.handlerRef({ - show: this.show, - }); - } - } - - show = () => { - if (this.modalHandler) { - this.modalHandler.show(); - } - }; - - handleCopy = () => { - message.success(intl.get('common.copySuccess')); - }; - - render() { - const { gql } = this.props; - return ( - { - this.modalHandler = handler; - }} - footer={false} - width={700} - > - -
- - - -
-
- ); - } -} - -export default GQLModal; diff --git a/app/components/Icon/index.tsx b/app/components/Icon/index.tsx index 3556a408..28334298 100644 --- a/app/components/Icon/index.tsx +++ b/app/components/Icon/index.tsx @@ -1,14 +1,7 @@ -import React, { HTMLProps } from 'react'; - -interface IIconFontProps extends HTMLProps { - type: string; -} - -const IconFont = (props: IIconFontProps) => { - const { type, className, ...others } = props; - return ( - - ); -}; +import { createFromIconfontCN } from '@ant-design/icons'; +import icon from '@app/static/fonts/iconfont.js'; +const IconFont = createFromIconfontCN({ + scriptUrl: icon, +}); export default IconFont; diff --git a/app/components/IconPicker/iconCfg.ts b/app/components/IconPicker/iconCfg.ts deleted file mode 100644 index 616ed325..00000000 --- a/app/components/IconPicker/iconCfg.ts +++ /dev/null @@ -1,124 +0,0 @@ -const IconCfg = [ - { - type: 'iconimage-iconUnselect', - content: '', - }, - { - type: 'iconimage-icon1', - content: '\ue6fd', - }, - { - type: 'iconimage-icon2', - content: '\ue6ff', - }, - { - type: 'iconimage-icon3', - content: '\ue6fe', - }, - { - type: 'iconimage-icon4', - content: '\ue700', - }, - { - type: 'iconimage-icon5', - content: '\ue701', - }, - { - type: 'iconimage-icon6', - content: '\ue702', - }, - { - type: 'iconimage-icon7', - content: '\ue703', - }, - { - type: 'iconimage-icon8', - content: '\ue704', - }, - { - type: 'iconimage-icon9', - content: '\ue705', - }, - { - type: 'iconimage-icon10', - content: '\ue706', - }, - { - type: 'iconimage-icon11', - content: '\ue709', - }, - { - type: 'iconimage-icon12', - content: '\ue707', - }, - { - type: 'iconimage-icon13', - content: '\ue708', - }, - { - type: 'iconimage-icon14', - content: '\ue714', - }, - { - type: 'iconimage-icon15', - content: '\ue70e', - }, - { - type: 'iconimage-icon16', - content: '\ue70b', - }, - { - type: 'iconimage-icon17', - content: '\ue70a', - }, - { - type: 'iconimage-icon18', - content: '\ue712', - }, - { - type: 'iconimage-icon19', - content: '\ue710', - }, - { - type: 'iconimage-icon20', - content: '\ue70d', - }, - { - type: 'iconimage-icon21', - content: '\ue70c', - }, - { - type: 'iconimage-icon22', - content: '\ue70f', - }, - { - type: 'iconimage-icon23', - content: '\ue711', - }, - { - type: 'iconimage-icon24', - content: '\ue715', - }, - { - type: 'iconimage-icon25', - content: '\ue713', - }, - { - type: 'iconimage-icon26', - content: '\ue716', - }, - { - type: 'iconimage-icon27', - content: '\ue719', - }, - { - type: 'iconimage-icon28', - content: '\ue717', - }, - { - type: 'iconimage-icon29', - content: '\ue718', - }, -]; - -export default IconCfg; diff --git a/app/components/IconPicker/index.less b/app/components/IconPicker/index.less deleted file mode 100644 index 4022752b..00000000 --- a/app/components/IconPicker/index.less +++ /dev/null @@ -1,24 +0,0 @@ -.icon-picker { - .icon-box { - display: inline-block; - width: 40px; - height: 40px; - padding: 5px; - background-color: #f3f2f2; - cursor: pointer; - margin: 5px; - color: #000; - - .nebula-cloud-icon { - font-size: 30px; - display: inline-block; - margin-top: -6px; - } - } - - .slick-slide:nth-child(2) { - .icon-box:first-child { - color: #c00a0a; - } - } -} diff --git a/app/components/IconPicker/index.tsx b/app/components/IconPicker/index.tsx deleted file mode 100644 index cc8e47ad..00000000 --- a/app/components/IconPicker/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Carousel, Popover } from 'antd'; -import { chunk } from 'lodash'; -import React from 'react'; - -import Icon from '#app/components/Icon'; - -import IconCfg from './iconCfg'; -import './index.less'; - -interface IIcon { - type: string; - content: string; -} - -interface IProps { - handleChangeIconComplete?: (icon: IIcon) => void; -} - -const iconGroup = chunk(IconCfg, 16); - -interface IIconItem { - onClick: (icon: IIcon) => void; - icon: IIcon; -} - -const IconItem = (props: IIconItem) => { - const { - onClick, - icon: { type, content }, - } = props; - const iconElement = ( - onClick(props.icon)} /> - ); - - return ( -
- {!!content ? ( - iconElement - ) : ( - {iconElement} - )} -
- ); -}; - -class IconPickerBtn extends React.PureComponent { - handleChangeIconComplete = (icon: IIcon) => { - this.props.handleChangeIconComplete?.(icon); - }; - - render() { - return ( -
- - {iconGroup.map((group, index) => ( -
- {group.map(icon => ( - - ))} -
- ))} -
-
- ); - } -} -export default IconPickerBtn; diff --git a/app/components/Instruction/index.tsx b/app/components/Instruction/index.tsx index 1fc2443b..ead2ff54 100644 --- a/app/components/Instruction/index.tsx +++ b/app/components/Instruction/index.tsx @@ -1,4 +1,5 @@ -import { Icon, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; +import Icon from '@app/components/Icon'; import React from 'react'; import './index.less'; @@ -7,7 +8,7 @@ const Instruction = (props: { description: string; onClick?: () => void }) => { return ( diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx deleted file mode 100644 index bd67a3fb..00000000 --- a/app/components/Modal.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Modal as AntModal } from 'antd'; -import { ModalProps } from 'antd/lib/modal'; -import React, { Component } from 'react'; - -interface IModalState { - visible: boolean; -} - -interface IModalHandler { - show: (callback?: any) => void; - hide: (callback?: any) => void; -} - -interface IModalProps extends ModalProps { - /** - * use this hook you can get the handler of Modal - * handlerRef => ({ visible, show, hide }) - */ - handlerRef?: (handler: IModalHandler) => void; - children?: any; -} -export default class Modal extends Component { - constructor(props: IModalProps) { - super(props); - this.state = { - visible: false, - }; - } - componentDidMount() { - if (this.props.handlerRef) { - this.props.handlerRef({ - show: this.show, - hide: this.hide, - }); - } - } - - show = (callback?: any) => { - this.setState( - { - visible: true, - }, - () => { - if (callback) { - callback(); - } - }, - ); - }; - - hide = (callback?: any) => { - this.setState( - { - visible: false, - }, - () => { - if (callback) { - callback(); - } - }, - ); - }; - - render() { - return ( - this.state.visible && ( - { - this.hide(); - }} - {...this.props} - > - {this.props.children} - - ) - ); - } -} diff --git a/app/components/NebulaD3/Links.tsx b/app/components/NebulaD3/Links.tsx deleted file mode 100644 index d193a170..00000000 --- a/app/components/NebulaD3/Links.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import * as d3 from 'd3'; -import _ from 'lodash'; -import * as React from 'react'; - -import { IPath } from '#app/utils/interface'; - -interface IProps { - links: any[]; - selectedPaths: any[]; - onUpdateLinks: () => void; - onMouseInLink: (d, event) => void; - onMouseOut: () => void; -} - -export default class Links extends React.Component { - ref: SVGGElement; - - componentDidMount() { - this.linkRender(this.props.links, this.props.selectedPaths); - } - - componentDidUpdate(prevProps) { - const { links } = this.props; - if (links.length < prevProps.links.length) { - const removeLinks = _.differenceBy( - prevProps.links, - links, - (v: any) => v.id, - ); - removeLinks.forEach(removeLink => { - const id = removeLink.uuid; - d3.select('#text-path-' + id).remove(); - d3.select('#text-marker' + id).remove(); - d3.select('#text-marker-id' + id).remove(); - }); - } else { - this.linkRender(this.props.links, this.props.selectedPaths); - } - } - - getNormalWidth = d => { - const { selectedPaths } = this.props; - return selectedPaths.map(path => path.id).includes(d.id) ? 3 : 2; - }; - - linkRender(links: IPath[], selectedPaths: IPath[]) { - const self = this; - const selectPathIds = selectedPaths.map(node => node.id); - d3.select(this.ref) - .selectAll('path') - .data(links) - .classed('active-link', (d: IPath) => selectPathIds.includes(d.id)) - .enter() - .append('svg:path') - .attr('pointer-events', 'visibleStroke') - .attr('class', 'link') - .classed('ring', (d: IPath) => d.source.name === d.target.name) - .style('fill', 'none') - .style('stroke', '#595959') - .style('stroke-width', 2) - .on('mouseover', function(d) { - self.props.onMouseInLink(d, d3.event); - d3.select(this) - .classed('hovered-link', true) - .style('stroke-width', 3); - }) - .on('mouseout', function() { - self.props.onMouseOut(); - d3.select(this) - .classed('hovered-link', false) - .style('stroke-width', self.getNormalWidth); - }) - .attr('id', (d: any) => 'text-path-' + d.uuid); - - d3.select(this.ref) - .selectAll('text') - .data(links) - .enter() - .append('text') - .attr('class', 'text') - .attr('id', (d: any) => 'text-marker-id' + d.uuid) - .append('textPath') - .attr('id', (d: any) => 'text-marker' + d.uuid) - .attr('class', 'textPath'); - - if (this.ref) { - this.props.onUpdateLinks(); - } - } - - render() { - return ( - (this.ref = ref)} /> - ); - } -} diff --git a/app/components/NebulaD3/NodeTexts.tsx b/app/components/NebulaD3/NodeTexts.tsx deleted file mode 100644 index 162e68a3..00000000 --- a/app/components/NebulaD3/NodeTexts.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as d3 from 'd3'; -import _ from 'lodash'; -import * as React from 'react'; - -import { INode } from '#app/utils/interface'; - -interface IProps { - nodes: INode[]; - onUpDataNodeTexts: () => void; -} - -export default class NodeTexts extends React.Component { - ref: SVGGElement; - - componentDidMount() { - this.labelRender(this.props.nodes); - } - - componentDidUpdate(prevProps) { - const { nodes } = this.props; - if (nodes.length < prevProps.nodes.length) { - const removeNodes = _.differenceBy( - prevProps.nodes, - nodes, - (v: any) => v.name, - ); - removeNodes.forEach(removeNode => { - d3.select('#name_' + removeNode.uuid).remove(); - }); - } else { - this.labelRender(this.props.nodes); - } - } - - labelRender(nodes) { - d3.select(this.ref) - .selectAll('.label') - .data(nodes) - .enter() - .append('text') - .attr('class', 'label') - .attr('id', d => 'name_' + d.uuid) - .attr('text-anchor', 'middle'); - - if (this.ref) { - this.props.onUpDataNodeTexts(); - } - } - render() { - return ( - (this.ref = ref)} /> - ); - } -} diff --git a/app/components/NebulaD3/SelectIds.tsx b/app/components/NebulaD3/SelectIds.tsx deleted file mode 100644 index b5315c89..00000000 --- a/app/components/NebulaD3/SelectIds.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import * as d3 from 'd3'; -import * as React from 'react'; - -import { INode, IPath } from '#app/utils/interface'; - -interface IProps { - nodes: INode[]; - links: IPath[]; - selectedPaths: IPath[]; - offsetX: number; - offsetY: number; - scale: number; - onSelectVertexes: (vertexes: INode[]) => void; - onSelectEdges: (vertexes: IPath[]) => void; -} -/** - * Test line and line acrosses - * @method isIntersectedLines - * @param a1 - The start point of the first line - * @param a2 - The end point of the first line - * @param b1 - The start point of the second line - * @param b2 - The end point of the second line - */ - -function isIntersectedLines(a1, a2, b1, b2) { - // b1->b2 向量 与 a1->b1向量的向量积 - const u1 = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x); - // a1->a2向量 与 a1->b1向量的向量积 - const u2 = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x); - // a1->a2向量 与 b1->b2向量的向量积 - const u3 = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); - // u3 == 0时,角度为0或者180 平行或者共线不属于相交 - if (u3 !== 0) { - const ua = u1 / u3; - const ub = u2 / u3; - if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { - return true; - } - } - return false; -} - -export default class SelectIds extends React.Component { - componentDidMount() { - const { nodes, links } = this.props; - if (nodes.length !== 0 || links.length !== 0) { - this.rectRender(nodes, links); - } - } - - componentDidUpdate() { - const { nodes, links } = this.props; - if (nodes.length !== 0 || links.length !== 0) { - this.rectRender(nodes, links); - } - } - - rectRender(nodes: INode[], links: IPath[]) { - const selectStartPosition = { - x: 0, - y: 0, - }; - const rect = d3 - .selectAll('.rect') - .style('stroke', 'gray') - .style('stroke-width', '0.6') - .style('fill', 'transparent') - .style('stroke-opacity', '0.6'); - - d3.select('#output-graph') - .on('mousedown', () => { - // Prohibit right click trigger, conflict with right click menu - if (d3.event.button === 2) { - return; - } - - selectStartPosition.x = d3.event.offsetX; - selectStartPosition.y = d3.event.offsetY; - }) - .on('mousemove', () => { - if (selectStartPosition.x !== 0) { - rect - .attr('x', Math.min(d3.event.offsetX, selectStartPosition.x)) - .attr('y', Math.min(d3.event.offsetY, selectStartPosition.y)) - .attr('width', Math.abs(d3.event.offsetX - selectStartPosition.x)) - .attr('height', Math.abs(d3.event.offsetY - selectStartPosition.y)); - } - }) - .on('mouseup', () => { - // Prohibit right click trigger, conflict with right click menu - if (d3.event.button === 2) { - return; - } - const selectEndPosition = { - x: d3.event.offsetX, - y: d3.event.offsetY, - }; - this.props.onSelectVertexes( - nodes.filter( - node => - !this.isNotSelected(node, selectStartPosition, selectEndPosition), - ), - ); - if ( - selectStartPosition.x !== selectEndPosition.x && - selectStartPosition.y !== selectEndPosition.y - ) { - const _edges = [] as IPath[]; - links.forEach((link: IPath) => { - if ( - !this.isNotSelected( - link.source, - selectStartPosition, - selectEndPosition, - ) && - !this.isNotSelected( - link.target, - selectStartPosition, - selectEndPosition, - ) - ) { - // startPoint and endPoint are in rect - _edges.push(link); - } else if ( - this.isLinkIntersectedWithRect( - link.source, - link.target, - selectStartPosition, - selectEndPosition, - ) - ) { - // no point in rect but line between two point is acrossed with rect - _edges.push(link); - } - }); - this.props.onSelectEdges(_edges); - } else if (d3.event.target.nodeName === 'svg') { - // when click on canvas, clean the select result - this.props.onSelectEdges([]); - } - selectStartPosition.x = 0; - selectStartPosition.y = 0; - rect.attr('width', 0).attr('height', 0); - }); - } - - isNotSelected(nodePoint, selectStartPosition, selectEndPosition) { - const { scale, offsetX, offsetY } = this.props; - const x = nodePoint.x * scale + offsetX; - const y = nodePoint.y * scale + offsetY; - if ( - (x > selectStartPosition.x && x > selectEndPosition.x) || - (x < selectStartPosition.x && x < selectEndPosition.x) || - (y > selectStartPosition.y && y > selectEndPosition.y) || - (y < selectStartPosition.y && y < selectEndPosition.y) - ) { - return true; - } - return false; - } - - isLinkIntersectedWithRect = ( - source, - target, - selectStartPosition, - selectEndPosition, - ) => { - const { scale, offsetX, offsetY } = this.props; - const startPoint = { - x: source.x * scale + offsetX, - y: source.y * scale + offsetY, - }; - const endPoint = { - x: target.x * scale + offsetX, - y: target.y * scale + offsetY, - }; - const r0 = { - x: selectStartPosition.x, - y: selectStartPosition.y, - }; - const r1 = { - x: selectStartPosition.x, - y: selectEndPosition.y, - }; - const r2 = { - x: selectEndPosition.x, - y: selectStartPosition.y, - }; - const r3 = { - x: selectEndPosition.x, - y: selectEndPosition.y, - }; - if (isIntersectedLines(startPoint, endPoint, r0, r1)) { - return true; - } - if (isIntersectedLines(startPoint, endPoint, r1, r2)) { - return true; - } - if (isIntersectedLines(startPoint, endPoint, r2, r3)) { - return true; - } - if (isIntersectedLines(startPoint, endPoint, r3, r0)) { - return true; - } - return false; - }; - - render() { - return ; - } -} diff --git a/app/components/NebulaD3/index.less b/app/components/NebulaD3/index.less deleted file mode 100644 index dd590fc3..00000000 --- a/app/components/NebulaD3/index.less +++ /dev/null @@ -1,56 +0,0 @@ -#output-graph { - text-align: center; - overflow: hidden; - user-select: none; - - .links path { - cursor: default; - } - - .nebula-d3-nodes { - cursor: pointer; - stroke: #fff; - stroke-width: 2.5; - width: 100%; - height: 100%; - - .node { - .circle { - &.active { - stroke: rgba(0, 0, 0, 0.5); - stroke-width: 6; - r: 20; - } - } - } - } - - .labels text { - line-height: 40px; - font-size: 12px; - cursor: pointer; - fill: #333; - } - - .text { - color: #333; - pointer-events: none; - text-anchor: middle; - font-size: 12px; - } -} - -.graph-btn { - float: right; - margin-right: 20px; - - i { - font-size: 16px; - margin-top: 2px; - } -} - - -.cursor-move { - cursor: move; -} diff --git a/app/components/NebulaD3/index.tsx b/app/components/NebulaD3/index.tsx deleted file mode 100644 index b19f55d9..00000000 --- a/app/components/NebulaD3/index.tsx +++ /dev/null @@ -1,575 +0,0 @@ -import * as d3 from 'd3'; -import * as React from 'react'; -import { connect } from 'react-redux'; - -import IconCfg from '#app/components/IconPicker/iconCfg'; -import Menu from '#app/modules/Explore/NebulaGraph/Menu'; -import { IRootState } from '#app/store'; -import { INode, IPath } from '#app/utils/interface'; - -import './index.less'; -import Links from './Links'; -import Labels from './NodeTexts'; -import SelectIds from './SelectIds'; - -const mapState = (state: IRootState) => ({ - offsetX: state.d3Graph.canvasOffsetX, - offsetY: state.d3Graph.canvasOffsetY, - isZoom: state.d3Graph.isZoom, - scale: state.d3Graph.canvasScale, -}); -interface IProps extends ReturnType { - width: number; - height: number; - data: { - vertexes: INode[]; - edges: IPath[]; - }; - showTagFields: string[]; - showEdgeFields: string[]; - selectedNodes: INode[]; - selectedPaths: IPath[]; - onSelectVertexes: (vertexes: INode[]) => void; - onSelectEdges: (edges: IPath[]) => void; - onMouseInNode: (node: INode, event: MouseEvent) => void; - onMouseOut: () => void; - onMouseInLink: (link: IPath, event: MouseEvent) => void; - onDblClickNode: () => void; -} - -class NebulaD3 extends React.Component { - nodeRef: SVGGElement; - circleRef: SVGCircleElement; - canvasBoardRef: SVGCircleElement; - force: any; - svg: any; - node: any; - link: any; - linksText: any; - nodeText: any; - iconText: any; - - componentDidMount() { - this.svg = d3.select('#output-graph'); - const { offsetX, offsetY, scale } = this.props; - this.initMarker(); - d3.select('.nebula-d3-canvas').attr( - 'transform', - `translate(${offsetX},${offsetY}) scale(${scale})`, - ); - } - - initMarker = () => { - const defs = this.svg.append('defs'); - defs - .append('marker') - .attr('id', 'marker') - .attr('markerUnits', 'userSpaceOnUse') - .attr('viewBox', '-20 -10 20 20') - .attr('refX', 20) - .attr('refY', 0) - .attr('orient', 'auto') - .attr('markerWidth', 20) - .attr('markerHeight', 20) - .attr('xoverflow', 'visible') - .append('path') - .attr('d', 'M-10, -5 L 0,0 L -10, 5') - .attr('fill', '#595959') - .attr('stroke', '#595959'); - defs - .append('marker') - .attr('id', 'marker-actived') - .attr('markerUnits', 'userSpaceOnUse') - .attr('viewBox', '-20 -10 20 20') - .attr('refX', 25.5) - .attr('refY', 0) - .attr('orient', 'auto') - .attr('markerWidth', 16) - .attr('markerHeight', 16) - .attr('xoverflow', 'visible') - .append('path') - .attr('d', 'M-16, -8 L 0,0 L -16, 8') - .attr('fill', '#0091FF') - .attr('stroke', '#0091FF') - .attr('stroke-opacity', '0.6') - .attr('fill-opacity', '0.9'); - }; - - componentDidUpdate() { - const { data, selectedNodes } = this.props; - this.handleDeleteNodes(data.vertexes); - this.handleUpdateNodes(data.vertexes, selectedNodes); - this.handleUpdateIcons(data.vertexes); - this.force.on('tick', () => this.tick()); - } - - handleNodeClick = (d: any) => { - const event = d3.event; - const { selectedNodes, onSelectVertexes } = this.props; - if (event.shiftKey) { - const data = selectedNodes.find(n => n.name === d.name) - ? selectedNodes.filter(n => n.name !== d.name) - : [...selectedNodes, d]; - onSelectVertexes(data); - } else { - onSelectVertexes([d]); - } - }; - - handleEdgeClick = (d: any) => { - const event = d3.event; - const { selectedPaths, onSelectEdges } = this.props; - if (event.shiftKey) { - const data = selectedPaths.find(n => n.id === d.id) - ? selectedPaths.filter(n => n.id !== d.id) - : [...selectedPaths, d]; - onSelectEdges(data); - } else { - onSelectEdges([d]); - } - }; - - dragged = d => { - d.fx = d3.event.x; - d.fy = d3.event.y; - d.isFixed = true; - }; - - dragstart = (d: any) => { - if (!d3.event.active) { - this.force.alphaTarget(0.6).restart(); - } - return d; - }; - - dragEnded = () => { - if (!d3.event.active) { - this.force.alphaTarget(0); - } - }; - - tick = () => { - this.link.attr('d', (d: any) => { - if (d.target.name === d.source.name) { - const param = d.size > 1 ? 50 : 30; - const dr = param / d.linknum; - return ( - 'M' + - d.source.x + - ',' + - d.source.y + - 'A' + - dr + - ',' + - dr + - ' 0 1,1 ' + - d.target.x + - ',' + - (d.target.y + 1) - ); - } else if (d.size % 2 !== 0 && d.linknum === 1) { - return ( - 'M ' + - d.source.x + - ' ' + - d.source.y + - ' L ' + - d.target.x + - ' ' + - d.target.y - ); - } - const curve = 3; - const homogeneous = 0.5; - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; - const dr = - (Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous)) / - (curve * homogeneous); - if (d.linknum < 0) { - const dr = - (Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous)) / - (curve * homogeneous); - return ( - 'M' + - d.source.x + - ',' + - d.source.y + - 'A' + - dr + - ',' + - dr + - ' 0 0,0 ' + - d.target.x + - ',' + - d.target.y - ); - } - return ( - 'M' + - d.source.x + - ',' + - d.source.y + - 'A' + - dr + - ',' + - dr + - ' 0 0, 1 ' + - d.target.x + - ',' + - d.target.y - ); - }); - this.node.attr('cx', d => d.x).attr('cy', d => d.y); - this.iconText?.attr('x', d => d.x).attr('y', d => d.y); - - d3.selectAll('.text') - .attr('transform-origin', (d: any) => { - return `${(d.source.x + d.target.x) / 2} ${(d.source.y + d.target.y) / - 2}`; - }) - .attr('rotate', (d: any) => { - if (d.source.x - d.target.x > 0) { - return 180; - } - return 0; - }); - this.linksText - .attr('x', (d: any) => { - return (d.source.x + d.target.x) / 2; - }) - .attr('y', (d: any) => { - return (d.source.y + d.target.y) / 2; - }) - .text((d: any) => { - if (d.source.x - d.target.x > 0) { - return ( - this.edgeName(d) - .join(' & ') - .split('') - .reverse() - .join('') || - d.type - .split('') - .reverse() - .join('') - ); - } - return this.edgeName(d).join(' & ') || d.type; - }); - this.nodeRenderText(); - }; - - handleDeleteNodes(nodes: INode[]) { - const currentNodes = d3.selectAll('.node'); - if (nodes.length === 0) { - currentNodes.remove(); - return; - } else if (currentNodes.size() > nodes.length) { - const ids = nodes.map(i => i.name); - const deleteNodes = currentNodes.filter((data: any) => { - return !ids.includes(data.name); - }); - deleteNodes.remove(); - return; - } - } - - handleUpdateNodes(nodes: INode[], selectNodes: INode[]) { - const selectNodeIds = selectNodes.map(node => node.uuid); - d3.select(this.nodeRef) - .selectAll('circle') - .data(nodes) - .style('fill', (d: INode) => d.color) - .classed('active', (d: INode) => selectNodeIds.includes(d.uuid)) - .attr('id', (d: INode) => `circle-${d.uuid}`) - .enter() - .append('g') - .attr('id', (d: INode) => `node_${d.uuid}`) - .attr('class', 'node') - .append('circle') - .attr('class', 'circle') - .attr('r', 20) - .style('fill', (d: INode) => d.color) // HACK: Color distortion caused by delete node - .on('mouseover', (d: INode) => { - if (this.props.onMouseInNode) { - this.props.onMouseInNode(d, d3.event); - } - }) - .on('mouseout', () => { - if (this.props.onMouseOut) { - this.props.onMouseOut(); - } - }); - - d3.select(this.nodeRef) - .selectAll('g') - .data(nodes) - .classed('active-node', (d: INode) => selectNodeIds.includes(d.uuid)); - - this.node = d3 - .selectAll('.circle') - .on('click', this.handleNodeClick) - .on('dblclick', this.props.onDblClickNode) - .call( - d3 - .drag() - .on('start', d => this.dragstart(d)) - .on('drag', d => this.dragged(d)) - .on('end', this.dragEnded) as any, - ); - } - - handleUpdateIcons = (nodes: INode[]) => { - nodes.forEach(a => { - if ( - a.icon && - !d3 - .select('#node_' + a.uuid) - .select('.icon') - .node() - ) { - d3.selectAll('#node_' + a.uuid) - .append('text') - .attr('class', 'icon') - .attr('text-anchor', 'middle') - .attr('dominant-baseline', 'central') - .attr('stroke', 'black') - .attr('stroke-width', '0.00001%') - .attr('font-family', 'nebula-cloud-icon') - .attr('x', (d: any) => d.x) - .attr('y', (d: any) => d.y) - .attr('id', (d: any) => d.uuid) - .attr('font-size', '20px') - .text(IconCfg.filter(icon => icon.type === a.icon)[0].content); - } - }); - if (d3.selectAll('.icon').node()) { - this.iconText = d3 - .selectAll('.icon') - .on('click', this.handleNodeClick) - .on('dblclick', this.props.onDblClickNode) - .call( - d3 - .drag() - .on('start', d => this.dragstart(d)) - .on('drag', d => this.dragged(d)) - .on('end', this.dragEnded) as any, - ); - } - }; - - handleUpdataNodeTexts = () => { - if (this.force) { - this.nodeText = d3 - .selectAll('.label') - .on('click', this.handleNodeClick) - .on('mouseover', () => { - if (this.props.onMouseOut) { - this.props.onMouseOut(); - } - }) - .call( - d3 - .drag() - .on('start', d => this.dragstart(d)) - .on('drag', d => this.dragged(d)) - .on('end', this.dragEnded) as any, - ); - } - }; - - handleUpdateLinks = () => { - if (this.force) { - this.link = d3.selectAll('.link').on('click', this.handleEdgeClick); - d3.selectAll('.link:not(.active-link):not(.hovered-link)') - .attr('marker-end', 'url(#marker)') - .style('stroke', '#595959') - .style('stroke-width', 2); - d3.selectAll('.link.active-link') - .attr('marker-end', 'url(#marker-actived)') - .style('stroke', '#0091ff') - .style('stroke-width', 3); - this.linksText = d3 - .selectAll('.text') - .selectAll('.textPath') - .attr(':href', (d: any) => '#text-path-' + d.uuid) - .attr('startOffset', '50%'); - } - }; - - // compute to get (x,y ) of the nodes by d3-force: https://github.com/d3/d3-force/blob/v1.2.1/README.md#d3-force - // it will change the data.edges and data.vertexes passed in - computeDataByD3Force() { - const { data } = this.props; - const linkForce = d3 - .forceLink(data.edges) - .id((d: any) => { - return d.name; - }) - .distance(210); - if (!this.force) { - this.force = d3 - .forceSimulation() - .force('charge', d3.forceManyBody().strength(-20)) - .force( - 'collide', - d3 - .forceCollide() - .radius(35) - .iterations(2), - ); - } - this.force - .nodes(data.vertexes) - .force('link', linkForce) - .restart(); - } - - isIncludeField = (node, field) => { - let isInclude = false; - if (node.nodeProp && node.nodeProp.properties) { - const properties = node.nodeProp.properties; - isInclude = Object.keys(properties).some(v => { - const valueObj = properties[v]; - return Object.keys(valueObj).some( - nodeField => field === v + '.' + nodeField, - ); - }); - } - return isInclude; - }; - - edgeName = edge => { - const { showEdgeFields } = this.props; - const edgeText: any = []; - if (showEdgeFields.includes(`${edge.type}.type`)) { - if (showEdgeFields.includes(`${edge.type}._rank`)) { - edgeText.push(`${edge.type}@${edge.rank}`); - } else { - edgeText.push(edge.type); - } - } - - showEdgeFields.forEach(field => { - Object.keys(edge.edgeProp.properties).forEach(property => { - if (field === `${edge.type}.${property}`) { - edgeText.push(edge.edgeProp.properties[property]); - } - }); - }); - return edgeText; - }; - - targetName = (node, field) => { - let nodeText = ''; - const properties = node.nodeProp.properties; - Object.keys(properties).some(property => { - const value = properties[property]; - return Object.keys(value).some(nodeField => { - const fieldStr = property + '.' + nodeField; - if (fieldStr === field) { - nodeText = `${value[nodeField]}`; - return true; - } - }); - }); - return nodeText; - }; - - nodeRenderText() { - const { showTagFields, data } = this.props; - d3.selectAll('tspan').remove(); - data.vertexes.forEach((node: any) => { - let line = 1; - if (node.nodeProp) { - showTagFields.forEach(field => { - if (this.isIncludeField(node, field)) { - line++; - d3.select('#name_' + node.uuid) - .append('tspan') - .attr('x', (d: any) => d.x) - .attr('y', (d: any) => d.y - 20 + 20 * line) - .attr('dy', '1em') - .text(d => this.targetName(d, field)); - } - }); - } - }); - } - - iconRenderText() { - const { data } = this.props; - data.vertexes.forEach((node: any) => { - if (node.nodeProp) { - d3.select('#icon_' + node.uuid) - .append('tspan') - .attr('x', (d: any) => d.x) - .attr('y', (d: any) => d.y) - .attr('dy', '1em'); - } - }); - } - - render() { - this.computeDataByD3Force(); - const { - width, - height, - data, - onMouseInLink, - onMouseOut, - offsetX, - offsetY, - scale, - selectedPaths, - onSelectVertexes, - onSelectEdges, - isZoom, - } = this.props; - return ( - <> - - (this.canvasBoardRef = ref)} - > - - (this.nodeRef = ref)} - /> - - - - - - - ); - } -} - -export default connect(mapState)(NebulaD3); diff --git a/app/components/OutputBox/Export.less b/app/components/OutputBox/Export.less deleted file mode 100644 index e1960008..00000000 --- a/app/components/OutputBox/Export.less +++ /dev/null @@ -1,34 +0,0 @@ -.export-modal { - .ant-radio-group { - width: 100%; - text-align: center; - margin: 10px 0; - - & > label { - width: 50%; - } - } - - .form { - text-align: center; - - & > p:first-child { - font-weight: bold; - margin: 5px 0 10px; - } - - .select-component { - text-align: center; - margin: 5px 0; - - input { - width: 150px; - } - } - } - - .modal-footer { - text-align: center; - margin-top: 25px; - } -} diff --git a/app/components/OutputBox/Export.tsx b/app/components/OutputBox/Export.tsx deleted file mode 100644 index 36da86c4..00000000 --- a/app/components/OutputBox/Export.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Button, Form, Input, Radio, Select } from 'antd'; -import { FormComponentProps } from 'antd/lib/form/Form'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; - -import { IDispatch } from '#app/store'; -import { trackEvent } from '#app/utils/stat'; - -import './Export.less'; - -const Option = Select.Option; -interface IProps - extends ReturnType, - FormComponentProps, - RouteComponentProps { - data: any; -} - -const mapState = () => ({}); - -const mapDispatch = (dispatch: IDispatch) => ({ - updatePreloadData: data => - dispatch.explore.update({ - preloadData: data, - }), -}); - -class Export extends React.Component { - handleExport = () => { - const { getFieldsValue } = this.props.form; - const { type, vertexId, srcId, dstId, edgeType, rank } = getFieldsValue(); - const { tables } = this.props.data; - const vertexes = - type === 'vertex' - ? tables - .map(vertex => { - if (vertex.type === 'vertex') { - return vertex.vid; - } else { - return vertex[vertexId].toString(); - } - }) - .filter(vertexId => vertexId !== '') - : tables - .map(edge => [edge[srcId], edge[dstId]]) - .flat() - .filter(id => id !== ''); - const edges = - type === 'edge' - ? tables - .map(edge => ({ - srcId: edge[srcId], - dstId: edge[dstId], - rank: rank !== '' && rank !== undefined ? edge[rank] : 0, - edgeType, - })) - .filter(edge => edge.srcId !== '' && edge.dstId !== '') - : []; - this.props.updatePreloadData({ - vertexes, - edges, - }); - this.props.history.push('/explore'); - trackEvent('navigation', 'view_explore', 'from_console_btn'); - }; - - render() { - const { headers } = this.props.data; - const { getFieldDecorator, getFieldsValue } = this.props.form; - const { - type = 'vertex', - vertexId, - srcId, - dstId, - edgeType, - } = getFieldsValue(); - const disabled = - (type === 'vertex' && !vertexId) || - (type === 'edge' && (!srcId || !dstId || !edgeType)); - const layout = { - labelCol: { span: 10 }, - wrapperCol: { span: 8 }, - }; - return ( -
-
- {getFieldDecorator('type', { - initialValue: 'vertex', - })( - - - {intl.get('import.vertexText')} - - - {intl.get('common.edge')} - - , - )} - {type === 'vertex' && ( - <> -

{intl.get('console.exportVertex')}

- - {getFieldDecorator('vertexId', { - rules: [ - { - required: true, - }, - ], - })( - , - )} - - - )} - {type === 'edge' && ( - <> -

{intl.get('console.exportEdge')}

- - {getFieldDecorator('edgeType', { - rules: [ - { - required: true, - }, - ], - })()} - - - {getFieldDecorator('srcId', { - rules: [ - { - required: true, - }, - ], - })( - , - )} - - - {getFieldDecorator('dstId', { - rules: [ - { - required: true, - }, - ], - })( - , - )} - - - {getFieldDecorator('rank')( - , - )} - - - )} - -
- -
-
- ); - } -} - -export default connect( - mapState, - mapDispatch, -)(withRouter(Form.create()(Export))); diff --git a/app/components/OutputBox/Graphviz.tsx b/app/components/OutputBox/Graphviz.tsx deleted file mode 100644 index f0523111..00000000 --- a/app/components/OutputBox/Graphviz.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { graphviz, GraphvizOptions } from 'd3-graphviz'; -import _ from 'lodash'; -import * as React from 'react'; - -import './Graphviz.less'; - -interface IProps { - graph: string; -} - -export default class Graphviz extends React.Component { - ref: HTMLDivElement; - - componentDidMount() { - this.renderFlowChart(this.props.graph); - } - - componentDidUpdate() { - this.renderFlowChart(this.props.graph); - } - - renderFlowChart(graph) { - const defaultOptions: GraphvizOptions = { - fit: true, - width: '100%', - zoom: false, - }; - graphviz('#graph') - .options({ - ...defaultOptions, - }) - .renderDot(graph); - } - - render() { - return
(this.ref = ref)} />; - } -} diff --git a/app/components/OutputBox/index.less b/app/components/OutputBox/index.less deleted file mode 100644 index ce88d9a6..00000000 --- a/app/components/OutputBox/index.less +++ /dev/null @@ -1,85 +0,0 @@ -.output-box { - min-height: 560px; - max-height: 100%; - overflow: hidden; - background: #fff; - position: relative; - display: flex; - flex-direction: column; - - .output-value { - padding: 0 12px; - width: 100%; - cursor: pointer; - font-size: 16px; - white-space: nowrap; - text-overflow: ellipsis; - line-height: 42px; - border: none; - - .gql { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .ant-alert-success .gql { - color: #52c41a; - } - - .ant-alert-error .gql { - color: #ff4d4f; - } - - .ant-table { - overflow: auto; - } - - .tab-container { - border-top: 1px solid #ddd; - flex: 1; - overflow: auto; - padding: 20px; - margin-bottom: 10px; - - .operation { - display: flex; - justify-content: flex-end; - margin-bottom: 10px; - - a { - color: #fff; - background-color: #1890ff; - border-color: #1890ff; - } - - .btn-link { - background: transparent; - } - } - - table { - background: #f8f8fa; - width: 100%; - - thead > tr > th { - background: #ddd !important; - } - - tr:nth-child(2n) { - background: #fff; - } - } - } - - .output-footer { - padding: 17px 15px; - width: 100%; - height: 60px; - border-top: 1px solid #d9d9d9; - font-family: PingFangSC-Regular, serif; - font-size: 16px; - color: #595959; - letter-spacing: 1.48px; - } -} diff --git a/app/components/OutputBox/index.tsx b/app/components/OutputBox/index.tsx deleted file mode 100644 index 3eb26b5d..00000000 --- a/app/components/OutputBox/index.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Alert, Button, Icon, Table, Tabs } from 'antd'; -import { BigNumber } from 'bignumber.js'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; - -import { Modal, OutputCsv } from '#app/components'; -import { IDispatch, IRootState } from '#app/store'; -import { parseSubGraph } from '#app/utils/parseData'; -import { trackEvent } from '#app/utils/stat'; - -import Export from './Export'; -import Graphviz from './Graphviz'; -import './index.less'; - -interface IProps - extends ReturnType, - ReturnType, - RouteComponentProps { - value: string; - result: any; - onHistoryItem: (value: string) => void; -} - -const mapState = (state: IRootState) => ({ - spaceVidType: state.nebula.spaceVidType, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - updatePreloadData: data => - dispatch.explore.update({ - preloadData: data, - }), -}); -class OutputBox extends React.Component { - importNodesHandler; - outputClass = (code: any) => { - if (code !== undefined) { - if (code === 0) { - return 'success'; - } - return 'error'; - } - return 'info'; - }; - - handleExplore = () => { - const { result = {} } = this.props; - if ( - result.data.tables.filter( - item => - item._verticesParsedList || - item._edgesParsedList || - item._pathsParsedList, - ).length > 0 - ) { - this.parseToGraph(); - } else { - if (this.importNodesHandler) { - this.importNodesHandler.show(); - } - } - }; - - parseToGraph = () => { - const { - result: { - data: { tables }, - }, - } = this.props; - const { spaceVidType } = this.props; - const { vertexes, edges } = parseSubGraph(tables, spaceVidType); - this.props.updatePreloadData({ - vertexes: _.uniq(vertexes), - edges: _.uniqBy(edges, (e: any) => e.id), - }); - this.props.history.push('/explore'); - trackEvent('navigation', 'view_explore', 'from_console_btn'); - }; - - handleTabChange = async key => { - trackEvent('console', `change_tab_${key}`); - }; - render() { - const { value, result = {} } = this.props; - let columns = [] as any; - let showSubgraphs = false; - let dataSource = [] as any; - if (result.data && result.data.tables.length > 0) { - dataSource = result.data.tables; - } else if (result.data?.localParams) { - const params = {}; - { - Object.entries(result.data?.localParams).forEach( - ([k, v]) => (params[k] = JSON.stringify(v)), - ); - } - dataSource = [{ ...params }]; - } - if (result.code === 0) { - if (result.data && result.data.headers.length > 0) { - columns = result.data.headers.map(column => { - return { - title: column, - dataIndex: column, - sorter: (r1, r2) => { - const v1 = r1[column]; - const v2 = r2[column]; - return v1 === v2 ? 0 : v1 > v2 ? 1 : -1; - }, - sortDirections: ['descend', 'ascend'], - render: value => { - if (typeof value === 'boolean') { - return value.toString(); - } else if ( - typeof value === 'number' || - BigNumber.isBigNumber(value) - ) { - return value.toString(); - } - return value; - }, - }; - }); - showSubgraphs = - dataSource.filter( - item => - item._verticesParsedList || - item._edgesParsedList || - item._pathsParsedList, - ).length > 0; - } else if (result.data?.localParams) { - columns = Object.keys(result.data?.localParams).map(column => { - return { - title: column, - dataIndex: column, - render: value => { - if (typeof value === 'boolean') { - return value.toString(); - } else if ( - typeof value === 'number' || - BigNumber.isBigNumber(value) - ) { - return value.toString(); - } - return value; - }, - }; - }); - } - } - return ( -
- this.props.onHistoryItem(value)}> - $ {value} -

- } - className="output-value" - type={this.outputClass(result.code)} - /> -
- - {result.code === 0 && ( - - - {intl.get('common.table')} - - } - key="table" - > -
- - -
-
- `${intl.get('common.total')} ${dataSource.length}`, - }} - rowKey={(_, index) => index.toString()} - /> - - )} - {result.code === 0 && result.data.headers[0] === 'format' && ( - - - {intl.get('common.graph')} - - } - key="graph" - > - {} - - )} - {result.code !== 0 && ( - - - {intl.get('common.log')} - - } - key="log" - > - {result.message} - - )} - - - {result.code === 0 && result.data.timeCost !== undefined && ( -
- - {`${intl.get('console.execTime')} ${result.data.timeCost / - 1000000} (s)`} - -
- )} - (this.importNodesHandler = handler)} - footer={null} - width="650px" - > - - - - ); - } -} - -export default connect(mapState, mapDispatch)(withRouter(OutputBox)); diff --git a/app/components/OutputCsv/index.tsx b/app/components/OutputCsv/index.tsx deleted file mode 100644 index c2ca6d6f..00000000 --- a/app/components/OutputCsv/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; - -interface IProps { - tableData?: { - headers: any[]; - tables: any[]; - }; -} - -export default class OutputCsv extends React.PureComponent { - getCsvDownloadUrl = () => { - const { tableData } = this.props; - if (!tableData) { - return ''; - } - const { headers = [], tables = [] } = tableData; - const csv = [ - headers, - ...tables.map(values => headers.map(field => values[field])), - ] - .map(row => - // HACK: waiting for use case if there need to check int or string - row.map(value => `"${value.toString().replace(/"/g, '""')}"`).join(','), - ) - .join('\n'); - if (!csv) { - return ''; - } - - const _utf = '\uFEFF'; - if (window.Blob && window.URL && window.URL.createObjectURL) { - const csvBlob = new Blob([_utf + csv], { - type: 'text/csv', - }); - return URL.createObjectURL(csvBlob); - } - return ( - 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csv) - ); - }; - - render() { - const url = this.getCsvDownloadUrl(); - - return ( - url && ( - - {intl.get('common.output')} - - ) - ); - } -} diff --git a/app/components/VertexStyleSet/DisplayBtn.tsx b/app/components/VertexStyleSet/DisplayBtn.tsx deleted file mode 100644 index ab57dd44..00000000 --- a/app/components/VertexStyleSet/DisplayBtn.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import classnames from 'classnames'; -import _ from 'lodash'; -import React from 'react'; - -import Icon from '#app/components/Icon'; - -interface IProps { - icon?: string; - color: string; -} - -class DisplayBtn extends React.PureComponent { - render() { - const { icon, color } = this.props; - return ( -
-
-
- {icon && } -
-
-
- ); - } -} - -export default DisplayBtn; diff --git a/app/components/VertexStyleSet/StyleSetTabs.tsx b/app/components/VertexStyleSet/StyleSetTabs.tsx deleted file mode 100644 index 6028e746..00000000 --- a/app/components/VertexStyleSet/StyleSetTabs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Tabs } from 'antd'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; - -import ColorPicker from '#app/components/ColorPicker'; -import IconPicker from '#app/components/IconPicker'; -interface IIcon { - type: string; - content: string; -} - -interface IProps { - handleChangeColorComplete: (color: string) => void; - handleChangeIconComplete: (icon: IIcon) => void; -} - -class StyleSetTabs extends React.PureComponent { - render() { - const { handleChangeColorComplete, handleChangeIconComplete } = this.props; - return ( - - - - - - - - - ); - } -} - -export default StyleSetTabs; diff --git a/app/components/VertexStyleSet/index.tsx b/app/components/VertexStyleSet/index.tsx deleted file mode 100644 index c0bfde66..00000000 --- a/app/components/VertexStyleSet/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Popover } from 'antd'; -import _ from 'lodash'; -import React from 'react'; - -import DisplayBtn from './DisplayBtn'; -import StyleSetTabs from './StyleSetTabs'; -interface IIcon { - type: string; - content: string; -} - -interface IProps { - icon?: string; - color: string; - handleChangeColorComplete: (color: string) => void; - handleChangeIconComplete: (icon: IIcon) => void; -} - -class VertexStyleSet extends React.PureComponent { - render() { - const { - icon, - color, - handleChangeColorComplete, - handleChangeIconComplete, - } = this.props; - return ( - - } - > -
- -
-
- ); - } -} - -export default VertexStyleSet; diff --git a/app/components/index.ts b/app/components/index.ts deleted file mode 100644 index a138b452..00000000 --- a/app/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as CodeMirror } from './CodeMirror'; -export { default as OutputBox } from './OutputBox'; -export { default as Modal } from './Modal'; -export { default as NebulaD3 } from './NebulaD3'; -export { default as OutputCsv } from './OutputCsv'; -export { default as Instruction } from './Instruction'; diff --git a/app/config/explore.ts b/app/config/explore.ts index cae94253..b52f615d 100644 --- a/app/config/explore.ts +++ b/app/config/explore.ts @@ -1,57 +1,12 @@ +import { NodeObject } from '@vesoft-inc/force-graph'; import BigNumber from 'bignumber.js'; import JSONBigint from 'json-bigint'; -import json2csv from 'json2csv'; +import { remove } from 'lodash'; +export const LINE_LENGTH = 150; +export const FONT_SIZE = 10; +export const NODE_SIZE = 18; +export const NODE_AREA = NODE_SIZE * NODE_SIZE; -import { INode, IPath } from '#app/utils/interface'; - -export const MIN_SCALE = 0.3; -export const MAX_SCALE = 1; - -export const HOT_KEYS = intl => [ - { - operation: `Shift + 'Enter'`, - desc: intl.get('explore.expand'), - }, - { - operation: `Shift + '-'`, - desc: intl.get('common.zoomOut'), - }, - { - operation: `Shift + '+'`, - desc: intl.get('common.zoomIn'), - }, - { - operation: `Shift + 'l'`, - desc: intl.get('common.show'), - }, - { - operation: `Shift + 'z'`, - desc: intl.get('common.rollback'), - }, - { - operation: intl.get('common.selected') + ` + Shift + 'del'`, - desc: intl.get('common.delete'), - }, -]; - -export const GRAPH_ALOGORITHM = intl => [ - { - label: intl.get('explore.allPath'), - value: 'ALL', - }, - { - label: intl.get('explore.shortestPath'), - value: 'SHORTEST', - }, - { - label: intl.get('explore.noLoopPath'), - value: 'NOLOOP', - }, -]; - -export const DEFAULT_COLOR_PICKER = '#5CDBD3'; -export const DEFAULT_COLOR_MIX = - 'linear-gradient(225deg, #32C5FF 0%, #B620E0 51%, #F7B500 100%)'; export const COLOR_PICK_LIST = [ '#B93431', '#B95C31', @@ -152,38 +107,8 @@ export const flattenData = data => { return { result, fieldData }; }; -export const downloadCSVFiles = ({ headers, tables, title }) => { - try { - const result = json2csv.parse(tables, { - fields: headers, - }); - // Determine browser type - if ( - (navigator.userAgent.indexOf('compatible') > -1 && - navigator.userAgent.indexOf('MSIE') > -1) || - navigator.userAgent.indexOf('Edge') > -1 - ) { - // IE10 or Edge browsers - const BOM = '\uFEFF'; - const csvData = new Blob([BOM + result], { type: 'text/csv' }); - navigator.msSaveBlob(csvData, `test.csv`); - } else { - // Non-Internet Explorer - // Use the download property of the A tag to implement the download function - const link = document.createElement('a'); - link.href = - 'data:text/csv;charset=utf-8,\uFEFF' + encodeURIComponent(result); - link.download = `${title}.csv`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - } catch (err) { - alert(err); - } -}; -export const parseData = (data: INode[] | IPath[], type: 'vertex' | 'edge') => { +export const parseData = (data, type: 'vertex' | 'edge') => { const fields = type === 'vertex' ? ['vid', 'attributes'] @@ -191,17 +116,16 @@ export const parseData = (data: INode[] | IPath[], type: 'vertex' | 'edge') => { const tables: any = []; data.forEach((item: any) => { const _result = {} as any; - const properties = - type === 'vertex' ? item.nodeProp.properties : item.edgeProp.properties; + const properties = item.properties; const { result } = flattenData(properties) as any; if (type === 'vertex') { - _result.vid = item.name; + _result.vid = item.id; _result.attributes = JSONBigint.stringify(result); tables.push(_result); } else if (type === 'edge') { - _result.type = item.type; - _result.srcId = item.source.name; - _result.dstId = item.target.name; + _result.type = item.edgeType; + _result.srcId = item.source; + _result.dstId = item.target; _result.rank = item.rank; _result.attributes = JSONBigint.stringify(result); tables.push(_result); @@ -210,18 +134,78 @@ export const parseData = (data: INode[] | IPath[], type: 'vertex' | 'edge') => { return { tables, headers: fields }; }; -export const exportDataToCSV = ( - data: INode[] | IPath[], - type: 'vertex' | 'edge', -) => { - const { headers, tables } = parseData(data, type); - downloadCSVFiles({ headers, tables, title: type }); + +export const updateTagMap = (tagMap, vertexes) => { + Object.keys(tagMap).forEach(tag => { + const colorGroup = tagMap[tag]; + colorGroup.forEach(colorMap => { + colorMap.countIds = []; + }); + }); + vertexes.forEach(vertex => { + const { color, tags = [], id } = vertex; + const group = tags.sort().join('-'); + const colorMap = tagMap[group]; + if (colorMap) { + const hasColor = colorMap.some(item => { + if (item.color === color && !item.countIds.includes(id)) { + item.countIds.push(String(id)); + return true; + } + }); + if (!hasColor) { + colorMap.push({ + color, + countIds: [String(id)] + }); + } + } else { + tagMap[group] = [{ + color, + countIds: [String(id)] + }]; + } + }); + // remove color without data, but need to remain one + Object.keys(tagMap).forEach(tag => { + const colorGroup = tagMap[tag]; + const noDataList = colorGroup.filter(item => item.countIds.length === 0).map(item => item.color); + const removeList = colorGroup.length === noDataList.length ? noDataList.slice(1) : noDataList; + remove(tagMap[tag], (item: any) => removeList.includes(item.color)); + }); + return { ...tagMap }; }; -export const DEFAULT_EXPLORE_RULES = { - edgeTypes: [], - edgeDirection: 'outgoing', - stepsType: 'single', - step: 1, - vertexStyle: 'colorGroupByTag', - quantityLimit: 100, + +export const updateEdgeMap = (edgeMap, edges) => { + Object.keys(edgeMap).forEach(item => { + edgeMap[item] = { + countIds: [] + }; + }); + edges.forEach(edge => { + const { edgeType, id } = edge; + edgeMap[edgeType].countIds = [...edgeMap[edgeType].countIds, id]; + }); + return { ...edgeMap }; }; +export const makeRoundPosition = (data: NodeObject[] | Set, center: { x: number; y: number }) => { + const nodes = [...data]; + const length = nodes.length; + const radius = Math.min(Math.max((length * NODE_SIZE) / 2, LINE_LENGTH), window.innerHeight / 2); + // when nodes.length>50 use sphere layout + if (nodes.length < 50) { + nodes.forEach((node, index) => { + const angle = (index / length) * Math.PI * 2; + node.x = radius * Math.sin(angle) + center.x; + node.y = radius * Math.cos(angle) + center.y; + }); + } else { + nodes.forEach((node) => { + const angle = Math.random() * Math.PI * 2; + const r = radius * Math.random(); + node.x = r * Math.sin(angle) + center.x; + node.y = r * Math.cos(angle) + center.y; + }); + } + +}; \ No newline at end of file diff --git a/app/config/locale/en-US.json b/app/config/locale/en-US.json index 6da45a3e..841f256c 100644 --- a/app/config/locale/en-US.json +++ b/app/config/locale/en-US.json @@ -2,11 +2,9 @@ "common": { "requestError": "Request Error", "currentSpace": "Current Graph Space", - "languageSelect": "Language", "seeTheHistory": "History", "table": "Table", "log": "Log", - "record": "Record", "sorryNGQLCannotBeEmpty": "Sorry, nGQL cannot be empty", "disablesUseToSwitchSpace": "Switching space from console is not allowed by current role", "NGQLHistoryList": "nGQL History", @@ -14,34 +12,21 @@ "empty": "Clear", "run": "Run", "console": "Console", - "explore": "Explore", "ok": "OK", "success": "Success", "fail": "Fail", - "noData": "There is no data", "cancel": "Cancel", "confirm": "Confirm", "import": "Import", - "help": "Help", - "use": "Use Manual", - "release": "New Version", - "nebula": "Nebula", - "setting": "Setting", - "feedback": "Feedback", - "forum": "Help Forum", - "forumLink": "https://discuss.nebula-graph.io/", "ask": "Are you sure to proceed?", - "output": "Export CSV File", - "openInExplore": "Open In Explore", + "openInExplore": "Open In Explorer", "schema": "Schema", "create": "Create", - "serialNumber": "No.", "name": "Name", "operation": "Operations", "delete": "Delete", - "optionalParameters": "Optional Parameters", + "optional": "Optional", "exportNGQL": "View nGQL", - "field": "Field", "relatedProperties": "Related Properties", "type": "Type", "edit": "Edit", @@ -56,41 +41,33 @@ "tag": "Tag", "edge": "Edge Type", "index": "Index", - "list": "List", "yes": "Yes", "no": "No", "graph": "Graph", - "description": "Description", - "zoomOut": "Zoom Out", - "zoomIn": "Zoom In", - "move": "Move", - "rollback": "Rollback", - "unlock": "Unlock", - "lock": "Lock", - "moreSuggestion": "More Suggestions", - "algorithm": "Algorithm", - "viewDocs": "View Docs", - "hotKeys": "Shortcut Keys", - "show": "Show", - "selected": "Selected", - "search": "Search", "color": "Color", - "icon": "Icon", - "copy": "Copy", - "copySuccess": "Copied to clipboard", - "expansionConditions": "Expansion conditions", "total": "Total", - "exportSelectVertexes": "Export Vertexes to CSV", - "exportSelectEdges": "Export Edges to CSV", - "noSelectedData": "No data currently selected", - "namePlaceholder": "Please enter a search name", - "comment": "Comment" + "namePlaceholder": "Please enter the {name} name", + "comment": "Comment", + "space": "Space", + "version": "Version", + "statistics": "Statistics" }, - "NGQLOutput": { - "success": "Execution successful!" + "doc": { + "welcome": "Welcome to", + "functionIntro": "Functions introduction", + "schemaIntro": "You can use the Schema page to operate graph spaces in Nebula Graph", + "importIntro": "You can use the Import page to batch import vertex and edge data into Nebula Graph for graph exploration and data analysis.", + "consoleIntro": "When data is imported, you can use the Console page to query graph data.", + "learningDoc": "Learning Docs", + "getStarted": "Getting started with Nebula Studio", + "getStartedTip": "What's Nebula Studio", + "useGuide": "Documents of Nebula Studio", + "useGuideTip": "Learn how to use Nebula Studio", + "ngqlIntro": "Nebula Graph Query Language (nGQL)", + "ngqlIntroTip": "nGQL is a declarative graph query language for Nebula Graph. It allows expressive and efficient graph patterns. ", + "start": "Get Started" }, "warning": { - "configServer": "Please configure the nebula server", "connectError": "Connection refused, please configure server again" }, "configServer": { @@ -99,247 +76,169 @@ "username": "Username", "password": "Password", "success": "succeed", - "fail": "Failed", - "clear": "Clear Connection", - "title": "Configure Server" + "clear": "Log out", + "title": "Connect to Nebula Graph" }, "formRules": { "hostRequired": "Host Required", "usernameRequired": "Username Required", "passwordRequired": "Password Required", - "nodeIdError": "The format is invalid, should be one node per line, split by \\n like: \nid1\nid2\nid3", - "idRequired": "The id field is mandatory", "positiveIntegerRequired": "Please enter a non-negative integer", "nameValidate": "The name must start with a letter, and it only supports English letters, numbers and underscores", "nameRequired": "Please enter the name", "numberRequired": "Please enter a positive integer", "replicaLimit": "Replica factor must not exceed the number of your current online machines({number})", - "propertyRequired": "Please enter the property name", - "defaultRequired": "Please enter the default value", "ttlRequired": "Please select the corresponding property, and the data type of the property must be integer or timestamp", "ttlDurationRequired": "Please enter the time (in seconds)", - "dataTypeRequired": "Please select the data type", - "fixedStringLength": "Fixed String length must be a positive integer" + "dataTypeRequired": "Please select the data type" }, "console": { - "cost": "Cost", "execTime": "Execution Time", "exportVertex": "Please choose the column representing vertex IDs from the table", "exportEdge": "Please choose the columns representing source vertex ID, destination vertex ID, and rank of an edge", - "showSubgraphs": "View Subgraphs", "deleteHistory": "Clear History", - "parameterDisplay": "Custom Parameters Display" + "cypherParam": "Cypher Parameter", + "favorites": "Favorites", + "addToFavorites": "Add to favorites", + "unfavorite": "Unfavorite", + "clearFavorites": "Clear Favorites", + "graphviz": "Graphviz", + "selectSpace": "Please select Graph Space" }, "explore": { - "clear": "Clear", - "clearTip": "Are you sure to proceed the cleanup of the renderred graph view?", - "startWithVertices": "Start with Vertices", - "addConfirm": "Add", - "expand": "Expand", - "unExpand": "Undo Expand", - "undo": "Undo", - "deleteSelectNodes": "Remove Selected Nodes", - "fileImport": "Import File", - "sampleImport": "Import Sample", - "importPlaceholder": "Enter VIDs or other data for VID generation, one data per line, and split them by pressing the Enter key. Here is an example:\nstring1\nstring2\nstring3", - "outgoing": "Outgoing", - "incoming": "Incoming", - "bidirect": "Bidirect", - "filter": "Custom filter conditions", - "operator": "Operator", - "value": "Value", - "selectSpace": "Please select the space", - "selectReminder": "The selection of space will cleanup the renderred graph view, are you sure to proceed?", - "zoom": "Zoom", - "showTags": "Show Tags", - "showEdges": "Show Edges", - "confirm": "Confirm", - "vertexStyle": "Vertex Color/Icon", - "quantityLimit": "Query Limit", - "colorGroupByTag": "Group by vertex tag", - "noVertexPrompt": "No vertices on the board. ", - "search": "Start graph exploration", - "queryById": "Query by VID", - "queryByIndex": "Query by Index", - "queryByCustom": "Custom Query", - "idToBeQueried": "Specify Vertex ID", - "idPretreatment": "Pre-process Vertex IDs", - "indexQueryPrompt_prefix": "In the ", - "indexQueryPrompt_suffix": " space, no tag indexes are found, so query by index is not available.", - "indexQueryPrompt2": "Please create indexes on the tags. Here is an example:", - "runCodeInConsole": "Execute queries in the console", - "indexLink": "For more information about index, see its ", - "documentIntroduction": "documentation", - "selectIndex": "Select a Index", - "paramFilter": "Use Index", - "relationship": "Logical Operator", - "operationConfirm": "This Delete operation will clear the following fields. Are you sure you want to continue the operation?", - "quiry": "Query", - "customQueryDescription": "Enter the statements in the console. When the results are returned, click the Open in Explore button as shown in the preceding figure to explore the graph.", - "openInConsole": "Go to Console", - "insertMethodSelect": "How do you like to render the new data with the existing graph view, incrementally or wipe the graph view first?", - "incrementalInsertion": "Incrementally Render", - "insertAfterClear": "Wipe and Render", - "emptyIndex": "No Index", - "indexConditionDescription": "To use a composite index for a query, we should either filter all fields or the left matching contiguous fields in sequence. That is, the first field is mandatory and skipping field is not allowed", - "timestampInput": "Only numbers are supported for the timestamp field", - "documentIntroductionUrl": "https://docs.nebula-graph.io/2.6.1/3.ngql-guide/14.native-index-statements/", - "customQueryUrl": "https://cloud-cdn.nebula-graph.com.cn/studio-resource/go-to-explore_en.png", - "pretreatmentExplaination": "Hash can pre-process data of the bool, double, int, or string type to generate VIDs, but UUID can pre-process data of the string type only. To generate VIDs by pre-processing strings, enclose each string with single or double quotes.", - "exportToImg": "Export Graph", - "exportToCSV":"Export CSV", - "export":"Export", - "toBlobError": "Export failed. The current canvas size is too large. Please zoom out to retry.", - "expandTip": "Double-click any vertex to explore its related vertices and edges.", - "hotKeysInstructions": "Shortcut Keys Instruction", - "graphAlgorithm": "Graph Algorithm", - "srcId": "Src ID", - "dstId": "Dst ID", - "relation": "Relation", - "direction": "Direction", - "stepLimit": "Step Limit", - "allPath": "All path", - "shortestPath": "Shortest Path", - "noLoopPath": "NoLoop Path", - "algorithmParams": "Algorithm Parameters", - "steps": "Steps", - "singleStep": "Single", - "rangeStep": "Range", - "addCondition": "Add condition", - "customStyle": "Custom Color/Icon", - "nodeSearch":"Artboard node search", - "searchEmpty": "No data found", - "selectedVertexes": "Selected Vertexes", - "selectedEdges": "Selected Edges", - "viewDetails": "View Details", + "vertexStyle": "Vertex Color", + "notExist": "Not exist", "expandItem": "Expand", - "collapseItem": "Collapse", - "searchTip": "The following comparison operators are currently supported [=, >, <, !=, <>, <=, >=]", - "expressionError": "Expression error", - "expandTips": "Double-click the vertex to quickly expand according to the current configuration by default", - "missingParams": "Missing parameters", - "emptyIndexTips": "No attribute index currently does not support the data query function in Explore, Please select index with attribute for query", - "docForFindPath": "https://docs.nebula-graph.io/2.5.0/3.ngql-guide/16.subgraph-and-path/2.find-path/" + "collapseItem": "Collapse" }, "import": { - "import": "Import", - "selectSpace": "Select Space", "uploadFile": "Upload Files", - "vertex": "Map Vertices", + "importData": "Import Data", + "createTask": "New Import", + "uploadTemp": "Import Template", + "downloadConfig": "Download Config", + "downloadLog": "Download Log", + "viewLogs": "View Logs", + "details": "Details", + "task": "import task", + "taskList": "Task List", + "taskName": "Task Name", + "vertices": "Map Vertices", "edge": "Map Edges", - "runImport": "Start Import", - "next": "Next", - "goback": "Prev", - "mountPath": "Mount Path", - "importConfigValidationSuccess": "The configuration validation was successful", - "mountPathPlaceholder": "Please input the docker data mount path", - "fileName": "Name", + "runImport": "Import", + "fileName": "File Name", "withHeader": "Header", - "fileType": "Type", "fileSize": "Size", "fileTitle": "Select Files", - "fileSizeErrorMsg": "File must smaller than 100 MB", - "preview": "Preview", "bindDatasource": "Bind Datasource", - "confirm": "Confirm", - "stopImportFailed": "Stop Import Failed", - "uploadFailed": "Upload Failed", - "importResults": "Import Information", - "newImport": "New Import", "endImport": "Stop Import", - "againImport": "Import Again", "prop": "Prop", - "propTip": "{name}'s Property", "mapping": "CSV Index", - "mappingTip": "The index of the csv file", - "setMappingTip": "Make the prop {prop} map to csv column{index}", - "typeTip": "Prop Type", - "setVertexId": "Set ID", - "setVertexIdTip": "Set current prop as vertex id", - "useHash": "ID Hash", - "useHashTip": "VertexId Process Method", - "unset": "Original ID", - "uuid": "UUID", - "hash": "Hash", - "setSrc": "Set SrcId", - "setSrcTip": "Set field's value as edge source id", - "setDst": "Set DstId", - "setDstTip": "Set field's value as edge destination id", - "setRank": "Set Rank", - "setRankTip": "Set field's value as edge rank", "edgeText": "Edge", "choose": "Mapping", "ignore": "Ignore", "vertexText": "Vertex", - "createConfigError": "Create config file error", - "importErrorInfo": "Error importing data. Please check the configuration or data file", - "clearAllConfigInfo": "Confirm to clear all config?", - "promptConfigInfo": "The configuration cannot be empty", - "configFile": "Configuration File: ", - "logFile":"Log File: ", - "vertexesFile": "Vertices Files: ", - "vertexFile": "The Vertex File: ", - "vertexErrorFilePath": "Error Vertex File: ", - "edgesFilePath": "Edges Files: ", - "edgeFilePath": "The Edge Files: ", - "edgeErrorFilePath": "Error Edge File Path: ", - "clearoAllConfigInfo": "Confirm to clear all config?", - "all": "All", - "mountPathWarning": "Import data need to config the WORKING_DIR env variable before starting.", - "notExist": "Not exist", - "importError": "Import Error", - "importMappingError": "The data file configuration map import failed", - "importFormatError": "Data file format is not uniform", - "importFileConfigError": "Data file configuration related error", - "importFileDownloadError": "Data file download failed", - "importFileError": "File related error", - "importNebulaError": "Error associated with instance interaction", - "datasource": "DataSource", "indexNotEmpty": "column index can't be null.", - "reset": "Reset", - "importFinished": "Import task has ended.", - "enterPassword": "Please enter your nebula account password" + "enterPassword": "Please enter your nebula account password", + "isEmpty": "is empty", + "startImporting": "Start importing", + "stopImportingSuccess": "Stop import successfully.", + "deleteSuccess": "Delete task successfully", + "batchSize": "Batch Size", + "importCompleted": "Import completed", + "importStopped": "Import stopped", + "importFailed": "Failed", + "notImported": "{total} lines not imported", + "readFailed": "{total} lines read failed", + "selectFile": "Select bind source file", + "addTag": "Add Tag", + "selectTag": "Select Tag", + "selectEdge": "Select Edge Type", + "config": "Task Config", + "parseFailed": "File parsing failed", + "uploadTemplate": "Drag & drop the YAML configuration file to this area", + "uploadBoxTip": "The YAML configuration file is used to describe information about the files to be imported, the Nebula Graph server, and more. ", + "fileUploadRequired": "1. Please make sure all CSV data files are uploaded before import the YAML file. If not, please go to ", + "fileUploadRequired2": " first.", + "exampleDownload": "2. An example for the configuration file: ", + "uploadTemplateTip": "3. Configure the Yaml file: please keep only the file name (retain the file extension) for all file paths (path, failDataPath, logPath) in the template, e.g. logPath: config.csv", + "reUpload": "Re-upload", + "fileNotExist": "{name} file does not exist!", + "importYaml": "Import the YAML file" }, "schema": { "spaceList": "Graph Space List", - "backToSpaceList": "Graph Space List", "useSpaceErrTip": "Space not found. Trying to use a newly created graph space may fail because the creation is implemented asynchronously. To make sure the follow-up operations work as expected, Wait for two heartbeat cycles, i.e., 20 seconds.", - "partitionNumDescription": "partition_num specifies the number of partitions in one replica. The default value is 100. It is usually 5 times the number of hard disks in the cluster.", - "replicaFactorDescription": "replica_factor specifies the number of replicas in the cluster. The default replica factor is 1. The suggested number is 3 in cluster. It is usually 3 in production. Due to the majority voting principle, it must set to be odd.", - "charsetDescription": "charset is short for character set. A character set is a set of symbols and encodings. The default value is utf8.", - "collateDescription": "A collation is a set of rules for comparing characters in a character set. The default value is utf8_bin.", - "vidTypeDescription": "Specifies the data type of vertex IDs (VIDs) in a graph space. ", "createSuccess": "Create Successfully", "defineFields": "Define Properties", - "setTTL": "Set TTL", "uniqProperty": "Property name cannot be duplicated", - "timestampFormat": "Supported data inserting methods:
1. call function now()
2. call function timestamp(), for example: timestamp('2021-07-05T06:18:43.984000')
3. Input the timestamp directly, namely the number of seconds from 1970-01-01 00:00:00", - "dateFormat": "Supported data inserting methods:
Call function date(), for example: date('2021-03-17')", - "timeFormat": "Supported data inserting methods:
Call function time(), for example: time('17:53:59')", - "datetimeFormat": "Supported data inserting methods:
Call function datetime(), for example: datetime('2021-03-17T17:53:59')", - "geographyFormat": "Supported data inserting methods:
Call function ST_GeogFromText(), for example:ST_GeogFromText('POINT(6 10)')", - "geography(point)Format": "Supported data inserting methods:
Call function ST_GeogFromText('POINT()'), for example:ST_GeogFromText('POINT(6 10)')", - "geography(linestring)Format": "Supported data inserting methods:
Call function ST_GeogFromText('LINESTRING()'), for example:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')", - "geography(polygon)Format": "Supported data inserting methods:
Call function ST_GeogFromText('POLYGON()'), for example:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", - "durationFormat": "Supported data inserting methods:
Call function duration(), for example:duration({years: 1, seconds: 0})", "cancelOperation": "Do you want to close this panel", "cancelPropmt": "If you close the panel, the configuration will be deleted automatically. Are you sure that you want to close the panel?", "fieldDisabled": "A TTL configuration is set for this property, so it cannot be edited. If you want to edit this property, delete the TTL configuration.", - "ttlRequired": "ttl_col and ttl_duration are required.", - "fieldRequired": "Property name and its data type are required", "indexExist": "An index exists, so TTL configuration is not permitted. A tag or edge type cannot have both an index and TTL configuration.", "indexType": "Index Type", "indexName": "Index Name", "indexFields": "Indexed Properties", + "associateName": "Associated {type} name", "dragSorting": "(Drag to Sort)", - "selectFields": "Choose Property", - "indexedLength": "Indexed length", + "selectFields": "Please choose a property", + "indexedLength": "Please enter indexed length", "indexedLengthDescription": "Set the indexed string length. If you are indexing fixed strings, you must not set this option.", "indexedLengthRequired": "Indexed length must be a positive integer", - "backToTagList": "Back to Tag List", - "backToEdgeList": "Back to Edge Type List", - "backToIndexList": "Back to Index List", - "leavePage": "Whether to leave the current page?", - "leavePagePrompt": "You have unsaved changes to the record on this tab. If you leave this tab without saving the changes, they will be lost. Are you sure that you want to leave?" + "rebuild": "Rebuild", + "createSpace": "Create Space", + "No": "No", + "spaceName": "Name", + "partitionNumber": "Partition Number", + "replicaFactor": "Replica Factor", + "charset": "Charset", + "collate": "Collate", + "vidType": "Vid Type", + "atomicEdge": "Atomic Edge", + "group": "Group", + "comment": "Comment", + "operations": "Operations", + "spaceNameEnter": "Please enter the space name", + "propertyCount": "Property Num", + "configTypeList": "{type} List", + "configTypeAction": "{action} {type}", + "timestampFormat": "Supported data inserting methods:
1. call function now()
2. call function timestamp(), for example: timestamp('2021-07-05T06:18:43.984000')
3. Input the timestamp directly, namely the number of seconds from 1970-01-01 00:00:00", + "dateFormat": "Supported data inserting methods:
Call function date(), for example: date('2021-03-17')", + "timeFormat": "Supported data inserting methods:
Call function time(), for example: time('17:53:59')", + "datetimeFormat": "Supported data inserting methods:
Call function datetime(), for example: datetime('2021-03-17T17:53:59')", + "geographyFormat": "Supported data inserting methods:
Call function ST_GeogFromText(), for example:ST_GeogFromText('POINT(6 10)')", + "geography(point)Format": "Supported data inserting methods:
Call function ST_GeogFromText('POINT()'), for example:ST_GeogFromText('POINT(6 10)')", + "geography(linestring)Format": "Supported data inserting methods:
Call function ST_GeogFromText('LINESTRING()'), for example:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')", + "geography(polygon)Format": "Supported data inserting methods:
Call function ST_GeogFromText('POLYGON()'), for example:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", + "durationFormat": "Supported data inserting methods:
Call function duration(), for example:duration({years: 1, seconds: 0})", + "setTTL": "Set TTL (Time To Live)", + "refresh": "Refresh", + "lastRefreshTime": "Last refreshed time", + "statsType": "Type", + "statsName": "Name", + "statsCount": "Count", + "statError": "Update Failed, Please try again.", + "statFinished": "Statistics end", + "deleteSpace": "Delete Graph Space", + "cloneSpace": "Clone Graph Space", + "length": "Length", + "selectVidTypeTip": "Please select the type", + "csvDownload": "Export CSV File", + "pngDownload": "Export PNG File", + "rebuildSuccess": "{names} rebuild successfully", + "rebuildFailed": "{names} rebuild failed", + "startRebuildIndex": "Start rebuilding the index {name}" + }, + "menu": { + "use": "Use Manual", + "release": "Release Note", + "forum": "Help Forum", + "nGql": "nGQL" + }, + "link": { + "nGQLHref": "https://docs.nebula-graph.io/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", + "mannualHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", + "startStudioHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/quick-start/st-ug-plan-schema/", + "versionLogHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-release-note/" } } diff --git a/app/config/locale/zh-CN.json b/app/config/locale/zh-CN.json index 7ce4b919..e07126cf 100644 --- a/app/config/locale/zh-CN.json +++ b/app/config/locale/zh-CN.json @@ -2,11 +2,9 @@ "common": { "requestError": "请求错误", "currentSpace": "当前图空间", - "languageSelect": "语言" , "seeTheHistory":"查看历史", "table": "表格", "log":"日志", - "record": "记录", "sorryNGQLCannotBeEmpty": "对不起,nGQL语句不能为空", "disablesUseToSwitchSpace": "禁止使用命令切换Space", "NGQLHistoryList": "nGQL历史列表", @@ -14,34 +12,21 @@ "empty": "清空", "run":"运行", "console": "控制台", - "explore": "图探索", "ok": "确认", "success": "成功", "fail": "失败", - "noData": "没有相应数据", "cancel": "取消", "confirm": "确认", "import": "导入", - "help": "帮助", - "use": "使用手册", - "release": "新发布", - "nebula": "Nebula", - "setting":"设置", - "feedback": "问题反馈", - "forum": "求助论坛", - "forumLink": "https://discuss.nebula-graph.com.cn/", "ask": "确定进行当前操作?", - "output":"导出CSV文件", "openInExplore": "导入图探索", "schema": "Schema", "create": "创建", - "serialNumber": "序号", "name": "名称", "operation": "操作", "delete": "删除", - "optionalParameters": "可选参数", + "optional": "可选", "exportNGQL": "对应的nGQL语句", - "field": "字段", "relatedProperties": "相关属性", "type": "类型", "edit": "编辑", @@ -56,286 +41,204 @@ "tag": "标签", "edge": "边类型", "index": "索引", - "list": "列表", "yes": "确定", "no": "取消", "graph": "可视化", - "description": "说明", - "zoomOut": "缩小", - "zoomIn": "放大", - "move": "移动", - "rollback": "撤销", - "unlock": "解锁", - "lock": "锁定", - "moreSuggestion": "更多建议", - "algorithm": "算法", - "viewDocs": "查看文档", - "hotKeys": "快捷键", - "show": "显示", - "selected": "选中", - "search": "查询", "color": "颜色", - "icon": "图标", - "copy": "复制", - "copySuccess": "已复制到剪切板", "total": "共计", - "exportSelectVertexes": "导出选中点CSV", - "exportSelectEdges": "导出选中边CSV", - "noSelectedData": "当前没有选中数据", - "namePlaceholder":"请输入搜索名称", - "comment": "描述" + "namePlaceholder":"请输入{name}名", + "comment": "描述", + "space": "图空间", + "version": "版本", + "statistics": "统计" + }, + "doc": { + "welcome": "欢迎使用", + "functionIntro": "功能介绍", + "schemaIntro": "您可以在 schema 模块对 Nebula Graph 图空间进行管理。", + "importIntro": "您可以使用导入模块将数据批量导入 Nebula Graph。", + "consoleIntro": "您可以使用控制台模块对 Nebula Graph 内的数据进行查询操作。", + "learningDoc": "学习文档", + "getStarted": "认识 Nebula Studio", + "getStartedTip": "什么是 Nebula Studio。", + "useGuide": "Nebula Studio 使用手册", + "useGuideTip": "学习如何使用 Nebula Studio。", + "ngqlIntro": "Nebula Graph 查询语言 (nGQL)", + "ngqlIntroTip": "nGQL 是 Nebula Graph 使用的的声明式图查询语言,是为开发和运维人员设计的类 SQL 查询语言,易于学习。", + "start": "快速开始" }, "warning": { - "configServer": "请先配置服务器", "connectError": "数据库连接有误,请重新配置" }, - "NGQLOutput": { - "success": "执行成功" - }, "configServer": { "connect": "连接", "host": "Host", "username": "用户名", "password": "密码", "success": "配置成功", - "fail": "配置失败", - "clear": "清除连接", + "clear": "登出", "title": "配置数据库" }, "formRules": { "hostRequired": "请填写数据库服务器的IP地址", "usernameRequired": "请填写用户名", "passwordRequired": "请填写密码", - "nodeIdError": "格式错误,一行1个VID,按回车键分隔", - "idRequired": "请输入导入的节点id", "positiveIntegerRequired": "请输入一个非负整数", "nameValidate": "命名必须以字母开头,且只支持输入英文字母、数字以及下划线_", "nameRequired": "请输入名称", "numberRequired": "请输入正整数", "replicaLimit": "副本数量不得超过你当前 online 机器数量({number})", - "propertyRequired": "请输入属性名称", - "defaultRequired": "请输入默认值", "ttlRequired": "请选择TTL指定的属性, 且属性的数据类型需为integer或timestamp", "ttlDurationRequired": "请输入时间(s)", - "dataTypeRequired": "请选择数据类型", - "fixedStringLength": "Fixed String 长度需为正整数" + "dataTypeRequired": "请选择数据类型" }, "console": { - "cost": "开销", "execTime": "执行时间消耗", "exportVertex": "请选择表中代表点VID的列", "exportEdge": "请选择结果中分别代表边的起点(src_vid)、终点(dst_vid)和权重(rank)的列", - "showSubgraphs": "查看子图", "deleteHistory": "清除历史", - "parameterDisplay": "自定义参数 展示" + "cypherParam": "自定义参数", + "favorites": "收藏夹", + "addToFavorites": "添加到收藏夹", + "unfavorite": "取消收藏", + "clearFavorites": "清空收藏夹", + "graphviz": "Graphviz", + "selectSpace": "请选择图空间" }, "explore": { - "clear": "清除", - "clearTip": "是否清除当前视图?", - "startWithVertices": "开始探索", - "addConfirm": "确认添加", - "undo": "回退", - "deleteSelectNodes": "删除选中", - "expand": "拓展", - "unExpand": "取消拓展", - "fileImport": "文件导入", - "sampleImport": "样本导入", - "importPlaceholder": "输入VID或者用于生成VID的数据,一行一个数据,按回车键断开。格式示例如下:\nstring1\nstring2\nstring3", - "outgoing": "流出", - "incoming": "流入", - "bidirect": "双向", - "filter": "自定义筛选条件", - "operator": "运算符", - "value": "值", - "selectSpace": "请选择Space", - "selectReminder": "切换Space会清除当前显示的数据,您确定要切换吗?", - "zoom": "缩放", - "showTags": "显示点", - "showEdges": "显示边", - "confirm": "确定", - "vertexStyle": "节点颜色/图标", - "quantityLimit": "结果数量限制", - "colorGroupByTag": "按标签类型分类", - "noVertexPrompt": "当前画板没有点数据,请", - "search": "探索", - "queryById": "按VID查询", - "queryByIndex": "按索引查询", - "queryByCustom": "自定义查询", - "idToBeQueried": "指定VID", - "idPretreatment": "VID预处理", - "indexQueryPrompt_prefix": "当前Space ", - "indexQueryPrompt_suffix": "下,没有任何标签的索引,无法进行索引查询", - "indexQueryPrompt2": "请按如下示例创建标签索引", - "runCodeInConsole": "去控制台运行语句", - "indexLink": "关于索引的更多信息,请查看对应的", - "documentIntroduction": "文档介绍", - "selectIndex": "选择索引", - "paramFilter": "使用索引", - "relationship": "组合关系", - "operationConfirm": "删除操作会清空后续筛选条件。请确认是否继续执行", - "quiry": "查询", - "customQueryDescription": "可在控制台输入相应nGQL语句,查询得到结果后,点击上图中的“导入图探索”按钮,进行可视化探索", - "openInConsole": "去控制台", - "insertMethodSelect": "当前画板存在部分数据,请选择新增查询结果的插入方式", - "incrementalInsertion": "增量插入", - "insertAfterClear": "清除插入", - "emptyIndex": "索引为空", - "indexConditionDescription": "匹配字段时,必须以索引中左边第一个字段开始,如果需要匹配多个字段,不得跳过字段,但是可以省略后续字段。", - "timestampInput": "时间戳字段只支持输入数字", - "documentIntroductionUrl": "https://docs.nebula-graph.com.cn/2.5.0/3.ngql-guide/14.native-index-statements/", - "customQueryUrl": "https://cloud-cdn.nebula-graph.com.cn/studio-resource/go-to-explore_zh.png", - "pretreatmentExplaination": "Hash能预处理bool、double、int、string类型的数据生成VID,但是UUID仅支持预处理string类型的数据。如果您需要使用Hash或UUID预处理string生成VID,则使用单引号或双引号标示每个string。", - "exportToImg": "导出图形", - "exportToCSV":"导出CSV", - "export":"导出", - "toBlobError": "导出失败。当前画布尺寸过大,请缩放画布尺寸后重试。", - "expandTip": "双击任意点也可实现该点的拓展。", - "hotKeysInstructions": "图探索快捷键说明", - "graphAlgorithm": "图算法", - "allPath": "全路径", - "shortestPath": "最短路径", - "noLoopPath": "非循环路径", - "algorithmParams": "算法参数", - "srcId": "起点", - "dstId": "终点", - "relation": "关系", - "direction": "方向", - "stepLimit": "步数限制", - "steps": "步数", - "singleStep": "单步", - "rangeStep": "范围", - "addCondition": "添加条件", - "expansionConditions": "拓展条件", - "customStyle": "自定义颜色/图标", - "nodeSearch": "画板节点搜索", - "searchEmpty": "未查询到相应数据", - "selectedVertexes": "选中的点", - "selectedEdges": "选中的边", - "viewDetails": "查看详情", + "vertexStyle": "节点颜色", + "notExist": "不存在", "expandItem": "展开", - "collapseItem": "收起", - "searchTip": "当前支持以下比较符 [=, >, <, !=, <>, <=, >=]", - "expressionError": "表达式错误", - "expandTips": "双击节点默认按当前配置快捷展开", - "missingParams": "参数缺失", - "emptyIndexTips": "无属性索引暂不支持查询数据功能,建议选择带属性索引查询", - "docForFindPath": "https://docs.nebula-graph.com.cn/2.5.0/3.ngql-guide/16.subgraph-and-path/2.find-path/" + "collapseItem": "收起" }, "import": { - "import":"导入", - "selectSpace": "选择Space", "uploadFile": "上传文件", - "vertex": "关联点", + "importData": "导入数据", + "createTask": "创建导入任务", + "uploadTemp": "导入模板", + "downloadConfig": "下载配置文件", + "downloadLog": "下载日志", + "viewLogs": "查看日志", + "details": "详情", + "task": "导入任务", + "taskList": "任务列表", + "taskName": "任务名称", + "vertices": "关联点", "edge": "关联边", "runImport": "导入", - "next": "下一步", - "goback":"上一步", - "mountPath": "挂载路径", - "importConfigValidationSuccess": "配置验证成功", - "mountPathPlaceholder": "请输入docker启动的数据挂载路径", "fileName": "文件名", "withHeader": "头字段", - "fileType": "类型", "fileSize": "大小", "fileTitle": "文件列表", - "fileSizeErrorMsg": "文件必须小于100MB", - "preview": "预览", "bindDatasource": "绑定数据源", - "confirm": "确认", - "importResults": "导入信息", - "newImport": "新建导入", "endImport": "终止导入", - "againImport": "再次导入", "prop": "属性", - "propTip": "{name}中拥有的属性", "mapping": "对应列标", - "mappingTip": "属性字段对应csv文件的哪一列", - "setMappingTip": "将属性{prop}对应当前csv的第{index}列", - "typeTip": "属性字段对应的数据类型", - "setVertexId": "设为ID", - "setVertexIdTip": "当前字段是否作为Vertex Id", - "useHash": "ID Hash", - "useHashTip": "id字段对应值插入数据库中所做的处理", - "unset": "保持原值", - "uuid": "UUID", - "hash": "Hash", - "setSrc": "设为起点", - "setSrcTip": "将当前字段值作为起点", - "setDst": "设为终点", - "setDstTip": "将当前字段值作为终点", - "setRank": "设为Rank", - "setRankTip": "将当前字段值作为rank", "edgeText": "边", "choose": "选择", "ignore": "忽略", "vertexText": "点", - "createConfigError": "创建配置文件错误", - "importErrorInfo": "导入数据错误,请检查配置或数据文件", - "clearAllConfigInfo": "是否确定清空所有配置?", - "configFile": "配置文件:", - "logFile": "日志文件:", - "vertexesFile": "点相关文件:", - "vertexFile": "该点配置文件:", - "vertexErrorFile": "该点错误数据文件:", - "edgesFilePath": "边配置文件:", - "edgeFilePath": "该边配置文件:", - "edgeErrorFilePath": "该边错误数据文件:", - "all": "全部", - "mountPathWarning": "导入数据需在应用启动时配置WORKING_DIR环境变量,否则无法进行。", - "notExist": "不存在", - "importError": "未知错误", - "importMappingError": "数据文件配置映射导入失败", - "importFormatError": "数据文件格式不统一", - "importFileConfigError": "数据文件配置相关错误", - "importFileDownloadError": "数据文件下载失败", - "importFileError": "文件相关错误", - "importNebulaError": "与实例交互相关错误", - "datasource": "数据源", "indexNotEmpty": "对应列标不能为空", - "reset": "重置", - "importFinished": "导入任务已结束", - "enterPassword": "请输入 nebula 账号密码" + "enterPassword": "请输入 nebula 账号密码", + "isEmpty": "为空", + "startImporting": "开始导入", + "stopImportingSuccess": "已停止导入", + "deleteSuccess": "已删除任务记录", + "batchSize": "批处理量", + "importCompleted": "导入完成", + "importStopped": "导入中止", + "importFailed": "导入失败", + "notImported": "{total}行未导入", + "readFailed": "{total}行读取失败", + "selectFile": "选择绑定文件", + "addTag": "添加 Tag", + "selectTag": "选择 Tag", + "selectEdge": "选择 Edge 类型", + "config": "任务配置", + "parseFailed": "文件解析失败", + "uploadTemplate": "将 YAML 配置文件拖放到该区域", + "uploadBoxTip": "The YAML configuration file is used to describe information about the files to be imported, the Nebula Graph server, and more. ", + "fileUploadRequired": "1. 请确保在导入 YAML 文件之前上传所有 CSV 数据文件。 如果没有,请先前往", + "fileUploadRequired2": "数据文件", + "exampleDownload": "2. 配置文件示例:", + "uploadTemplateTip": "3. 配置Yaml文件:模板中所有文件路径(path、failDataPath、logPath)请只保留文件名(保留文件扩展名),例如: 日志路径:config.csv", + "reUpload": "重新上传", + "fileNotExist": "文件 {name} 不存在", + "importYaml": "导入 YAML 文件" }, "schema": { "spaceList": "图空间列表", - "backToSpaceList": "图空间列表", "useSpaceErrTip": "图空间未找到。立刻尝试使用刚创建的图空间可能会失败,因为创建是异步实现的。为确保数据同步,后续操作能顺利进行,请等待 2 个心跳周期(20 秒)。", - "partitionNumDescription": "partition_num 表示数据分片数量。默认值为 100。建议为硬盘数量的 5 倍。", - "replicaFactorDescription": "replica_factor 表示副本数量。默认值是 1,生产集群建议为 3。由于采用多数表决原理,因此需为奇数。", - "charsetDescription": "charset 表示字符集,定义了字符以及字符的编码,默认为 utf8。", - "collateDescription": "collate 表示字符序,定义了字符的比较规则,默认为 utf8_bin。", - "vidTypeDescription": "vid type 指定图空间中点 ID(VID)的数据类型。", "createSuccess": "创建成功", "defineFields": "定义属性", - "setTTL": "设置TTL", "uniqProperty": "属性名称不允许重名", - "timestampFormat": "时间类型支持插入方式:
1. 调用函数 now()
2. 调用函数 timestamp(),例如:timestamp('2021-07-05T06:18:43.984000')
3. 直接输入时间戳,即从 1970-01-01 00:00:00 开始的秒数", - "dateFormat": "日期类型支持插入方式:
调用函数 date(),例如:date('2021-03-17')", - "timeFormat": "时间类型支持插入方式:
调用函数 time(),例如time('17:53:59')", - "datetimeFormat": "日期时间类型支持插入方式:
调用函数 datetime(),例如:datetime('2021-03-17T17:53:59')", - "geographyFormat": "geo 类型支持插入方式:
调用函数 ST_GeogFromText(),例如:ST_GeogFromText('POINT(6 10)')", - "geography(point)Format": "geo(point) 类型支持插入方式:
调用函数 ST_GeogFromText('POINT()'),例如:ST_GeogFromText('POINT(6 10)')", - "geography(linestring)Format": "geo(linestring) 类型支持插入方式:
调用函数 ST_GeogFromText('LINESTRING()'),例如:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')", - "geography(polygon)Format": "geo(polygon) 类型支持插入方式:
调用函数 ST_GeogFromText('POLYGON()'),例如:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", - "durationFormat": "duration 类型支持插入方式:
调用函数 duration(),例如:duration({years: 1, seconds: 0})", "cancelOperation": "是否取消配置并关闭面板", "cancelPropmt": "关闭面板将删除所有属性,是否继续?", "fieldDisabled": "该属性被 ttl_col 引用,不支持更改操作,如要更改,请先更新 ttl", - "ttlRequired": "请填写完整 ttl 关联的属性以及持续时间", - "fieldRequired": "请填写完整属性名称及数据类型", "indexExist": "已拥有索引,无法同时配置 TTL", "indexType": "索引类型", "indexName": "索引名称", "indexFields": "索引属性", + "associateName": "关联 {type} 名称", "dragSorting": "(可拖拽排序)", - "selectFields": "选择关联的属性", - "indexedLength": "索引长度", + "selectFields": "请选择关联的属性", + "indexedLength": "请输入索引长度", "indexedLengthDescription": "设置索引字符串的长度。如果索引定长字符串,则索引长度无法修改。", "indexedLengthRequired": "索引长度应为正整数", - "backToTagList": "返回标签列表", - "backToEdgeList": "返回边类型列表", - "backToIndexList": "返回索引列表", - "leavePage": "是否离开当前页面", - "leavePagePrompt": "离开当前页面后,未保存的记录将丢失" + "rebuild": "重建索引", + "createSpace": "创建图空间", + "No": "序号", + "spaceName": "名称", + "partitionNumber": "Partition Number", + "replicaFactor": "Replica Factor", + "charset": "Charset", + "collate": "Collate", + "vidType": "Vid Type", + "atomicEdge": "Atomic Edge", + "group": "Group", + "comment": "Comment", + "operations": "操作", + "spaceNameEnter": "请输入图空间名称", + "propertyCount": "属性数量", + "configTypeList": "{type}列表", + "configTypeAction": "{action}{type}", + "timestampFormat": "时间类型支持插入方式:
1. 调用函数 now()
2. 调用函数 timestamp(),例如:timestamp('2021-07-05T06:18:43.984000')
3. 直接输入时间戳,即从 1970-01-01 00:00:00 开始的秒数", + "dateFormat": "日期类型支持插入方式:
调用函数 date(),例如:date('2021-03-17')", + "timeFormat": "时间类型支持插入方式:
调用函数 time(),例如time('17:53:59')", + "datetimeFormat": "日期时间类型支持插入方式:
调用函数 datetime(),例如:datetime('2021-03-17T17:53:59')", + "geographyFormat": "geo 类型支持插入方式:
调用函数 ST_GeogFromText(),例如:ST_GeogFromText('POINT(6 10)')", + "geography(point)Format": "geo(point) 类型支持插入方式:
调用函数 ST_GeogFromText('POINT()'),例如:ST_GeogFromText('POINT(6 10)')", + "geography(linestring)Format": "geo(linestring) 类型支持插入方式:
调用函数 ST_GeogFromText('LINESTRING()'),例如:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')", + "geography(polygon)Format": "geo(polygon) 类型支持插入方式:
调用函数 ST_GeogFromText('POLYGON()'),例如:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", + "durationFormat": "duration 类型支持插入方式:
调用函数 duration(),例如:duration({years: 1, seconds: 0})", + "setTTL": "设置TTL(存活时间)", + "refresh": "更新", + "lastRefreshTime": "上次更新时间", + "statsType": "维度", + "statsName": "名称", + "statsCount": "数量", + "statError": "统计失败,请重试", + "statFinished": "统计结束", + "deleteSpace": "删除图空间", + "cloneSpace": "克隆图空间", + "length": "长度", + "selectVidTypeTip": "选择 Vid 类型", + "csvDownload": "导出 CSV", + "pngDownload": " 导出 PNG", + "rebuildSuccess": "{names}重建完成", + "rebuildFailed": "{names}重建失败", + "startRebuildIndex": "开始重建索引{name}" + }, + "menu": { + "use": "使用手册", + "release": "更新日志", + "forum": "求助论坛", + "nGql": "nGQL" + }, + "link": { + "nGQLHref": "https://docs.nebula-graph.com.cn/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", + "mannualHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", + "startStudioHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/quick-start/st-ug-plan-schema/", + "versionLogHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-release-note/" } } \ No newline at end of file diff --git a/app/config/nebulaQL.ts b/app/config/nebulaQL.ts index 950fdbaf..e2d04d31 100644 --- a/app/config/nebulaQL.ts +++ b/app/config/nebulaQL.ts @@ -228,4 +228,4 @@ const nebulaWordsLowercase = nebulaWordsUppercase.map(w => w.toLowerCase()); export const keyWords = [...nebulaWordsUppercase, ...nebulaWordsLowercase]; -export const maxLineNum = 20; +export const maxLineNum = 5; diff --git a/app/config/rules.ts b/app/config/rules.ts index 30411c01..0d0ad482 100644 --- a/app/config/rules.ts +++ b/app/config/rules.ts @@ -1,52 +1,52 @@ -import { POSITIVE_INTEGER_REGEX } from '#app/utils/constant'; +import { NAME_REGEX, POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; +import intl from 'react-intl-universal'; -export const hostRulesFn = intl => [ +export const hostRulesFn = () => [ { required: true, message: intl.get('formRules.hostRequired'), }, ]; -export const usernameRulesFn = intl => [ +export const usernameRulesFn = () => [ { required: true, message: intl.get('formRules.usernameRequired'), }, ]; -export const passwordRulesFn = intl => [ +export const passwordRulesFn = () => [ { required: true, message: intl.get('formRules.passwordRequired'), }, ]; -export const nodeIdRulesFn = intl => [ - { - required: true, - message: intl.get('formRules.idRequired'), - }, - { - pattern: /^(.+)*(\n.+)*(\n)*$/, - message: intl.get('formRules.nodeIdError'), - }, -]; - -export const nameRulesFn = intl => [ - { - required: true, - message: intl.get('formRules.nameRequired'), - }, -]; +export const nameRulesFn = () => { + const version = sessionStorage.getItem('nebulaVersion'); + const nameRequired = [ + { + required: true, + message: intl.get('formRules.nameRequired'), + }, + ]; + const nameValidate = [ + { + pattern: NAME_REGEX, + message: intl.get('formRules.nameValidate'), + }, + ]; + return version?.startsWith('v2') ? [...nameRequired, ...nameValidate] : nameRequired; +}; -export const numberRulesFn = intl => [ +export const numberRulesFn = () => [ { pattern: POSITIVE_INTEGER_REGEX, message: intl.get('formRules.numberRequired'), }, ]; -export const replicaRulesFn = (intl, activeMachineNum) => [ +export const replicaRulesFn = (activeMachineNum) => [ { pattern: POSITIVE_INTEGER_REGEX, message: intl.get('formRules.numberRequired'), diff --git a/app/config/service.ts b/app/config/service.ts index 6cbdccdf..5db84c3b 100644 --- a/app/config/service.ts +++ b/app/config/service.ts @@ -2,20 +2,22 @@ import { _delete, get, post, put } from '../utils/http'; const execNGQL = post('/api-nebula/db/exec'); +const batchExecNGQL = post('/api-nebula/db/batchExec'); + const connectDB = post('/api-nebula/db/connect'); const disconnectDB = post('/api-nebula/db/disconnect'); -const importData = post('/api-nebula/task/import'); - -const handleImportAction = post('/api-nebula/task/import/action'); +const importData = post('/api/import-tasks/import'); -const createConfigFile = post('/api/import/config'); +const handleImportAction = post('/api/import-tasks/action'); -const getLog = get('/api/import/log'); +const getLog = get('/api/import-tasks/logs'); +const getErrLog = get('/api/import-tasks/err-logs'); const finishImport = post('/api/import/finish'); -const getImportWokingDir = get('/api/import/working_dir'); +const getUploadDir = get('/api/import-tasks/working-dir'); +const getTaskDir = get('/api/import-tasks/task-dir'); const deteleFile = params => { const { filename } = params; @@ -29,17 +31,33 @@ const uploadFiles = (params?, config?) => 'Content-Type': 'multipart/form-data', }, }); + + +const getTaskLogs = (params?, config?) => { + const { id, ...others } = params; + return get(`/api/import-tasks/${id}/task-log-names`)(others, config); +}; + +const getTaskConfigUrl = (id: string | number) => `/api/import-tasks/config/${id}`; +const getTaskLogUrl = (id: string | number) => `/api/import-tasks/${id}/log`; +const getTaskErrLogUrl = (id: string | number) => `/api/import-tasks/${id}/err-logs`; export default { execNGQL, + batchExecNGQL, connectDB, disconnectDB, importData, finishImport, handleImportAction, - createConfigFile, getLog, - getImportWokingDir, + getErrLog, + getUploadDir, + getTaskDir, deteleFile, getFiles, uploadFiles, + getTaskConfigUrl, + getTaskLogs, + getTaskLogUrl, + getTaskErrLogUrl }; diff --git a/app/context/LanguageContext.ts b/app/context/LanguageContext.ts index 323d6540..b1e5e8ce 100644 --- a/app/context/LanguageContext.ts +++ b/app/context/LanguageContext.ts @@ -1,10 +1,14 @@ import React from 'react'; - -import { INTL_LOCALE_SELECT } from '../config'; - +import intl from 'react-intl-universal'; +import Cookie from 'js-cookie'; +import { INTL_LOCALES, INTL_LOCALE_SELECT } from '@app/config/constants'; export const LanguageContext = React.createContext({ currentLocale: INTL_LOCALE_SELECT.EN_US.NAME, - toggleLanguage: (locale: string) => { - console.log('Select locale:', locale); + toggleLanguage: (currentLocale: string) => { + Cookie.set('lang', currentLocale); + intl.init({ + currentLocale, + locales: INTL_LOCALES, + }); }, }); diff --git a/app/global.d.ts b/app/global.d.ts new file mode 100644 index 00000000..8c730b5e --- /dev/null +++ b/app/global.d.ts @@ -0,0 +1,8 @@ +declare module '*.svg'; +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.gif'; +declare module '*.bmp'; +declare module '*.tiff'; + diff --git a/app/index.html b/app/index.html index 2daa3bfc..f6e993f8 100644 --- a/app/index.html +++ b/app/index.html @@ -1,5 +1,5 @@ - + @@ -73,8 +73,8 @@ - -
+ +
diff --git a/app/index.tsx b/app/index.tsx index 37c2a8d6..0a3b4149 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,19 +1,78 @@ -import { message } from 'antd'; -import React from 'react'; +import { hot } from 'react-hot-loader/root'; +import { Spin, message } from 'antd'; +import React, { Suspense, lazy, useState } from 'react'; import ReactDom from 'react-dom'; -import { Provider } from 'react-redux'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { Route, BrowserRouter as Router, Switch, useHistory } from 'react-router-dom'; +import { observer } from 'mobx-react-lite'; +import dayjs from 'dayjs'; +import intl from 'react-intl-universal'; +import duration from 'dayjs/plugin/duration'; +import Cookie from 'js-cookie'; +import { INTL_LOCALES } from '@app/config/constants'; +import { LanguageContext } from '@app/context'; +import AuthorizedRoute from './AuthorizedRoute'; +import rootStore, { StoreProvider } from './stores'; +const Login = lazy(() => import('@app/pages/Login')); +const MainPage = lazy(() => import('@app/pages/MainPage')); -import App from './App'; -import { store } from './store'; +import './common.less'; +import './app.less'; +dayjs.extend(duration); message.config({ maxCount: 1, }); -ReactDom.render( - - - - - , - document.getElementById('app'), -); +const defaultLanguage = Cookie.get('lang') || document.documentElement.getAttribute('lang'); +intl.init({ + currentLocale: defaultLanguage || 'EN_US', + locales: INTL_LOCALES, +}); + + +const PageRoot = observer(() => { + const [currentLocale, setCurrentLocale] = useState( + defaultLanguage || 'EN-US', + ); + + const toggleLanguage = (locale: string) => { + Cookie.set('lang', locale); + setCurrentLocale(locale); + intl + .init({ + currentLocale: locale, + locales: INTL_LOCALES, + }); + }; + + return ( + + + + + + + + ); +}); + +const App = () => { + const history = useHistory(); + rootStore.global.history = history; + + return ( + + }> + + + + + ); +}; + +const HotPageRoot = hot(PageRoot); + +ReactDom.render(, document.getElementById('studio-app')); diff --git a/app/interfaces/graph.ts b/app/interfaces/graph.ts new file mode 100644 index 00000000..cda62eda --- /dev/null +++ b/app/interfaces/graph.ts @@ -0,0 +1,13 @@ +import { LinkObject, NodeObject } from '@vesoft-inc/force-graph'; + +export interface IDataMap { + [key: string]: NodeObject | LinkObject; +} + +export interface Pointer { + top: number; + left: number; + event?: any; + node?: NodeObject; + showContextMenu?: boolean; +} diff --git a/app/interfaces/import.ts b/app/interfaces/import.ts new file mode 100644 index 00000000..61391df1 --- /dev/null +++ b/app/interfaces/import.ts @@ -0,0 +1,48 @@ + +export enum ITaskStatus { + 'StatusFinished' = 'statusFinished', + 'StatusStoped' = 'statusStoped', + 'StatusProcessing' = 'statusProcessing', + 'StatusNotExisted' = 'statusNotExisted', + 'StatusAborted' = 'statusAborted', +} + +export interface ITaskStats { + totalBatches: number; + totalBytes: number; + totalImportedBytes: number; + totalLatency: number; + totalReqTime: number; + numFailed: number; + numReadFailed: number; +} +export interface ITaskItem { + taskID: number; + nebulaAddress: string; + space: string; + name: string; + createdTime: number; + updatedTime: number; + user: string; + taskStatus: ITaskStatus; + taskMessage: string; + stats: ITaskStats; +} + +export interface IVerticesConfig { + name: string; + file: any; + tags: any[]; + idMapping: any; +} +export interface IEdgeConfig { + name: string; + file: any; + props: any[]; + type: string; +} + +export interface IBasicConfig { + taskName: string; + batchSize?: string; +} \ No newline at end of file diff --git a/app/interfaces/schema.ts b/app/interfaces/schema.ts new file mode 100644 index 00000000..8afe8992 --- /dev/null +++ b/app/interfaces/schema.ts @@ -0,0 +1,76 @@ +export interface IIndex { + indexName: string; + props: IField[]; +} +export interface ITree { + name: string; + indexes: IIndex[]; +} + +export interface IField { + Field: string; + Type: string; +} + +export interface ISpace { + serialNumber: number; + Name: string; + ID: number; + Charset: string; + Collate: string; + 'Partition Number': string; + 'Replica Factor': string; +} + +export interface ITag { + name: string; + fields: IField[]; +} +export interface IEdge { + name: string; + fields: IField[]; +} +export interface IIndexList { + name: string; + owner: string; + comment?: string | null; + fields: IField[]; +} + +export interface IProperty { + name: string; + type: string; + value?: string; + allowNull?: boolean; + fixedLength?: string; + comment?: string; + showType?: string; +} + +export type IndexType = 'tag' | 'edge'; +export type ISchemaType = 'tag' | 'edge'; +export type AlterType = 'ADD' | 'DROP' | 'CHANGE' | 'TTL' | 'COMMENT'; +export interface IAlterConfig { + fields?: IProperty[]; + comment?: string; + ttl?: { + col?: string; + duration?: string; + }; +} + +export interface IAlterForm { + type: ISchemaType; + name: string; + action: AlterType; + config: IAlterConfig; +} + +export enum IJobStatus { + Queue = 'QUEUE', + Running = 'RUNNING', + Finished = 'FINISHED', + Failed = 'FAILED', + Stopped = 'STOPPED', + Removed = 'REMOVED', +} \ No newline at end of file diff --git a/app/modules/ConfigServer/index.less b/app/modules/ConfigServer/index.less deleted file mode 100644 index ad78d6d9..00000000 --- a/app/modules/ConfigServer/index.less +++ /dev/null @@ -1,19 +0,0 @@ -.config-server { - position: relative; - background: #fff; - height: 100%; - - .nebula-authorization { - width: 100%; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - > h3 { - padding-top: 12px; - text-align: center; - font-size: 24px; - } - } -} diff --git a/app/modules/ConfigServer/index.tsx b/app/modules/ConfigServer/index.tsx deleted file mode 100644 index 78bed497..00000000 --- a/app/modules/ConfigServer/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { WrappedFormUtils } from 'antd/lib/form/Form'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; -import { RouteComponentProps } from 'react-router-dom'; - -import ConfigServerForm from '#app/components/ConfigServerForm'; -import { IDispatch } from '#app/store'; -import { trackPageView } from '#app/utils/stat'; - -import './index.less'; - -const mapDispatch = (dispatch: IDispatch) => ({ - asyncConfigServer: dispatch.nebula.asyncConfigServer, -}); - -const mapState = () => ({}); - -interface IProps - extends ReturnType, - ReturnType, - RouteComponentProps {} - -class ConfigServer extends React.Component { - componentDidMount() { - trackPageView('/connect-server'); - } - - handleConfigServer = (form: WrappedFormUtils) => { - form.validateFields(async (err, data) => { - if (!err) { - const ok = await this.props.asyncConfigServer(data); - if (ok) { - this.props.history.replace('/'); - } - } - }); - }; - render() { - return ( -
-
-

{intl.get('configServer.title')}

- -
-
- ); - } -} - -export default connect(mapState, mapDispatch)(ConfigServer); diff --git a/app/modules/Console/SpaceSearchInput.tsx b/app/modules/Console/SpaceSearchInput.tsx deleted file mode 100644 index 64be2d28..00000000 --- a/app/modules/Console/SpaceSearchInput.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Select } from 'antd'; -import React from 'react'; -import { connect } from 'react-redux'; - -import { IDispatch, IRootState } from '#app/store'; - -const { Option } = Select; - -const mapState = (state: IRootState) => ({ - spaces: state.nebula.spaces, - loading: state.loading.effects.nebula.asyncGetSpaces, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - asyncGetSpaces: dispatch.nebula.asyncGetSpaces, -}); - -interface IProps - extends ReturnType, - ReturnType { - onSpaceChange: (value: string) => void; - value: string; -} - -class SpaceSearchInput extends React.Component { - componentDidMount() { - this.getSpaces(); - } - getSpaces = () => { - this.props.asyncGetSpaces(); - }; - - render() { - const { value, onSpaceChange, loading } = this.props; - return ( - - ); - } -} - -export default connect(mapState, mapDispatch)(SpaceSearchInput); diff --git a/app/modules/Console/index.less b/app/modules/Console/index.less deleted file mode 100644 index 4dee001a..00000000 --- a/app/modules/Console/index.less +++ /dev/null @@ -1,153 +0,0 @@ -.nebula-console { - display: flex; - flex-direction: column; - height: 100%; - - .ngql-content { - position: relative; - display: flex; - background: #f7f7f7; - padding: 0 8px 16px 8px; - font-size: 16px; - - > .mirror-wrap { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - - .mirror-content { - width: 100%; - display: flex; - align-items: center; - - .btn-drawer { - cursor: pointer; - } - - > .CodeMirror { - min-height: 240px; - width: 100%; - } - } - - - .mirror-nav { - display: flex; - align-items: center; - text-align: left; - background: #f7f7f7; - padding: 16px 0 16px 30px; - width: 100%; - - .space-search-input { - width: 350px; - margin: 0 8px; - } - - svg { - color: #999; - } - } - - > .expand { - height: 21px; - display: flex; - justify-content: center; - align-items: center; - background: #f9f9f9; - cursor: pointer; - - &:hover { - background: #f7f7f7; - } - - > .anticon { - font-size: 20px; - } - } - } - - .operation { - display: flex; - justify-content: flex-end; - flex: 1; - - .anticon-play-circle, - .anticon-history, - .anticon-loading, - .anticon-delete { - display: flex; - width: 64px; - justify-content: center; - align-items: center; - font-size: 32px; - cursor: pointer; - } - } - } - - .result-wrap { - flex: 1; - display: flex; - flex-direction: column; - margin: 15px 0; - } -} - -.historyList { - height: 80%; - overflow: hidden; - position: relative; - - .ant-modal-content { - position: relative; - height: 100%; - } - - .ant-modal-body { - max-height: 100%; - overflow: auto; - padding-top: 55px; - } - - .ant-modal-header { - position: absolute; - top: 0; - z-index: 9; - width: 100%; - - button { - float: right; - margin-right: 20px; - margin-top: -2px; - color: rgba(0, 0, 0, 0.45); - } - } - - .ant-modal-close { - top: 10px; - right: 5px; - } - - .history-title { - display: inline-block; - margin-top: 2px; - } -} - -.param-box { - padding: 10px; - background-color: white; - height: 100%; - margin-left: 5px; - max-width: 320px; - overflow: auto; - max-height: 240px; - - p { - font-size: 12px; - word-break: break-all; - margin-bottom: 8px; - } -} diff --git a/app/modules/Console/index.tsx b/app/modules/Console/index.tsx deleted file mode 100644 index 60afad67..00000000 --- a/app/modules/Console/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { Button, Icon, List, message, Modal, Tooltip } from 'antd'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; -import { RouteComponentProps } from 'react-router-dom'; - -import { CodeMirror, OutputBox } from '#app/components'; -import { maxLineNum } from '#app/config/nebulaQL'; -import { IDispatch, IRootState } from '#app/store'; -import { trackPageView } from '#app/utils/stat'; - -import './index.less'; -import SpaceSearchInput from './SpaceSearchInput'; - -interface IState { - isUpDown: boolean; - history: boolean; - visible: boolean; -} - -const mapState = (state: IRootState) => ({ - result: state._console.result, - currentGQL: state._console.currentGQL, - paramsMap: state._console.paramsMap, - currentSpace: state.nebula.currentSpace, - runGQLLoading: state.loading.effects._console.asyncRunGQL, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - asyncRunGQL: dispatch._console.asyncRunGQL, - asyncGetParams: dispatch._console.asyncGetParams, - updateCurrentGQL: gql => - dispatch._console.update({ - currentGQL: gql, - }), - asyncSwitchSpace: async space => { - await dispatch.nebula.asyncSwitchSpace(space); - await dispatch.explore.clear(); - }, -}); - -interface IProps - extends ReturnType, - ReturnType, - RouteComponentProps {} - -// split from semicolon out of quotation marks -const SEMICOLON_REG = /((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/; -class Console extends React.Component { - codemirror; - editor; - - constructor(props: IProps) { - super(props); - - this.state = { - isUpDown: true, - history: false, - visible: false, - }; - } - - componentDidMount() { - trackPageView('/console'); - this.props.asyncGetParams(); - } - - getLocalStorage = () => { - const value: string | null = localStorage.getItem('history'); - if (value && value !== 'undefined' && value !== 'null') { - return JSON.parse(value).slice(-15); - } - return []; - }; - - handleSaveQuery = (query: string) => { - if (query !== '') { - const history = this.getLocalStorage(); - history.push(query); - localStorage.setItem('history', JSON.stringify(history)); - } - }; - - checkSwitchSpaceGql = (query: string) => { - const queryList = query.split(SEMICOLON_REG).filter(Boolean); - const reg = /^USE `?[0-9a-zA-Z_]+`?(?=[\s*;?]?)/gim; - if (queryList.some(sentence => sentence.trim().match(reg))) { - return intl.get('common.disablesUseToSwitchSpace'); - } - }; - handleRun = async () => { - const query = this.editor.getValue(); - if (!query) { - message.error(intl.get('common.sorryNGQLCannotBeEmpty')); - return; - } - const errInfo = this.checkSwitchSpaceGql(query); - if (errInfo) { - return message.error(errInfo); - } - - this.editor.execCommand('goDocEnd'); - this.handleSaveQuery(query); - await this.props.asyncRunGQL(query); - this.setState({ - isUpDown: true, - }); - }; - - handleHistoryItem = (value: string) => { - this.props.updateCurrentGQL(value); - this.setState({ - history: false, - }); - }; - - handleEmptyNgqlHistory = () => { - localStorage.setItem('history', 'null'); - this.setState({ - history: false, - }); - }; - - handleEmptyNgql = () => { - this.props.updateCurrentGQL(''); - this.forceUpdate(); - }; - - getInstance = instance => { - if (instance) { - this.codemirror = instance.codemirror; - this.editor = instance.editor; - } - }; - - handleUpDown = () => { - this.setState({ - isUpDown: !this.state.isUpDown, - }); - }; - - handleLineCount = () => { - let line; - if (this.editor.lineCount() > maxLineNum) { - line = maxLineNum; - } else if (this.editor.lineCount() < 5) { - line = 5; - } else { - line = this.editor.lineCount(); - } - this.editor.setSize(undefined, line * 24 + 10 + 'px'); - }; - - historyListShow = (str: string) => { - if (str.length < 300) { - return str; - } - return str.substring(0, 300) + '...'; - }; - - toggleDrawer = () => { - const { visible } = this.state; - this.setState({ - visible: !visible, - }); - }; - - render() { - const { isUpDown, history } = this.state; - const { - currentSpace, - currentGQL, - result, - runGQLLoading, - paramsMap, - } = this.props; - return ( -
-
-
-
- {intl.get('common.currentSpace')}: - - - - -
- - - - - { - this.setState({ history: true }); - }} - /> - - - {!!runGQLLoading ? ( - - ) : ( - this.handleRun()} - /> - )} - -
-
-
- - - - {this.state.visible && ( -
- {Object.entries(paramsMap).map(([k, v]) => ( -

{`${k} => ${JSON.stringify(v)}`}

- ))} -
- )} - this.props.updateCurrentGQL(value)} - onChangeLine={this.handleLineCount} - ref={this.getInstance} - height={isUpDown ? '240px' : 24 * maxLineNum + 'px'} - onShiftEnter={this.handleRun} - options={{ - keyMap: 'sublime', - fullScreen: true, - mode: 'nebula', - }} - /> -
-
-
-
- this.handleHistoryItem(e)} - /> -
- - - {intl.get('common.NGQLHistoryList')} - - -
- } - visible={history} - className="historyList" - footer={null} - onCancel={() => { - this.setState({ history: false }); - }} - > - { - ( - this.handleHistoryItem(item)} - > - {this.historyListShow(item)} - - )} - /> - } - -
- ); - } -} - -export default connect(mapState, mapDispatch)(Console); diff --git a/app/modules/Explore/Control/AlgorithmQuery/index.less b/app/modules/Explore/Control/AlgorithmQuery/index.less deleted file mode 100644 index ecca24a6..00000000 --- a/app/modules/Explore/Control/AlgorithmQuery/index.less +++ /dev/null @@ -1,16 +0,0 @@ -.algorithm-query { - text-align: center; - - .algorithm-form { - .select-algorithm .icon-instruction { - position: absolute; - right: -25px; - top: 50%; - transform: translateY(-50%); - } - } - - button { - margin-top: 20px; - } -} diff --git a/app/modules/Explore/Control/AlgorithmQuery/index.tsx b/app/modules/Explore/Control/AlgorithmQuery/index.tsx deleted file mode 100644 index 321d5a59..00000000 --- a/app/modules/Explore/Control/AlgorithmQuery/index.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { Button, Divider, Form, Input, Select } from 'antd'; -import { FormComponentProps } from 'antd/lib/form/Form'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import { Instruction } from '#app/components'; -import GQLCodeMirror from '#app/components/GQLCodeMirror'; -import { GRAPH_ALOGORITHM } from '#app/config/explore'; -import { IDispatch, IRootState } from '#app/store'; -import { getPathGQL } from '#app/utils/gql'; - -import './index.less'; - -const Option = Select.Option; - -const mapState = (state: IRootState) => ({ - edgeTypes: state.nebula.edgeTypes, - spaceVidType: state.nebula.spaceVidType, - loading: state.loading.effects.explore.asyncGetPathResult, -}); - -const mapDispatch = (dispatch: IDispatch) => ({ - asyncGetEdges: dispatch.nebula.asyncGetEdges, - asyncGetPathResult: dispatch.explore.asyncGetPathResult, -}); - -interface IProps - extends ReturnType, - ReturnType, - FormComponentProps { - closeHandler: any; -} - -class AlgorithmQuery extends React.Component { - componentDidMount() { - this.props.asyncGetEdges(); - } - - viewDoc = () => { - window.open(intl.get('explore.docForFindPath'), '_blank'); - }; - - handleInquiry = async () => { - const { getFieldsValue } = this.props.form; - const { spaceVidType } = this.props; - const { - type, - srcId, - dstId, - relation, - direction, - stepLimit, - quantityLimit, - } = getFieldsValue(); - this.props.form.validateFields(async err => { - if (!err) { - await this.props.asyncGetPathResult({ - spaceVidType, - type, - srcId, - dstId, - relation, - direction, - stepLimit, - quantityLimit, - }); - this.props.closeHandler(); - } - }); - }; - - render() { - const { getFieldDecorator, getFieldsValue } = this.props.form; - const { edgeTypes, spaceVidType, loading } = this.props; - const formItemLayout = { - labelCol: { - span: 6, - }, - wrapperCol: { - span: 11, - }, - }; - const { - type, - srcId, - dstId, - relation, - direction, - stepLimit, - quantityLimit, - } = getFieldsValue(); - const currentGQL = - type && srcId && dstId - ? getPathGQL({ - spaceVidType, - type, - srcId, - dstId, - relation, - direction, - stepLimit, - quantityLimit, - }) - : ''; - return ( -
-
- - {getFieldDecorator('type', { - rules: [ - { - required: true, - }, - ], - })( - , - )} - - - - {intl.get('explore.algorithmParams')} - - - {getFieldDecorator('srcId', { - rules: [ - { - required: true, - message: 'Src ID is required', - }, - ], - })()} - - - {getFieldDecorator('relation')( - , - )} - - - {getFieldDecorator('direction')( - , - )} - - - {getFieldDecorator('stepLimit', { - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - ], - })()} - - - {getFieldDecorator('quantityLimit', { - rules: [ - { - message: intl.get('formRules.positiveIntegerRequired'), - pattern: /^\d+$/, - transform(value) { - if (value) { - return Number(value); - } - }, - }, - ], - })()} - - - - -
- ); - } -} - -export default connect(mapState, mapDispatch)(Form.create()(AlgorithmQuery)); diff --git a/app/modules/Explore/Control/CustomQuery/index.less b/app/modules/Explore/Control/CustomQuery/index.less deleted file mode 100644 index 405544a9..00000000 --- a/app/modules/Explore/Control/CustomQuery/index.less +++ /dev/null @@ -1,20 +0,0 @@ -.query-custom { - padding: 10px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-family: PingFangSC-Regular, serif; - font-size: 16px; - color: #646464; - letter-spacing: 1.48px; - - .logo { - width: 92%; - margin-bottom: 30px; - } - - .btn { - margin-top: 25px; - } -} diff --git a/app/modules/Explore/Control/CustomQuery/index.tsx b/app/modules/Explore/Control/CustomQuery/index.tsx deleted file mode 100644 index 5b7186eb..00000000 --- a/app/modules/Explore/Control/CustomQuery/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Button } from 'antd'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { Link } from 'react-router-dom'; - -import { LanguageContext } from '#app/context'; -import guideEnPng from '#app/static/images/go-to-explore_en.png'; -import guideZhPng from '#app/static/images/go-to-explore_zh.png'; - -import './index.less'; -class CustomQuery extends React.PureComponent { - static contextType = LanguageContext; - render() { - const lang = this.context.currentLocale || 'ZH_CN'; - return ( -
- -

{intl.get('explore.customQueryDescription')}

-
- -
-
- ); - } -} - -export default CustomQuery; diff --git a/app/modules/Explore/Control/IdQuery/index.less b/app/modules/Explore/Control/IdQuery/index.less deleted file mode 100644 index 3375843c..00000000 --- a/app/modules/Explore/Control/IdQuery/index.less +++ /dev/null @@ -1,46 +0,0 @@ -.import-node { - .header { - position: relative; - margin-bottom: 15px; - text-align: center; - - > span { - display: inline-block; - font-family: PingFangSC-Semibold, serif; - font-size: 16px; - color: #646464; - letter-spacing: 1.48px; - } - - .btn-upload { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - - button:not(:last-child) { - margin-right: 5px; - } - } - } - - .ant-form-item label { - display: flex; - align-items: center; - - > .anticon { - vertical-align: initial; - margin-left: 5px; - font-size: 16px; - } - } - - .btn-wrap { - text-align: center; - - .ant-btn { - width: 100px; - margin-left: 16px; - } - } -} diff --git a/app/modules/Explore/Control/IdQuery/index.tsx b/app/modules/Explore/Control/IdQuery/index.tsx deleted file mode 100644 index 4d7615b7..00000000 --- a/app/modules/Explore/Control/IdQuery/index.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Button, Form, Input, Spin, Upload } from 'antd'; -import { FormComponentProps } from 'antd/lib/form/Form'; -import _ from 'lodash'; -import React from 'react'; -import intl from 'react-intl-universal'; -import { connect } from 'react-redux'; - -import { nodeIdRulesFn } from '#app/config/rules'; -import { IDispatch, IRootState } from '#app/store'; -import readFileContent from '#app/utils/file'; - -import './index.less'; - -const { TextArea } = Input; -const mapState = (state: IRootState) => ({ - sampleLoading: !!state.loading.effects.explore.asyncGetSampleVertic, -}); -const mapDispatch = (dispatch: IDispatch) => ({ - asyncImportNodes: dispatch.explore.asyncImportNodes, - asyncGetSampleVertic: dispatch.explore.asyncGetSampleVertic, -}); - -interface IProps - extends ReturnType, - ReturnType, - FormComponentProps { - closeHandler: any; -} - -interface IState { - loading: boolean; -} - -class IdQuery extends React.Component { - constructor(props: IProps) { - super(props); - - this.state = { - loading: false, - }; - } - handleImport = () => { - this.props.form.validateFields(async (err, data) => { - if (!err) { - const { ids } = data; - const _ids = ids.trim().split('\n'); - this.props.asyncImportNodes({ ids: _ids }); - this.props.closeHandler(); - } - }); - }; - - handleFileImport = async ({ file }) => { - this.setState({ - loading: true, - }); - const ids = await readFileContent(file); - this.setState({ - loading: false, - }); - this.props.form.setFieldsValue({ - ids, - }); - }; - - resetValidator = () => { - const ids = this.props.form.getFieldValue('ids'); - this.props.form.resetFields(['ids']); - this.props.form.setFieldsValue({ ids }); - }; - - handleImportSample = async () => { - const { asyncGetSampleVertic, closeHandler } = this.props; - const { code } = await asyncGetSampleVertic(); - if (code === 0) { - closeHandler(); - } - }; - render() { - const { getFieldDecorator } = this.props.form; - const { loading } = this.state; - const { sampleLoading } = this.props; - return ( - -
-
- {intl.get('explore.idToBeQueried')} -
- - false} - onChange={this.handleFileImport} - showUploadList={false} - > - - -
-
-
- - {getFieldDecorator('ids', { - rules: nodeIdRulesFn(intl), - })( -