forked from ci4s/knowlegegraph-platform
v3.2.5 init
This commit is contained in:
parent
58e23becfa
commit
399a4e66a1
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": "commonjs",
|
||||
"targets": {
|
||||
"chrome": "58",
|
||||
"ie": "11"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel",
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "antd",
|
||||
"libraryDirectory": "es",
|
||||
"style": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
["@babel/plugin-proposal-class-properties",
|
||||
{
|
||||
"loose": true
|
||||
}
|
||||
],
|
||||
"@babel/plugin-transform-runtime",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
// commit message format:
|
||||
// [feat|fix|docs|refactor|test|chore|revert]: your message
|
||||
[
|
||||
'feat', // add function (添加功能)
|
||||
'mod', // modify changes (修改)
|
||||
'fix', // fix bugs
|
||||
'docs', // docs modify
|
||||
'refactor', // refactor (重构)
|
||||
'test', // test (测试)
|
||||
'chore', // other things like scaffold, ci/cd (其他诸如构建部署等修改)
|
||||
'revert', // revert commit
|
||||
],
|
||||
],
|
||||
'subject-full-stop': [0, 'never'],
|
||||
'subject-case': [0, 'never'],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
docs
|
||||
logs
|
||||
run
|
||||
scripts
|
||||
.git
|
||||
node_modules
|
||||
typings
|
||||
tmp
|
||||
.gitignore
|
||||
README.md
|
||||
docker/
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
module.exports = {
|
||||
extends: ['airbnb', 'prettier', 'prettier/react'],
|
||||
parser: 'babel-eslint',
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
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: './'
|
||||
}],
|
||||
'prettier/prettier': ['error', {
|
||||
singleQuote: true,
|
||||
semi: false
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Help us to improve this project
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug (__must be provided__)**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Your Environments (__must be provided__)**
|
||||
|
||||
* OS: Linux,Mac or Windows
|
||||
* Node-version: `node --version`
|
||||
* Studio-version: display in studio navBar
|
||||
|
||||
**How To Reproduce(__must be provided__)**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. Step 3
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Provide logs and configs, or any other context to trace the problem.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!--
|
||||
Thank you for contributing to **Nebula Graph Studio**!
|
||||
-->
|
||||
|
||||
### What's the bug and how to reproduce?
|
||||
<!-- Link the related issue if exists -->
|
||||
|
||||
### What's the expected behavior?
|
||||
<!-- Link the origin design if exists -->
|
||||
|
||||
### If possible,it's better to paste screeShots both before and after fixed.
|
||||
<!-- Screenshots will help us understand easily -->
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<!--
|
||||
Thank you for contributing to **Nebula Graph Studio**!
|
||||
-->
|
||||
|
||||
### What changes were proposed in this pull request?
|
||||
<!--
|
||||
Please clarify what changes you are proposing. The purpose of this section is to outline the changes and how this PR fixes the issue.
|
||||
If possible, please consider writing useful notes for better and faster reviews in your PR. See the examples below.
|
||||
1. If you refactor some codes with changing classes, showing the class hierarchy will help reviewers.
|
||||
2. If there is design documentation, please add the link.
|
||||
3. If there is a discussion in the mailing list, please add the link.
|
||||
-->
|
||||
|
||||
|
||||
### Why are the changes needed?
|
||||
<!--
|
||||
Please clarify why the changes are needed. For instance,
|
||||
1. If you propose a new feature, clarify the use case for a new feature.
|
||||
2. If you fix a bug, you can clarify why it is a bug.
|
||||
-->
|
||||
|
||||
### Does this PR introduce any user-facing change?
|
||||
<!--
|
||||
If yes, please clarify the previous behavior and the change this PR proposes,if possibel, paste screenshot.
|
||||
If no, write 'No'.
|
||||
-->
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
name: 'Build and upload'
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
oss_endpoint:
|
||||
required: true
|
||||
oss_id:
|
||||
required: true
|
||||
oss_secret:
|
||||
required: true
|
||||
oss_url:
|
||||
required: true
|
||||
ga_id:
|
||||
required: true
|
||||
jobs:
|
||||
package:
|
||||
name: build package
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- centos7
|
||||
- ubuntu1604
|
||||
container:
|
||||
image: vesoft/nebula-dev:${{ matrix.os }}
|
||||
steps:
|
||||
- uses: webiny/action-post-run@2.0.1
|
||||
with:
|
||||
run: sh -c "find . -mindepth 1 -delete"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
path: source/nebula-graph-studio
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17.0'
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '10'
|
||||
- name: Package
|
||||
run: bash ./source/nebula-graph-studio/scripts/pack.sh ${{ secrets.ga_id }} ${{ matrix.os }}
|
||||
- name: Upload to OSS
|
||||
run: bash ./source/nebula-graph-studio/scripts/upload.sh ${{ secrets.oss_endpoint }} ${{ secrets.oss_id }} ${{ secrets.oss_secret }} ${{ secrets.oss_url }} ${{ matrix.os }}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
name: Studio nightly Package
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: vesoft-inc/nebula-studio/.github/workflows/build.yml@master
|
||||
secrets:
|
||||
oss_endpoint: ${{ secrets.OSS_ENDPOINT }}
|
||||
oss_id: ${{ secrets.OSS_ID }}
|
||||
oss_secret: ${{ secrets.OSS_SECRET }}
|
||||
oss_url: ${{ secrets.OSS_TEST_URL }}
|
||||
ga_id: ${{ secrets.GA_ID }}
|
||||
docker-image:
|
||||
name: docker image build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout Github Action
|
||||
uses: actions/checkout@master
|
||||
-
|
||||
name: set track
|
||||
run: bash ./scripts/setEventTracking.sh ${{ secrets.GA_ID }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: vesoft/nebula-graph-studio:nightly
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
name: Studio Release
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: vesoft-inc/nebula-studio/.github/workflows/build.yml@master
|
||||
secrets:
|
||||
oss_endpoint: ${{ secrets.OSS_ENDPOINT }}
|
||||
oss_id: ${{ secrets.OSS_ID }}
|
||||
oss_secret: ${{ secrets.OSS_SECRET }}
|
||||
oss_url: ${{ secrets.OSS_URL }}
|
||||
ga_id: ${{ secrets.GA_ID }}
|
||||
docker-image:
|
||||
name: docker image build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout Github Action
|
||||
uses: actions/checkout@master
|
||||
-
|
||||
name: set track
|
||||
run: bash ./scripts/setEventTracking.sh ${{ secrets.GA_ID }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: vesoft/nebula-graph-studio:v3.2.5
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
logs/
|
||||
npm-debug.log
|
||||
node_modules/
|
||||
.idea/
|
||||
.DS_Store
|
||||
.vscode
|
||||
*.swp
|
||||
*.lock
|
||||
*.js
|
||||
*.map
|
||||
.github/workflows/nodejs.yml
|
||||
|
||||
app/**/*.js
|
||||
config/**/*.js
|
||||
app/**/*.map
|
||||
config/**/*.map
|
||||
dist/
|
||||
|
||||
!.eslintrc.js
|
||||
!.prettierrc.js
|
||||
!.commitlint.js
|
||||
!app/**/*.js
|
||||
tmp
|
||||
server/data
|
||||
assets/
|
||||
bin/
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
endOfLine: 'lf',
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-recommended"],
|
||||
"rules": {
|
||||
"block-no-empty": null,
|
||||
"color-no-invalid-hex": true,
|
||||
"declaration-colon-space-after": "always",
|
||||
"indentation": ["tab", {
|
||||
"except": ["value"]
|
||||
}],
|
||||
"max-empty-lines": 2,
|
||||
"unit-whitelist": ["em", "rem", "%", "s", "px", "deg"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
## Nebula Graph Studio Contributing Guide
|
||||
Thank you for being interested in contributing to Nebula Graph Studio. Before submitting your contribution, please make sure to take a moment and read through the following guidelines
|
||||
|
||||
A high level overview of tools used:
|
||||
|
||||
- TypeScript as the development language
|
||||
- Webpack for bundling
|
||||
- Egg.js for api proxy
|
||||
- Prettier and styleLint for code formating
|
||||
|
||||
Commit information must contain one item in list below:
|
||||
|
||||
'feat', // add function (添加功能)
|
||||
'mod', // modify changes (修改)
|
||||
'fix', // fix bugs
|
||||
'docs', // docs modify
|
||||
'refactor', // refactor (重构)
|
||||
'test', // test (测试)
|
||||
'chore', // other things like scaffold, ci/cd (其他诸如构建部署等修改)
|
||||
'revert', // revert commit
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Nebula Graph Studio Tar Package Deploy Guide
|
||||
|
||||
## Environment
|
||||
- Linux
|
||||
|
||||
## Download
|
||||
`wget https://oss-cdn.nebula-graph.com.cn/nebula-graph-studio/nebula-graph-studio-${version}.x86_64.tar.gz`
|
||||
|
||||
## Unpress
|
||||
`tar -xvf nebula-graph-studio-${version}.x86_64.tar.gz`
|
||||
|
||||
## Quick Start
|
||||
1. Start Service
|
||||
```bash
|
||||
nohup ./server
|
||||
```
|
||||
- Service address: http://127.0.0.1:7001
|
||||
|
||||
You can modify the port in example-config.yaml in the config directory
|
||||
|
||||
2. Open Nebula Graph Studio in browser
|
||||
url: http://{{ip}}:7001
|
||||
|
||||
|
||||
## Stop Service
|
||||
Using `kill pid` :
|
||||
|
||||
```bash
|
||||
$ kill $(lsof -t -i :7001)
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
FROM node:12-alpine as nodebuilder
|
||||
LABEL stage=nodebuilder
|
||||
# Set the working directory to /app
|
||||
WORKDIR /web
|
||||
# Copy the current directory contents into the container at /web
|
||||
COPY package.json /web/
|
||||
COPY package-lock.json /web/
|
||||
COPY .npmrc /web/
|
||||
|
||||
# Install any needed packages
|
||||
RUN npm install
|
||||
COPY . /web/
|
||||
|
||||
# build and remove front source code
|
||||
ENV NODE_OPTIONS=--max_old_space_size=2048
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:alpine AS gobuilder
|
||||
|
||||
LABEL stage=gobuilder
|
||||
|
||||
ENV CGO_ENABLED 1
|
||||
ENV GOOS linux
|
||||
ENV GOPROXY https://goproxy.cn,direct
|
||||
|
||||
WORKDIR /server
|
||||
|
||||
COPY server .
|
||||
COPY --from=nodebuilder /web/dist/ /server/assets
|
||||
RUN go mod download
|
||||
RUN apk add build-base
|
||||
RUN go build -ldflags="-s -w" -o /server/server /server/main.go
|
||||
|
||||
FROM alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=gobuilder /server/server /app/server
|
||||
COPY --from=gobuilder /server/config /app/config/
|
||||
|
||||
EXPOSE 7001
|
||||
|
||||
CMD ["./server"]
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
54
README.md
54
README.md
|
|
@ -1,2 +1,54 @@
|
|||
# knowlegegraph-platform
|
||||
# Nebula Graph Studio
|
||||
Nebula Graph Studio (Studio for short) is a web-based visualization tool for Nebula Graph. With Studio, you can create a graph schema, import data, edit nGQL statements for data queries, and explore graphs.
|
||||

|
||||
|
||||
## Architecture
|
||||

|
||||
|
||||
## Development Quick Start
|
||||
|
||||
### Set up nebula-graph-studio
|
||||
```
|
||||
$ npm install
|
||||
$ npm run dev
|
||||
```
|
||||
### Set up go-server
|
||||
```
|
||||
// remove default port 7001 in config/example-config.yaml first
|
||||
$ cd server
|
||||
$ go build -o server
|
||||
$ ./server &
|
||||
```
|
||||
|
||||
## Production Deploy
|
||||
|
||||
### 1. Build Web
|
||||
```
|
||||
$ npm run install
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
### 1. Build Web
|
||||
```
|
||||
$ mv dist server/assets
|
||||
$ cd server
|
||||
$ go build -o server
|
||||
```
|
||||
|
||||
### 3. Start
|
||||
```
|
||||
$ ./server &
|
||||
```
|
||||
|
||||
### 4. Stop Server
|
||||
Use when you want shutdown the web app
|
||||
```
|
||||
kill -9 $(lsof -t -i :7001)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
[中文](https://docs.nebula-graph.com.cn/2.5.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/)
|
||||
[ENGLISH](https://https://docs.nebula-graph.io/2.5.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/)
|
||||
|
||||
## Contributing
|
||||
Contributions are warmly welcomed and greatly appreciated. Please see [Guide Docs](https://github.com/vesoft-inc-private/nebula-graph-studio/blob/master/CONTRIBUTING.md)
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
@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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
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<typeof mapDispatch>,
|
||||
ReturnType<typeof mapState> {}
|
||||
|
||||
class App extends React.Component<IProps, IState> {
|
||||
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 (
|
||||
<>
|
||||
<LanguageContext.Provider
|
||||
value={{
|
||||
currentLocale: this.currentLocale,
|
||||
toggleLanguage: this.toggleLanguage,
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<Layout className="nebula-graph-studio">
|
||||
<Header>
|
||||
<div className="studio-logo">
|
||||
<img src={logo} />
|
||||
</div>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
selectedKeys={[activeMenu]}
|
||||
onClick={this.handleMenuClick as any}
|
||||
>
|
||||
<Menu.Item key="schema">
|
||||
<Link
|
||||
to="/schema"
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_schema"
|
||||
data-track-label="from_navigation"
|
||||
>
|
||||
<IconFont type="iconnav-model" />
|
||||
{intl.get('common.schema')}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="import">
|
||||
<Link
|
||||
to="/import"
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_import"
|
||||
data-track-label="from_navigation"
|
||||
>
|
||||
<Icon type="import" />
|
||||
{intl.get('common.import')}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="explore">
|
||||
<Link
|
||||
to="/explore"
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_explore"
|
||||
data-track-label="from_navigation"
|
||||
>
|
||||
<Icon type="branches" />
|
||||
{intl.get('common.explore')}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="console">
|
||||
<Link
|
||||
to="/console"
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_console"
|
||||
data-track-label="from_navigation"
|
||||
>
|
||||
<Icon type="code" />
|
||||
{intl.get('common.console')}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<div className="lang-select">
|
||||
<span>{intl.get('common.languageSelect')}: </span>
|
||||
<Select
|
||||
value={this.currentLocale}
|
||||
onChange={this.toggleLanguage}
|
||||
>
|
||||
{Object.keys(INTL_LOCALE_SELECT).map(locale => (
|
||||
<Option
|
||||
key={locale}
|
||||
value={INTL_LOCALE_SELECT[locale].NAME}
|
||||
>
|
||||
{INTL_LOCALE_SELECT[locale].TEXT}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<Dropdown
|
||||
className="setting"
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item>
|
||||
<a onClick={this.handleClear}>
|
||||
<Icon type="logout" />
|
||||
{intl.get('configServer.clear')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a className="ant-dropdown-link">
|
||||
{intl.get('common.setting')} <Icon type="down" />
|
||||
</a>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
className="help"
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item onClick={() => trackPageView('/user-mannual')}>
|
||||
<a href={mannualHref} target="_blank">
|
||||
<Icon type="compass" />
|
||||
{intl.get('common.use')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => trackPageView('/nebula-doc')}>
|
||||
<a href={nGQLHref} target="_blank">
|
||||
<Icon type="star" />
|
||||
nGQL
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<a href={intl.get('common.forumLink')} target="_blank">
|
||||
<Icon type="question" />
|
||||
{intl.get('common.forum')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a className="ant-dropdown-link">
|
||||
{intl.get('common.help')} <Icon type="down" />
|
||||
</a>
|
||||
</Dropdown>
|
||||
<div
|
||||
className="github-star"
|
||||
data-track-category="navigation"
|
||||
data-track-action="star_github"
|
||||
data-track-label="from_navigation"
|
||||
>
|
||||
<a
|
||||
className="github-button"
|
||||
href="https://github.com/vesoft-inc/nebula"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star vesoft-inc/nebula on GitHub"
|
||||
>
|
||||
Star
|
||||
</a>
|
||||
</div>
|
||||
{appVersion && (
|
||||
<Dropdown
|
||||
className="version"
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item>
|
||||
<a
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_changelog"
|
||||
href={versionLogHref}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon type="tags" />
|
||||
{intl.get('common.release')}
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a>
|
||||
v{appVersion}
|
||||
<Icon type="down" />
|
||||
</a>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Header>
|
||||
<Content>
|
||||
<Switch>
|
||||
<PrivateRoute
|
||||
path="/schema"
|
||||
exact={true}
|
||||
component={Schema}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/space/create"
|
||||
exact={true}
|
||||
component={CreateSpace}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/space/:space/:type?/:action?"
|
||||
component={SpaceConfig}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/import"
|
||||
exact={true}
|
||||
component={Import}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/explore"
|
||||
exact={true}
|
||||
component={Explore}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path="/console"
|
||||
exact={true}
|
||||
component={Console}
|
||||
/>
|
||||
<Route
|
||||
path="/connect-server"
|
||||
exact={true}
|
||||
component={ConfigServer}
|
||||
/>
|
||||
<Redirect to="/explore" />
|
||||
</Switch>
|
||||
</Content>
|
||||
</Layout>
|
||||
)}
|
||||
</LanguageContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapState, mapDispatch)(hot(App)));
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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 ? (
|
||||
<Route {...rest} render={props => <Component {...props} />} />
|
||||
) : (
|
||||
<Route render={render} {...rest} />
|
||||
);
|
||||
} else {
|
||||
return <Redirect to="/connect-server" {...rest} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapState, mapDispatch)(PrivateRoute);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@navHeight: 64px;
|
||||
@controlHeight: 65px;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
.panel-btn-item,
|
||||
.menu-color {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-menu-icon {
|
||||
svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.panel-disabled {
|
||||
cursor: not-allowed;
|
||||
color: #d9d9d9;
|
||||
|
||||
svg {
|
||||
fill: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-actived {
|
||||
color: #0091ff;
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { Icon, Tooltip } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import IconFont from '#app/components/Icon';
|
||||
|
||||
import './index.less';
|
||||
interface IBtnProps {
|
||||
disabled?: boolean;
|
||||
action?: () => void;
|
||||
icon?: string;
|
||||
iconfont?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
active?: boolean;
|
||||
component?: any;
|
||||
trackCategory?: string;
|
||||
trackAction?: string;
|
||||
trackLabel?: string;
|
||||
}
|
||||
|
||||
interface IMenuButton extends IBtnProps {
|
||||
tips?: string;
|
||||
}
|
||||
const CustomizeButton = (props: IBtnProps) => {
|
||||
const {
|
||||
icon,
|
||||
iconfont,
|
||||
action,
|
||||
disabled,
|
||||
title,
|
||||
active,
|
||||
component,
|
||||
className,
|
||||
trackCategory,
|
||||
trackAction,
|
||||
trackLabel,
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
'menu-color': className,
|
||||
'panel-btn-item': !className,
|
||||
'panel-disabled': disabled,
|
||||
'panel-actived': active,
|
||||
})}
|
||||
onClick={!disabled && action ? action : undefined}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
type={icon}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
className="panel-menu-icon"
|
||||
/>
|
||||
)}
|
||||
{iconfont && (
|
||||
<IconFont
|
||||
type={iconfont}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
className="panel-menu-icon"
|
||||
/>
|
||||
)}
|
||||
{component}
|
||||
{title && <span>{title}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// antd Tooltip can't wrap custom component
|
||||
const CustomizeTooltipBtn = (props: IMenuButton) => {
|
||||
const {
|
||||
icon,
|
||||
iconfont,
|
||||
action,
|
||||
disabled,
|
||||
active,
|
||||
tips,
|
||||
component,
|
||||
trackAction,
|
||||
trackCategory,
|
||||
trackLabel,
|
||||
} = props;
|
||||
return (
|
||||
<Tooltip title={tips}>
|
||||
{icon ? (
|
||||
<Icon
|
||||
type={icon}
|
||||
className={classnames('panel-menu-icon', {
|
||||
'panel-disabled': disabled,
|
||||
'panel-actived': active,
|
||||
})}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
onClick={!disabled ? action : undefined}
|
||||
/>
|
||||
) : iconfont ? (
|
||||
<IconFont
|
||||
type={iconfont}
|
||||
className={classnames('panel-menu-icon', {
|
||||
'panel-disabled': disabled,
|
||||
'panel-actived': active,
|
||||
})}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
onClick={!disabled ? action : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={classnames({
|
||||
'panel-disabled': disabled,
|
||||
'panel-actived': active,
|
||||
})}
|
||||
onClick={!disabled && action ? action : undefined}
|
||||
data-track-category={trackCategory}
|
||||
data-track-action={trackAction}
|
||||
data-track-label={trackLabel}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
class MenuButton extends React.PureComponent<IMenuButton> {
|
||||
render() {
|
||||
const { tips, ...rest } = this.props;
|
||||
if (tips) {
|
||||
return <CustomizeTooltipBtn {...this.props} />;
|
||||
} else {
|
||||
return <CustomizeButton {...rest} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default MenuButton;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
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<IProps> {
|
||||
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 ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
className="csv-select-index"
|
||||
onClick={() => this.handleMapping(textIndex)}
|
||||
>{`column ${textIndex}`}</Button>
|
||||
<Tooltip
|
||||
title={intl.get('import.setMappingTip', {
|
||||
prop,
|
||||
index: textIndex,
|
||||
})}
|
||||
>
|
||||
<Icon type="info-circle" />
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
`column ${textIndex}`
|
||||
),
|
||||
dataIndex: index,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="link" onClick={this.handleLinkClick}>
|
||||
{this.props.children}
|
||||
</Button>
|
||||
<Modal
|
||||
handlerRef={handler => {
|
||||
this.modalHandler = handler;
|
||||
}}
|
||||
footer={false}
|
||||
width={1000}
|
||||
>
|
||||
<div className="csv-preview">
|
||||
<Table
|
||||
bordered={true}
|
||||
dataSource={content}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey={(_, index) => index.toString()}
|
||||
/>
|
||||
<div className="operation">
|
||||
{onMapping && (
|
||||
<Button onClick={() => this.handleMapping(null)}>
|
||||
{intl.get('import.ignore')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CSVPreviewLink;
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import CodeMirror from 'codemirror';
|
||||
import 'codemirror/addon/comment/comment';
|
||||
import 'codemirror/addon/display/autorefresh';
|
||||
import 'codemirror/addon/edit/matchbrackets';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
import 'codemirror/addon/hint/show-hint.css';
|
||||
import 'codemirror/keymap/sublime';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/mode/meta';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import React from 'react';
|
||||
|
||||
import { ban, keyWords, maxLineNum, operators } from '#app/config/nebulaQL';
|
||||
|
||||
import './Codemirror.less';
|
||||
|
||||
interface IProps {
|
||||
options?: object;
|
||||
value: string;
|
||||
ref?: any;
|
||||
width?: string;
|
||||
height?: string;
|
||||
onShiftEnter?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onBlur?: (value: string) => void;
|
||||
onChangeLine?: () => void;
|
||||
}
|
||||
|
||||
export default class ReactCodeMirror extends React.PureComponent<IProps, any> {
|
||||
codemirror;
|
||||
editor;
|
||||
textarea;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
public componentDidMount() {
|
||||
CodeMirror.defineMode('nebula', () => {
|
||||
return {
|
||||
token: stream => {
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
stream.eatWhile(/[\$:\w\u4e00-\u9fa5]/);
|
||||
const cur = stream.current();
|
||||
if (keyWords.some(item => item === cur)) {
|
||||
return 'keyword';
|
||||
} else if (operators.some(item => item === cur)) {
|
||||
return 'def';
|
||||
} else if (ban.some(item => item === cur)) {
|
||||
return 'error';
|
||||
}
|
||||
stream.next();
|
||||
},
|
||||
// blockCommentStart: '/*',
|
||||
// blockCommentEnd: '*/',
|
||||
// lineComment: '//' ? '#' : '--',
|
||||
// closeBrackets: '()[]{}\'\'""``',
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper('hint', 'nebula', cm => {
|
||||
const cur = cm.getCursor();
|
||||
const token = cm.getTokenAt(cur);
|
||||
const str = token.string;
|
||||
const start = token.start;
|
||||
const end = cur.ch;
|
||||
|
||||
if (str === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = [...keyWords, ...operators, ...ban].filter(item => {
|
||||
return item.indexOf(str) === 0;
|
||||
});
|
||||
|
||||
if (list.length) {
|
||||
return {
|
||||
list,
|
||||
from: CodeMirror.Pos(cur.line, start),
|
||||
to: CodeMirror.Pos(cur.line, end),
|
||||
};
|
||||
}
|
||||
});
|
||||
this.renderCodeMirror();
|
||||
}
|
||||
renderCodeMirror() {
|
||||
// parameters of the combined
|
||||
const options = {
|
||||
tabSize: 2,
|
||||
fontSize: '14px',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
lineWrapping: true,
|
||||
// show number of rows
|
||||
lineNumbers: true,
|
||||
fullScreen: true,
|
||||
mode: 'nebula',
|
||||
...this.props.options,
|
||||
};
|
||||
this.editor = CodeMirror.fromTextArea(this.textarea, options);
|
||||
// Getting CodeMirror is used to get some of these constants
|
||||
this.codemirror = CodeMirror;
|
||||
// event
|
||||
this.editor.on('change', this.codemirrorValueChange);
|
||||
this.editor.on('keydown', this.keydown);
|
||||
this.editor.on('blur', this.blur);
|
||||
const { value, width, height } = this.props;
|
||||
this.editor.setValue(value || '');
|
||||
if (width || height) {
|
||||
// set size
|
||||
this.editor.setSize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
blur = instance => {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(instance.doc.getValue());
|
||||
}
|
||||
};
|
||||
|
||||
keydown = (_, change) => {
|
||||
if (change.shiftKey === true && change.keyCode === 13) {
|
||||
if (this.props.onShiftEnter) {
|
||||
this.props.onShiftEnter();
|
||||
}
|
||||
change.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
codemirrorValueChange = (doc, change) => {
|
||||
if (change.origin !== 'setValue') {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(doc.getValue());
|
||||
}
|
||||
}
|
||||
if (change.origin === '+input') {
|
||||
CodeMirror.commands.autocomplete(this.editor, null, {
|
||||
completeSingle: false,
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.props.onChangeLine &&
|
||||
(change.origin === '+delete' || change.origin === '+input')
|
||||
) {
|
||||
this.props.onChangeLine();
|
||||
}
|
||||
};
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
const { options, value } = nextProps;
|
||||
await this.setOptions(options);
|
||||
if (value !== this.editor.getValue()) {
|
||||
this.editor.setValue(value || '');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
async setOptions(options) {
|
||||
if (typeof options === 'object') {
|
||||
const mode = CodeMirror.findModeByName(options.mode);
|
||||
if (mode && mode.mode) {
|
||||
await import(`codemirror/mode/${mode.mode}/${mode.mode}.js`);
|
||||
}
|
||||
if (mode) {
|
||||
options.mode = mode.mime;
|
||||
}
|
||||
Object.keys(options).forEach(name => {
|
||||
if (options[name] && JSON.stringify(options[name])) {
|
||||
this.editor.setOption(name, options[name]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.toTextArea();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<textarea
|
||||
ref={instance => {
|
||||
this.textarea = instance;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.CodeMirror-wrap pre.CodeMirror-line,
|
||||
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
resize: vertical;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.popover-color {
|
||||
.ant-popover-inner-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.custom-picker {
|
||||
border: none !important;
|
||||
box-shadow: initial !important;
|
||||
border-radius: 0 !important;
|
||||
|
||||
> div {
|
||||
padding: 0 !important;
|
||||
|
||||
> span > div {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import { TwitterPicker } from 'react-color';
|
||||
|
||||
import { COLOR_PICK_LIST } from '#app/config/explore';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
children?: any;
|
||||
handleChangeColorComplete?: (color: string) => void;
|
||||
handleChange?: (color: string) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
class ColorPicker extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = color => {
|
||||
if (this.props.handleChange) {
|
||||
const { hex: _color } = color;
|
||||
this.props.handleChange(_color);
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeComplete = (color, _event) => {
|
||||
if (this.props.handleChangeColorComplete) {
|
||||
const { hex: _color } = color;
|
||||
this.props.handleChangeColorComplete(_color);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="popover-color">
|
||||
<TwitterPicker
|
||||
width="240px"
|
||||
className="custom-picker"
|
||||
onChange={this.handleChange}
|
||||
onChangeComplete={this.handleChangeComplete}
|
||||
colors={COLOR_PICK_LIST}
|
||||
triangle="hide"
|
||||
/>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorPicker;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.config-server-form {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
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<typeof mapState>, FormComponentProps {
|
||||
onConfig: (form: WrappedFormUtils) => void;
|
||||
}
|
||||
|
||||
const ConfigServerForm = Form.create<IProps>()((props: IProps) => {
|
||||
const { onConfig, loading } = props;
|
||||
const { getFieldDecorator } = props.form;
|
||||
return (
|
||||
<Form
|
||||
layout="horizontal"
|
||||
{...fomrItemLayout}
|
||||
className="config-server-form"
|
||||
>
|
||||
<FormItem label={intl.get('configServer.host')}>
|
||||
{getFieldDecorator('host', {
|
||||
rules: hostRulesFn(intl),
|
||||
})(<Input placeholder="GraphD Host: Port" />)}
|
||||
</FormItem>
|
||||
<FormItem label={intl.get('configServer.username')}>
|
||||
{getFieldDecorator('username', {
|
||||
rules: usernameRulesFn(intl),
|
||||
})(<Input />)}
|
||||
</FormItem>
|
||||
<FormItem label={intl.get('configServer.password')}>
|
||||
{getFieldDecorator('password', {
|
||||
rules: passwordRulesFn(intl),
|
||||
})(<Input.Password />)}
|
||||
</FormItem>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => onConfig(props.form)}
|
||||
loading={!!loading}
|
||||
>
|
||||
{intl.get('configServer.connect')}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
export default connect(mapState)(ConfigServerForm);
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
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<typeof mapState> {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
tagType: 'vertex' | 'edge';
|
||||
}
|
||||
|
||||
class DisplayComponent extends React.PureComponent<IProps, IState> {
|
||||
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<IState, keyof IState>);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="display-expand">
|
||||
<div className="header">
|
||||
<Tabs onChange={this.handleChangeType} defaultActiveKey={tagType}>
|
||||
<TabPane
|
||||
tab={intl.get('import.vertexText') + `(${selectVertexes.length})`}
|
||||
key="vertex"
|
||||
/>
|
||||
<TabPane
|
||||
tab={intl.get('import.edgeText') + `(${selectEdges.length})`}
|
||||
key="edge"
|
||||
/>
|
||||
</Tabs>
|
||||
<div
|
||||
className={data.length > 0 ? 'btn-view' : 'btn-disabled'}
|
||||
onClick={data.length > 0 ? this.showModal : undefined}
|
||||
data-track-category="explore"
|
||||
data-track-action="select_info_modal_view"
|
||||
>
|
||||
<Tooltip title={intl.get('explore.viewDetails')}>
|
||||
<IconFont type="iconstudio-window" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content">
|
||||
{data.length > 0 &&
|
||||
data.map((item: INode | IPath, index) => (
|
||||
<ExpandItem
|
||||
key={item.uuid}
|
||||
data={this.flattenProps(item)}
|
||||
title={`${tagType} ${index + 1}`}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{data.length === 0 && (
|
||||
<div className="empty-tip">
|
||||
<IconFont type="iconDefault-image-left" />
|
||||
<span>{intl.get('common.noSelectedData')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button
|
||||
disabled={data.length === 0}
|
||||
data-track-category="explore"
|
||||
data-track-action="export_csv"
|
||||
data-track-label="from_left_sider"
|
||||
onClick={
|
||||
data.length > 0
|
||||
? _.debounce(this.exportDataToCSV, 300)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<IconFont type="iconstudio-exportcsv" />
|
||||
{tagType === 'vertex'
|
||||
? intl.get('common.exportSelectVertexes')
|
||||
: intl.get('common.exportSelectEdges')}
|
||||
</Button>
|
||||
<IconFont
|
||||
type="iconstudio-indentright"
|
||||
data-track-category="explore"
|
||||
data-track-action="display_sider_close"
|
||||
onClick={close}
|
||||
/>
|
||||
</div>
|
||||
<SelectedGraphDetailShowModal
|
||||
handlerRef={modal => (this.SelectedGraphDetailShowModal = modal)}
|
||||
vertexes={selectVertexes}
|
||||
edges={selectEdges}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState)(DisplayComponent);
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
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<IProps, IState> {
|
||||
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 (
|
||||
<div className="display-row-item">
|
||||
<div
|
||||
className={classnames('item-header', 'row', { active: expandedAll })}
|
||||
onClick={this.toggleExpandAll}
|
||||
>
|
||||
{expandedAll ? <Icon type="down" /> : <Icon type="right" />}
|
||||
<span className="display-header-title">{title}</span>
|
||||
</div>
|
||||
{expandedAll && (
|
||||
<>
|
||||
{data.slice(0, EXPAND_NUM).map(item => (
|
||||
<div className="item-content" key={item.key}>
|
||||
<span className="item-key">{item.key} :</span>
|
||||
<span className="item-value">{this.handleShowValue(item)}</span>
|
||||
</div>
|
||||
))}
|
||||
{needExpandRest && (
|
||||
<>
|
||||
{expandedRestInfo ? (
|
||||
<>
|
||||
{data.slice(EXPAND_NUM).map(item => (
|
||||
<div className="item-content" key={item.key}>
|
||||
<span className="item-key">{item.key}</span>
|
||||
<span className="item-value">
|
||||
{this.handleShowValue(item)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="item-operation row"
|
||||
onClick={this.toggleExpandRest}
|
||||
>
|
||||
<IconFont type="iconstudio-seletup" />
|
||||
<span>{intl.get('explore.collapseItem')}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="item-operation row"
|
||||
onClick={this.toggleExpandRest}
|
||||
>
|
||||
<IconFont type="iconstudio-seletexpand" />
|
||||
<span>{intl.get('explore.expandItem')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RowItem;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
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<IProps, IState> {
|
||||
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 (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<pre>
|
||||
{JSONBigint.stringify(
|
||||
JSONBigint.parse(record),
|
||||
(_, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/\u0000+$/, '');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
{record}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
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 (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<pre>
|
||||
{JSONBigint.stringify(
|
||||
JSONBigint.parse(record),
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
{record}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Modal
|
||||
className="modal-show-selected"
|
||||
width="70%"
|
||||
handlerRef={handler => (this.modalHandler = handler)}
|
||||
title={
|
||||
<Tabs onChange={this.handleChangeType} defaultActiveKey={tagType}>
|
||||
<TabPane tab={intl.get('explore.selectedVertexes')} key="vertex" />
|
||||
<TabPane tab={intl.get('explore.selectedEdges')} key="edge" />
|
||||
</Tabs>
|
||||
}
|
||||
footer={null}
|
||||
>
|
||||
<div className="operation">
|
||||
<span>
|
||||
{intl.get('common.total')}: {data.length}
|
||||
</span>
|
||||
<div className="btns">
|
||||
<Button
|
||||
type="primary"
|
||||
data-track-category="explore"
|
||||
data-track-action="export_csv"
|
||||
data-track-label="from_info_modal"
|
||||
onClick={this.handleExportToCSV}
|
||||
>
|
||||
{intl.get('explore.exportToCSV')}
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder="person.name > xx"
|
||||
value={searchValue}
|
||||
onChange={this.handleUpdateValue}
|
||||
onSearch={this.handleSearch}
|
||||
/>
|
||||
<Instruction description={intl.get('explore.searchTip')} />
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey={(_, index) => index.toString()}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SelectedGraphDetailShowModal;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch> {
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
class DisplayPanel extends React.PureComponent<IProps> {
|
||||
handleClose = () => {
|
||||
this.props.toggleExpand({
|
||||
showDisplayPanel: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.props.toggleExpand({
|
||||
showDisplayPanel: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentSpace,
|
||||
selectVertexes,
|
||||
selectEdges,
|
||||
showDisplayPanel,
|
||||
} = this.props;
|
||||
if (currentSpace) {
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
visible={showDisplayPanel}
|
||||
className="display-drawer"
|
||||
width="290"
|
||||
onClose={this.handleClose}
|
||||
closable={false}
|
||||
getContainer={false}
|
||||
mask={false}
|
||||
placement="left"
|
||||
>
|
||||
<Expand close={this.handleClose} />
|
||||
</Drawer>
|
||||
{!showDisplayPanel && (
|
||||
<div className="display-sider">
|
||||
<div
|
||||
className="display-label"
|
||||
data-track-category="explore"
|
||||
data-track-action="display_sider_open"
|
||||
onClick={this.handleOpen}
|
||||
>
|
||||
<IconFont type="iconstudio-vertex" />
|
||||
{selectVertexes.length}
|
||||
</div>
|
||||
<div
|
||||
className="display-label"
|
||||
data-track-category="explore"
|
||||
data-track-action="display_sider_open"
|
||||
onClick={this.handleOpen}
|
||||
>
|
||||
<IconFont type="iconstudio-edge" />
|
||||
{selectEdges.length}
|
||||
</div>
|
||||
<div className="sider-footer">
|
||||
<IconFont
|
||||
type="iconstudio-indentleft"
|
||||
className="icon-collapse"
|
||||
data-track-category="explore"
|
||||
data-track-action="display_sider_open"
|
||||
onClick={this.handleOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default connect(mapState, mapDispatch)(DisplayPanel);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
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<IProps> {
|
||||
handleAddFilters = () => {
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.props.onConfirm(values);
|
||||
}
|
||||
});
|
||||
};
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<div className="form-add-filter">
|
||||
<Form hideRequiredMark={true}>
|
||||
<Form.Item label={intl.get('common.field')}>
|
||||
{getFieldDecorator(`field`, {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input placeholder="prop1" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.operator')}>
|
||||
{getFieldDecorator(`operator`, {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input placeholder="==" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.value')}>
|
||||
{getFieldDecorator(`value`, {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input placeholder="value" />)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="popover-footer">
|
||||
<Button onClick={this.handleAddFilters} type="primary">
|
||||
{intl.get('common.confirm')}
|
||||
</Button>
|
||||
<Button onClick={this.props.onCancel}>
|
||||
{intl.get('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({
|
||||
mapPropsToFields(_props: IProps) {
|
||||
return {
|
||||
onConfirm: Form.createFormField({
|
||||
onConfirm: _props.onConfirm,
|
||||
}),
|
||||
onCancel: Form.createFormField({
|
||||
onCancel: _props.onCancel,
|
||||
}),
|
||||
};
|
||||
},
|
||||
})(AddFilterForm);
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch> {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface IFilter {
|
||||
expression: string;
|
||||
relation?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
filters: IFilter[];
|
||||
visible: boolean;
|
||||
customColor: string;
|
||||
customIcon: string;
|
||||
}
|
||||
|
||||
class Expand extends React.Component<IProps, IState> {
|
||||
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) => (
|
||||
<div key={index} className="form-item">
|
||||
{index > 0 && (
|
||||
<Form.Item>
|
||||
<Select
|
||||
value={item.relation}
|
||||
onChange={value => this.handleUpdateFilter(value, index)}
|
||||
className="select-relation"
|
||||
dropdownClassName="select-relation-dropdown"
|
||||
size="small"
|
||||
>
|
||||
{RELATION_OPERATORS.map(item => (
|
||||
<Option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Tag
|
||||
className="tag-expression"
|
||||
closable={true}
|
||||
onClose={_ => this.handleDeleteFilter(index)}
|
||||
>
|
||||
{item.expression}
|
||||
</Tag>
|
||||
</Form.Item>
|
||||
</div>
|
||||
));
|
||||
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 (
|
||||
<div className="graph-expand">
|
||||
<div className="expand-config">
|
||||
<Form colon={false}>
|
||||
<Form.Item label={intl.get('common.edge')}>
|
||||
{getFieldDecorator('edgeTypes', {
|
||||
initialValue:
|
||||
rules.edgeTypes && rules.edgeTypes.length > 0
|
||||
? rules.edgeTypes
|
||||
: edgeTypes,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Edge Type is required',
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select mode="multiple" onChange={this.handleUpdateRules}>
|
||||
{edgeTypes.map(e => (
|
||||
<Option value={e} key={e}>
|
||||
{e}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.direction')}>
|
||||
{getFieldDecorator('edgeDirection', {
|
||||
initialValue: rules.edgeDirection || 'outgoing',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select onChange={this.handleUpdateRules}>
|
||||
<Option value="outgoing">
|
||||
{intl.get('explore.outgoing')}
|
||||
</Option>
|
||||
<Option value="incoming">
|
||||
{intl.get('explore.incoming')}
|
||||
</Option>
|
||||
<Option value="bidirect">
|
||||
{intl.get('explore.bidirect')}
|
||||
</Option>
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={intl.get('explore.steps')}
|
||||
className="select-step-type"
|
||||
>
|
||||
{getFieldDecorator('stepsType', {
|
||||
initialValue: rules.stepsType || 'single',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Radio.Group onChange={this.handleUpdateRules}>
|
||||
<Radio value="single">{intl.get('explore.singleStep')}</Radio>
|
||||
<Radio value="range">{intl.get('explore.rangeStep')}</Radio>
|
||||
</Radio.Group>,
|
||||
)}
|
||||
</Form.Item>
|
||||
{stepsType === 'single' && (
|
||||
<Form.Item className="input-step">
|
||||
{getFieldDecorator('step', {
|
||||
initialValue: rules.step || '1',
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input type="number" onChange={this.handleUpdateRules} />)}
|
||||
</Form.Item>
|
||||
)}
|
||||
{stepsType === 'range' && (
|
||||
<div className="input-step">
|
||||
<Form.Item>
|
||||
{getFieldDecorator('minStep', {
|
||||
initialValue: rules.minStep || '',
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input type="number" onChange={this.handleUpdateRules} />)}
|
||||
</Form.Item>
|
||||
-
|
||||
<Form.Item>
|
||||
{getFieldDecorator('maxStep', {
|
||||
initialValue: rules.maxStep || '',
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input type="number" onChange={this.handleUpdateRules} />)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item label={intl.get('explore.vertexStyle')}>
|
||||
{getFieldDecorator('vertexStyle', {
|
||||
initialValue: rules.vertexStyle || 'colorGroupByTag',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Radio.Group onChange={this.handleUpdateRules}>
|
||||
<Radio value="colorGroupByTag">
|
||||
{intl.get('explore.colorGroupByTag')}
|
||||
</Radio>
|
||||
<Radio value="custom">
|
||||
{intl.get('explore.customStyle')}
|
||||
</Radio>
|
||||
<VertexStyleSet
|
||||
handleChangeColorComplete={this.handleCustomColor}
|
||||
handleChangeIconComplete={this.handleCustomIcon}
|
||||
icon={customIcon}
|
||||
color={customColor}
|
||||
/>
|
||||
</Radio.Group>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.quantityLimit')}>
|
||||
{getFieldDecorator('quantityLimit', {
|
||||
initialValue: rules.quantityLimit || 100,
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})(<Input type="number" onChange={this.handleUpdateRules} />)}
|
||||
</Form.Item>
|
||||
<div className="filter-component">
|
||||
<div className="filter-header">
|
||||
<span>{intl.get('explore.filter')}</span>
|
||||
<div
|
||||
className="btn-reset"
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_filter_reset"
|
||||
onClick={this.handleResetFilters}
|
||||
>
|
||||
<IconFont type="iconstudio-remake" />
|
||||
<span>{intl.get('import.reset')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{fieldTable}
|
||||
<Popover
|
||||
content={
|
||||
<AddFilterForm
|
||||
onConfirm={this.handleAddFilter}
|
||||
onCancel={this.hide}
|
||||
/>
|
||||
}
|
||||
visible={this.state.visible}
|
||||
onVisibleChange={this.handleVisibleChange}
|
||||
trigger="click"
|
||||
>
|
||||
<Button
|
||||
className="btn-add-filter"
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_filter_add"
|
||||
icon="plus"
|
||||
type="link"
|
||||
>
|
||||
{intl.get('explore.addCondition')}
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</Form>
|
||||
<GQLModal
|
||||
gql={currentGQL}
|
||||
handlerRef={handler => {
|
||||
this.gqlRef = handler;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="btn-gql"
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_gql_view"
|
||||
onClick={this.handleViewGQL}
|
||||
>
|
||||
{intl.get('common.exportNGQL')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="expand-footer">
|
||||
<IconFont
|
||||
type="iconstudio-indentleft"
|
||||
className="btn-collapse"
|
||||
onClick={close}
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_sider_close"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={this.handleExpand}
|
||||
loading={!!getExpandLoading}
|
||||
data-track-category="explore"
|
||||
data-track-action="graph_expand"
|
||||
data-track-label="from_sider"
|
||||
disabled={
|
||||
!selectVertexes.length ||
|
||||
!selectEdgeTypes ||
|
||||
!selectEdgeTypes.length ||
|
||||
quantityLimit < 0
|
||||
}
|
||||
>
|
||||
{intl.get('explore.expand')}
|
||||
</Button>
|
||||
<Instruction description={intl.get('explore.expandTips')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(Form.create()(Expand));
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch> {
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
class ExpandBtn extends React.PureComponent<IProps> {
|
||||
handleClose = () => {
|
||||
this.props.toggleExpand({
|
||||
showSider: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.props.toggleExpand({
|
||||
showSider: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { currentSpace, showSider } = this.props;
|
||||
if (currentSpace) {
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={<span>{intl.get('explore.expansionConditions')}</span>}
|
||||
visible={showSider}
|
||||
className="expand-drawer"
|
||||
width="300"
|
||||
onClose={this.handleClose}
|
||||
getContainer={false}
|
||||
closable={false}
|
||||
mask={false}
|
||||
placement="right"
|
||||
>
|
||||
<Expand close={this.handleClose} />
|
||||
</Drawer>
|
||||
{!showSider && (
|
||||
<div className="expand-sider">
|
||||
<div className="btn-expand" onClick={this.handleOpen}>
|
||||
<IconFont
|
||||
type="iconstudio-expandcondition"
|
||||
className="icon-expand"
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_sider_open"
|
||||
/>
|
||||
{intl.get('explore.expand')}
|
||||
</div>
|
||||
<div className="sider-footer">
|
||||
<IconFont
|
||||
type="iconstudio-indentright"
|
||||
className="icon-collapse"
|
||||
onClick={this.handleOpen}
|
||||
data-track-category="explore"
|
||||
data-track-action="expand_sider_open"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default connect(mapState, mapDispatch)(ExpandBtn);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.export-gql {
|
||||
text-align: left;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.export-gql .ant-collapse-content-box div {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { Collapse } from 'antd';
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { CodeMirror } from '#app/components';
|
||||
|
||||
import './index.less';
|
||||
const Panel = Collapse.Panel;
|
||||
interface IOptions {
|
||||
[propName: string]: string;
|
||||
}
|
||||
const GQLCodeMirror = (props: { currentGQL: string; option?: IOptions }) => {
|
||||
const options = {
|
||||
keyMap: 'sublime',
|
||||
fullScreen: true,
|
||||
mode: 'nebula',
|
||||
readOnly: true,
|
||||
...props.option,
|
||||
};
|
||||
return (
|
||||
<Collapse className="export-gql">
|
||||
<Panel header={intl.get('common.exportNGQL')} key="ngql">
|
||||
<CodeMirror value={props.currentGQL} options={options} />
|
||||
</Panel>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
|
||||
export default GQLCodeMirror;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.modal-gql {
|
||||
.ant-modal-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
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<IProps> {
|
||||
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 (
|
||||
<Modal
|
||||
className="modal-gql"
|
||||
title={intl.get('common.exportNGQL')}
|
||||
handlerRef={handler => {
|
||||
this.modalHandler = handler;
|
||||
}}
|
||||
footer={false}
|
||||
width={700}
|
||||
>
|
||||
<CodeMirror value={gql} />
|
||||
<div className="footer">
|
||||
<CopyToClipboard text={gql} onCopy={this.handleCopy}>
|
||||
<Button type="primary">{intl.get('common.copy')}</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GQLModal;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React, { HTMLProps } from 'react';
|
||||
|
||||
interface IIconFontProps extends HTMLProps<HTMLElement> {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const IconFont = (props: IIconFontProps) => {
|
||||
const { type, className, ...others } = props;
|
||||
return (
|
||||
<span className={`nebula-cloud-icon ${type} ${className}`} {...others} />
|
||||
);
|
||||
};
|
||||
|
||||
export default IconFont;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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 = (
|
||||
<Icon type={type} key={type} onClick={() => onClick(props.icon)} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="icon-box">
|
||||
{!!content ? (
|
||||
iconElement
|
||||
) : (
|
||||
<Popover content="Remove Icon">{iconElement}</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class IconPickerBtn extends React.PureComponent<IProps> {
|
||||
handleChangeIconComplete = (icon: IIcon) => {
|
||||
this.props.handleChangeIconComplete?.(icon);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="icon-picker">
|
||||
<Carousel lazyLoad="progressive" dots={true}>
|
||||
{iconGroup.map((group, index) => (
|
||||
<div className="icon-group" key={index}>
|
||||
{group.map(icon => (
|
||||
<IconItem
|
||||
icon={icon}
|
||||
key={icon.type}
|
||||
onClick={this.handleChangeIconComplete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default IconPickerBtn;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.icon-instruction {
|
||||
margin: 0 2px;
|
||||
color: rgba(140, 140, 140, 1);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Icon, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const Instruction = (props: { description: string; onClick?: () => void }) => {
|
||||
return (
|
||||
<Tooltip title={props.description} placement="right">
|
||||
<Icon
|
||||
type="question-circle"
|
||||
className="icon-instruction"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default Instruction;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
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<IModalProps, IModalState> {
|
||||
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 && (
|
||||
<AntModal
|
||||
visible={true}
|
||||
onCancel={() => {
|
||||
this.hide();
|
||||
}}
|
||||
{...this.props}
|
||||
>
|
||||
{this.props.children}
|
||||
</AntModal>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
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<IProps, {}> {
|
||||
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 (
|
||||
<g className="links" ref={(ref: SVGTextElement) => (this.ref = ref)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
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<IProps, {}> {
|
||||
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<INode>(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 (
|
||||
<g className="labels" ref={(ref: SVGGElement) => (this.ref = ref)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
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<IProps, {}> {
|
||||
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 <rect className="rect" />;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -0,0 +1,575 @@
|
|||
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<typeof mapState> {
|
||||
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<IProps> {
|
||||
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<SVGGElement>('g')
|
||||
.attr('id', (d: INode) => `node_${d.uuid}`)
|
||||
.attr('class', 'node')
|
||||
.append<SVGCircleElement>('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 (
|
||||
<>
|
||||
<svg
|
||||
id="output-graph"
|
||||
className={isZoom ? 'cursor-move' : undefined}
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<g
|
||||
className="nebula-d3-canvas"
|
||||
ref={(ref: SVGCircleElement) => (this.canvasBoardRef = ref)}
|
||||
>
|
||||
<Links
|
||||
links={data.edges}
|
||||
selectedPaths={selectedPaths}
|
||||
onUpdateLinks={this.handleUpdateLinks}
|
||||
onMouseInLink={onMouseInLink}
|
||||
onMouseOut={onMouseOut}
|
||||
/>
|
||||
<g
|
||||
className="nebula-d3-nodes"
|
||||
ref={(ref: SVGGElement) => (this.nodeRef = ref)}
|
||||
/>
|
||||
<Labels
|
||||
nodes={data.vertexes}
|
||||
onUpDataNodeTexts={this.handleUpdataNodeTexts}
|
||||
/>
|
||||
</g>
|
||||
<SelectIds
|
||||
nodes={data.vertexes}
|
||||
links={data.edges}
|
||||
offsetX={offsetX}
|
||||
offsetY={offsetY}
|
||||
scale={scale}
|
||||
onSelectVertexes={onSelectVertexes}
|
||||
onSelectEdges={onSelectEdges}
|
||||
selectedPaths={selectedPaths}
|
||||
/>
|
||||
</svg>
|
||||
<Menu width={width} height={height} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState)(NebulaD3);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
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<typeof mapDispatch>,
|
||||
FormComponentProps,
|
||||
RouteComponentProps {
|
||||
data: any;
|
||||
}
|
||||
|
||||
const mapState = () => ({});
|
||||
|
||||
const mapDispatch = (dispatch: IDispatch) => ({
|
||||
updatePreloadData: data =>
|
||||
dispatch.explore.update({
|
||||
preloadData: data,
|
||||
}),
|
||||
});
|
||||
|
||||
class Export extends React.Component<IProps> {
|
||||
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 (
|
||||
<div className="export-modal">
|
||||
<Form className="form" {...layout}>
|
||||
{getFieldDecorator('type', {
|
||||
initialValue: 'vertex',
|
||||
})(
|
||||
<Radio.Group>
|
||||
<Radio.Button value="vertex">
|
||||
{intl.get('import.vertexText')}
|
||||
</Radio.Button>
|
||||
<Radio.Button value="edge">
|
||||
{intl.get('common.edge')}
|
||||
</Radio.Button>
|
||||
</Radio.Group>,
|
||||
)}
|
||||
{type === 'vertex' && (
|
||||
<>
|
||||
<p>{intl.get('console.exportVertex')}</p>
|
||||
<Form.Item className="select-component" label="vid">
|
||||
{getFieldDecorator('vertexId', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select>
|
||||
{headers.map(i => (
|
||||
<Option value={i} key={i}>
|
||||
{i}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{type === 'edge' && (
|
||||
<>
|
||||
<p>{intl.get('console.exportEdge')}</p>
|
||||
<Form.Item className="select-component" label="Edge Type">
|
||||
{getFieldDecorator('edgeType', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(<Input />)}
|
||||
</Form.Item>
|
||||
<Form.Item className="select-component" label="Src ID">
|
||||
{getFieldDecorator('srcId', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select>
|
||||
{headers.map(i => (
|
||||
<Option value={i} key={i}>
|
||||
{i}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item className="select-component" label="Dst ID">
|
||||
{getFieldDecorator('dstId', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select>
|
||||
{headers.map(i => (
|
||||
<Option value={i} key={i}>
|
||||
{i}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item className="select-component" label="Rank">
|
||||
{getFieldDecorator('rank')(
|
||||
<Select allowClear={true}>
|
||||
{headers.map(i => (
|
||||
<Option value={i} key={i}>
|
||||
{i}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
<div className="modal-footer">
|
||||
<Button
|
||||
disabled={!!disabled}
|
||||
key="confirm"
|
||||
type="primary"
|
||||
onClick={this.handleExport}
|
||||
>
|
||||
{intl.get('common.import')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapState,
|
||||
mapDispatch,
|
||||
)(withRouter(Form.create<IProps>()(Export)));
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.console-graph {
|
||||
#graph {
|
||||
svg {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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<IProps> {
|
||||
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 <div id="graph" ref={(ref: HTMLDivElement) => (this.ref = ref)} />;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch>,
|
||||
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<IProps> {
|
||||
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 (
|
||||
<div className="output-box">
|
||||
<Alert
|
||||
message={
|
||||
<p className="gql" onClick={() => this.props.onHistoryItem(value)}>
|
||||
$ {value}
|
||||
</p>
|
||||
}
|
||||
className="output-value"
|
||||
type={this.outputClass(result.code)}
|
||||
/>
|
||||
<div className="tab-container">
|
||||
<Tabs
|
||||
defaultActiveKey={'log'}
|
||||
size={'large'}
|
||||
tabPosition={'left'}
|
||||
onChange={this.handleTabChange}
|
||||
>
|
||||
{result.code === 0 && (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<>
|
||||
<Icon type="table" />
|
||||
{intl.get('common.table')}
|
||||
</>
|
||||
}
|
||||
key="table"
|
||||
>
|
||||
<div className="operation">
|
||||
<OutputCsv
|
||||
tableData={{
|
||||
headers: result.data && result.data.headers,
|
||||
tables: dataSource,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginLeft: '10px' }}
|
||||
onClick={this.handleExplore}
|
||||
>
|
||||
{showSubgraphs
|
||||
? intl.get('console.showSubgraphs')
|
||||
: intl.get('common.openInExplore')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
bordered={true}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={{
|
||||
showTotal: () =>
|
||||
`${intl.get('common.total')} ${dataSource.length}`,
|
||||
}}
|
||||
rowKey={(_, index) => index.toString()}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{result.code === 0 && result.data.headers[0] === 'format' && (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<>
|
||||
<Icon type="share-alt" />
|
||||
{intl.get('common.graph')}
|
||||
</>
|
||||
}
|
||||
key="graph"
|
||||
>
|
||||
{<Graphviz graph={dataSource[0].format} />}
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{result.code !== 0 && (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<>
|
||||
<Icon type="alert" />
|
||||
{intl.get('common.log')}
|
||||
</>
|
||||
}
|
||||
key="log"
|
||||
>
|
||||
{result.message}
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
{result.code === 0 && result.data.timeCost !== undefined && (
|
||||
<div className="output-footer">
|
||||
<span>
|
||||
{`${intl.get('console.execTime')} ${result.data.timeCost /
|
||||
1000000} (s)`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
className="export-node-modal"
|
||||
handlerRef={handler => (this.importNodesHandler = handler)}
|
||||
footer={null}
|
||||
width="650px"
|
||||
>
|
||||
<Export data={result.data} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(withRouter(OutputBox));
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
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<IProps> {
|
||||
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 && (
|
||||
<a
|
||||
className="csv-export ant-btn"
|
||||
href={url}
|
||||
download="result"
|
||||
data-track-category="console"
|
||||
data-track-action="export_csv_file"
|
||||
>
|
||||
{intl.get('common.output')}
|
||||
</a>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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<IProps> {
|
||||
render() {
|
||||
const { icon, color } = this.props;
|
||||
return (
|
||||
<div className="btn-nodeStyle-set">
|
||||
<div className="color-group">
|
||||
<div
|
||||
className={classnames('btn-color')}
|
||||
style={{ background: color }}
|
||||
>
|
||||
{icon && <Icon className="icon-selected" type={icon} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DisplayBtn;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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<IProps> {
|
||||
render() {
|
||||
const { handleChangeColorComplete, handleChangeIconComplete } = this.props;
|
||||
return (
|
||||
<Tabs className="tab-type-set">
|
||||
<Tabs.TabPane tab={intl.get('common.color')} key="color">
|
||||
<ColorPicker handleChangeColorComplete={handleChangeColorComplete} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={intl.get('common.icon')} key="icon">
|
||||
<IconPicker handleChangeIconComplete={handleChangeIconComplete} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StyleSetTabs;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
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<IProps> {
|
||||
render() {
|
||||
const {
|
||||
icon,
|
||||
color,
|
||||
handleChangeColorComplete,
|
||||
handleChangeIconComplete,
|
||||
} = this.props;
|
||||
return (
|
||||
<Popover
|
||||
overlayClassName="nodeStyle-popover"
|
||||
trigger={'click'}
|
||||
content={
|
||||
<StyleSetTabs
|
||||
handleChangeColorComplete={handleChangeColorComplete}
|
||||
handleChangeIconComplete={handleChangeIconComplete}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DisplayBtn icon={icon} color={color} />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VertexStyleSet;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
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';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const codeLog: string[] = ['请求成功'];
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import enUS from './locale/en-US.json';
|
||||
import zhCN from './locale/zh-CN.json';
|
||||
|
||||
export const INTL_LOCALE_SELECT = {
|
||||
EN_US: {
|
||||
TEXT: 'English',
|
||||
NAME: 'EN_US',
|
||||
},
|
||||
ZH_CN: {
|
||||
TEXT: '中文',
|
||||
NAME: 'ZH_CN',
|
||||
},
|
||||
};
|
||||
|
||||
export const INTL_LOCALES = {
|
||||
EN_US: enUS,
|
||||
ZH_CN: zhCN,
|
||||
};
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
import BigNumber from 'bignumber.js';
|
||||
import JSONBigint from 'json-bigint';
|
||||
import json2csv from 'json2csv';
|
||||
|
||||
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',
|
||||
'#B98031',
|
||||
'#B9B031',
|
||||
'#68B931',
|
||||
'#31B9B1',
|
||||
'#3180B9',
|
||||
'#7331B9',
|
||||
'#FF7875',
|
||||
'#FF9C6E',
|
||||
'#FFC069',
|
||||
'#FFF566',
|
||||
'#95DE64',
|
||||
'#5CDBD3',
|
||||
'#69C0FF',
|
||||
'#B37FEB',
|
||||
'#FFB9B8',
|
||||
'#FFCEB8',
|
||||
'#FFE1B8',
|
||||
'#FFFAB8',
|
||||
'#D7F2C4',
|
||||
'#C5F2EF',
|
||||
'#B8E1FF',
|
||||
'#DAC1F5',
|
||||
'#FFE6E6',
|
||||
'#FFEEE6',
|
||||
'#FFF4E6',
|
||||
'#FFFDE6',
|
||||
'#F1FBEA',
|
||||
'#EAFAF9',
|
||||
'#E6F4FF',
|
||||
'#F2E9FC',
|
||||
];
|
||||
export const DEFAULT_COLOR_PICK_LIST = [
|
||||
'#FF7875',
|
||||
'#FF9C6E',
|
||||
'#FFC069',
|
||||
'#FFF566',
|
||||
'#95DE64',
|
||||
'#5CDBD3',
|
||||
'#69C0FF',
|
||||
'#B37FEB',
|
||||
'#FFB9B8',
|
||||
'#FFCEB8',
|
||||
'#FFE1B8',
|
||||
'#FFFAB8',
|
||||
'#D7F2C4',
|
||||
'#C5F2EF',
|
||||
'#B8E1FF',
|
||||
'#DAC1F5',
|
||||
'#FFE6E6',
|
||||
'#FFEEE6',
|
||||
'#FFF4E6',
|
||||
'#FFFDE6',
|
||||
'#F1FBEA',
|
||||
'#EAFAF9',
|
||||
'#E6F4FF',
|
||||
'#F2E9FC',
|
||||
'#B93431',
|
||||
'#B95C31',
|
||||
'#B98031',
|
||||
'#B9B031',
|
||||
'#68B931',
|
||||
'#31B9B1',
|
||||
'#3180B9',
|
||||
'#7331B9',
|
||||
];
|
||||
|
||||
export const flattenData = data => {
|
||||
const result = {};
|
||||
const fieldData = [] as any;
|
||||
function recurse(cur: any, prop) {
|
||||
if (Object(cur) !== cur) {
|
||||
fieldData.push(prop);
|
||||
result[prop] = cur;
|
||||
} else if (Array.isArray(cur)) {
|
||||
for (let i = 0, l = cur.length; i < l; i++) {
|
||||
recurse(cur[i], prop ? prop + '.' + i : '' + i);
|
||||
if (l === 0) {
|
||||
result[prop] = [];
|
||||
}
|
||||
}
|
||||
} else if (BigNumber.isBigNumber(cur)) {
|
||||
result[prop] = cur;
|
||||
} else {
|
||||
let isEmpty = true;
|
||||
Object.keys(cur).forEach(p => {
|
||||
isEmpty = false;
|
||||
recurse(cur[p], prop ? prop + '.' + p : p);
|
||||
if (isEmpty) {
|
||||
result[prop] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
recurse(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') => {
|
||||
const fields =
|
||||
type === 'vertex'
|
||||
? ['vid', 'attributes']
|
||||
: ['type', 'srcId', 'dstId', 'rank', 'attributes'];
|
||||
const tables: any = [];
|
||||
data.forEach((item: any) => {
|
||||
const _result = {} as any;
|
||||
const properties =
|
||||
type === 'vertex' ? item.nodeProp.properties : item.edgeProp.properties;
|
||||
const { result } = flattenData(properties) as any;
|
||||
if (type === 'vertex') {
|
||||
_result.vid = item.name;
|
||||
_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.rank = item.rank;
|
||||
_result.attributes = JSONBigint.stringify(result);
|
||||
tables.push(_result);
|
||||
}
|
||||
});
|
||||
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 DEFAULT_EXPLORE_RULES = {
|
||||
edgeTypes: [],
|
||||
edgeDirection: 'outgoing',
|
||||
stepsType: 'single',
|
||||
step: 1,
|
||||
vertexStyle: 'colorGroupByTag',
|
||||
quantityLimit: 100,
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* this folder for config
|
||||
*/
|
||||
|
||||
export * from './constants';
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
{
|
||||
"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",
|
||||
"spaceTip": "Only required when you need to execute queries in a specified space.",
|
||||
"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",
|
||||
"schema": "Schema",
|
||||
"create": "Create",
|
||||
"serialNumber": "No.",
|
||||
"name": "Name",
|
||||
"operation": "Operations",
|
||||
"delete": "Delete",
|
||||
"optionalParameters": "Optional Parameters",
|
||||
"exportNGQL": "View nGQL",
|
||||
"field": "Field",
|
||||
"relatedProperties": "Related Properties",
|
||||
"type": "Type",
|
||||
"edit": "Edit",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"propertyName": "Property Name",
|
||||
"dataType": "Data Type",
|
||||
"allowNull": "Allow Null",
|
||||
"defaults": "Defaults",
|
||||
"addProperty": "Add Property",
|
||||
"updateSuccess": "Updated Successfully",
|
||||
"add": "Add",
|
||||
"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"
|
||||
},
|
||||
"NGQLOutput": {
|
||||
"success": "Execution successful!"
|
||||
},
|
||||
"warning": {
|
||||
"configServer": "Please configure the nebula server",
|
||||
"connectError": "Connection refused, please configure server again"
|
||||
},
|
||||
"configServer": {
|
||||
"connect": "Connect",
|
||||
"host": "Host",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"success": "succeed",
|
||||
"fail": "Failed",
|
||||
"clear": "Clear Connection",
|
||||
"title": "Configure Server"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"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/"
|
||||
},
|
||||
"import": {
|
||||
"import": "Import",
|
||||
"selectSpace": "Select Space",
|
||||
"uploadFile": "Upload Files",
|
||||
"vertex": "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",
|
||||
"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"
|
||||
},
|
||||
"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: <br />1. call function now() <br />2. call function timestamp(), for example: timestamp('2021-07-05T06:18:43.984000') <br />3. Input the timestamp directly, namely the number of seconds from 1970-01-01 00:00:00",
|
||||
"dateFormat": "Supported data inserting methods: <br />Call function date(), for example: date('2021-03-17')",
|
||||
"timeFormat": "Supported data inserting methods: <br />Call function time(), for example: time('17:53:59')",
|
||||
"datetimeFormat": "Supported data inserting methods: <br />Call function datetime(), for example: datetime('2021-03-17T17:53:59')",
|
||||
"geographyFormat": "Supported data inserting methods: <br /> Call function ST_GeogFromText(), for example:ST_GeogFromText('POINT(6 10)')",
|
||||
"geography(point)Format": "Supported data inserting methods: <br /> Call function ST_GeogFromText('POINT()'), for example:ST_GeogFromText('POINT(6 10)')",
|
||||
"geography(linestring)Format": "Supported data inserting methods: <br /> Call function ST_GeogFromText('LINESTRING()'), for example:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')",
|
||||
"geography(polygon)Format": "Supported data inserting methods: <br /> 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: <br /> Call function duration(<map>), 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",
|
||||
"dragSorting": "(Drag to Sort)",
|
||||
"selectFields": "Choose Property",
|
||||
"indexedLength": "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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
{
|
||||
"common": {
|
||||
"requestError": "请求错误",
|
||||
"currentSpace": "当前图空间",
|
||||
"languageSelect": "语言" ,
|
||||
"seeTheHistory":"查看历史",
|
||||
"table": "表格",
|
||||
"log":"日志",
|
||||
"record": "记录",
|
||||
"sorryNGQLCannotBeEmpty": "对不起,nGQL语句不能为空",
|
||||
"disablesUseToSwitchSpace": "禁止使用命令切换Space",
|
||||
"NGQLHistoryList": "nGQL历史列表",
|
||||
"spaceTip": "仅对某个Space进行操作时需要",
|
||||
"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": "可选参数",
|
||||
"exportNGQL": "对应的nGQL语句",
|
||||
"field": "字段",
|
||||
"relatedProperties": "相关属性",
|
||||
"type": "类型",
|
||||
"edit": "编辑",
|
||||
"deleteSuccess": "删除成功",
|
||||
"propertyName": "属性名称",
|
||||
"dataType": "数据类型",
|
||||
"allowNull": "允许空值",
|
||||
"defaults": "默认值",
|
||||
"addProperty": "添加属性",
|
||||
"updateSuccess": "更新成功",
|
||||
"add": "添加",
|
||||
"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": "描述"
|
||||
},
|
||||
"warning": {
|
||||
"configServer": "请先配置服务器",
|
||||
"connectError": "数据库连接有误,请重新配置"
|
||||
},
|
||||
"NGQLOutput": {
|
||||
"success": "执行成功"
|
||||
},
|
||||
"configServer": {
|
||||
"connect": "连接",
|
||||
"host": "Host",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"success": "配置成功",
|
||||
"fail": "配置失败",
|
||||
"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 长度需为正整数"
|
||||
},
|
||||
"console": {
|
||||
"cost": "开销",
|
||||
"execTime": "执行时间消耗",
|
||||
"exportVertex": "请选择表中代表点VID的列",
|
||||
"exportEdge": "请选择结果中分别代表边的起点(src_vid)、终点(dst_vid)和权重(rank)的列",
|
||||
"showSubgraphs": "查看子图",
|
||||
"deleteHistory": "清除历史",
|
||||
"parameterDisplay": "自定义参数 展示"
|
||||
},
|
||||
"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": "查看详情",
|
||||
"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/"
|
||||
},
|
||||
"import": {
|
||||
"import":"导入",
|
||||
"selectSpace": "选择Space",
|
||||
"uploadFile": "上传文件",
|
||||
"vertex": "关联点",
|
||||
"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 账号密码"
|
||||
},
|
||||
"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": "时间类型支持插入方式: <br />1. 调用函数 now() <br />2. 调用函数 timestamp(),例如:timestamp('2021-07-05T06:18:43.984000') <br />3. 直接输入时间戳,即从 1970-01-01 00:00:00 开始的秒数",
|
||||
"dateFormat": "日期类型支持插入方式: <br /> 调用函数 date(),例如:date('2021-03-17')",
|
||||
"timeFormat": "时间类型支持插入方式: <br /> 调用函数 time(),例如time('17:53:59')",
|
||||
"datetimeFormat": "日期时间类型支持插入方式: <br /> 调用函数 datetime(),例如:datetime('2021-03-17T17:53:59')",
|
||||
"geographyFormat": "geo 类型支持插入方式: <br /> 调用函数 ST_GeogFromText(),例如:ST_GeogFromText('POINT(6 10)')",
|
||||
"geography(point)Format": "geo(point) 类型支持插入方式: <br /> 调用函数 ST_GeogFromText('POINT()'),例如:ST_GeogFromText('POINT(6 10)')",
|
||||
"geography(linestring)Format": "geo(linestring) 类型支持插入方式: <br /> 调用函数 ST_GeogFromText('LINESTRING()'),例如:ST_GeogFromText('LINESTRING(3 4,10 50,20 25)')",
|
||||
"geography(polygon)Format": "geo(polygon) 类型支持插入方式: <br /> 调用函数 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 类型支持插入方式: <br /> 调用函数 duration(<map>),例如:duration({years: 1, seconds: 0})",
|
||||
"cancelOperation": "是否取消配置并关闭面板",
|
||||
"cancelPropmt": "关闭面板将删除所有属性,是否继续?",
|
||||
"fieldDisabled": "该属性被 ttl_col 引用,不支持更改操作,如要更改,请先更新 ttl",
|
||||
"ttlRequired": "请填写完整 ttl 关联的属性以及持续时间",
|
||||
"fieldRequired": "请填写完整属性名称及数据类型",
|
||||
"indexExist": "已拥有索引,无法同时配置 TTL",
|
||||
"indexType": "索引类型",
|
||||
"indexName": "索引名称",
|
||||
"indexFields": "索引属性",
|
||||
"dragSorting": "(可拖拽排序)",
|
||||
"selectFields": "选择关联的属性",
|
||||
"indexedLength": "索引长度",
|
||||
"indexedLengthDescription": "设置索引字符串的长度。如果索引定长字符串,则索引长度无法修改。",
|
||||
"indexedLengthRequired": "索引长度应为正整数",
|
||||
"backToTagList": "返回标签列表",
|
||||
"backToEdgeList": "返回边类型列表",
|
||||
"backToIndexList": "返回索引列表",
|
||||
"leavePage": "是否离开当前页面",
|
||||
"leavePagePrompt": "离开当前页面后,未保存的记录将丢失"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
const nebulaWordsUppercase = [
|
||||
'ADD',
|
||||
'ALTER',
|
||||
'AND',
|
||||
'AS',
|
||||
'ASC',
|
||||
'BALANCE',
|
||||
'BIGINT',
|
||||
'BOOL',
|
||||
'BY',
|
||||
'CHANGE',
|
||||
'COMPACT',
|
||||
'CREATE',
|
||||
'DELETE',
|
||||
'DESC',
|
||||
'DESCRIBE',
|
||||
'DISTINCT',
|
||||
'DOUBLE',
|
||||
'DOWNLOAD',
|
||||
'DROP',
|
||||
'EDGE',
|
||||
'EDGES',
|
||||
'EXISTS',
|
||||
'FETCH',
|
||||
'FIND',
|
||||
'FLUSH',
|
||||
'FROM',
|
||||
'GET',
|
||||
'GO',
|
||||
'GRANT',
|
||||
'IF',
|
||||
'IN',
|
||||
'INDEX',
|
||||
'INDEXES',
|
||||
'INGEST',
|
||||
'INSERT',
|
||||
'INT',
|
||||
'INTERSECT',
|
||||
'IS',
|
||||
'LIMIT',
|
||||
'LOOKUP',
|
||||
'MATCH',
|
||||
'MINUS',
|
||||
'NO',
|
||||
'NOT',
|
||||
'NULL',
|
||||
'OF',
|
||||
'OFFSET',
|
||||
'ON',
|
||||
'OR',
|
||||
'ORDER',
|
||||
'OVER',
|
||||
'OVERWRITE',
|
||||
'PROP',
|
||||
'REBUILD',
|
||||
'RECOVER',
|
||||
'REMOVE',
|
||||
'RETURN',
|
||||
'REVERSELY',
|
||||
'REVOKE',
|
||||
'SET',
|
||||
'SHOW',
|
||||
'STEPS',
|
||||
'STOP',
|
||||
'STRING',
|
||||
'SUBMIT',
|
||||
'TAG',
|
||||
'TAGS',
|
||||
'TIMESTAMP',
|
||||
'TO',
|
||||
'UNION',
|
||||
'UPDATE',
|
||||
'UPSERT',
|
||||
'UPTO',
|
||||
'USE',
|
||||
'VERTEX',
|
||||
'WHEN',
|
||||
'WHERE',
|
||||
'WITH',
|
||||
'XOR',
|
||||
'YIELD',
|
||||
'ACCOUNT',
|
||||
'ADMIN',
|
||||
'ALL',
|
||||
'AVG',
|
||||
'BIDIRECT',
|
||||
'BIT_AND',
|
||||
'BIT_OR',
|
||||
'BIT_XOR',
|
||||
'CHARSET',
|
||||
'COLLATE',
|
||||
'COLLATION',
|
||||
'CONFIGS',
|
||||
'COUNT',
|
||||
'COUNT_DISTINCT',
|
||||
'DATA',
|
||||
'DBA',
|
||||
'DEFAULT',
|
||||
'FORCE',
|
||||
'GOD',
|
||||
'GRAPH',
|
||||
'GROUP',
|
||||
'GUEST',
|
||||
'HDFS',
|
||||
'HOSTS',
|
||||
'JOB',
|
||||
'JOBS',
|
||||
'LEADER',
|
||||
'MAX',
|
||||
'META',
|
||||
'MIN',
|
||||
'OFFLINE',
|
||||
'PART',
|
||||
'PARTITION_NUM',
|
||||
'PARTS',
|
||||
'PASSWORD',
|
||||
'PATH',
|
||||
'REPLICA_FACTOR',
|
||||
'ROLE',
|
||||
'ROLES',
|
||||
'SHORTEST',
|
||||
'SNAPSHOT',
|
||||
'SNAPSHOTS',
|
||||
'SPACE',
|
||||
'SPACES',
|
||||
'STATUS',
|
||||
'STD',
|
||||
'STORAGE',
|
||||
'SUM',
|
||||
'TTL_COL',
|
||||
'TTL_DURATION',
|
||||
'USER',
|
||||
'USERS',
|
||||
'UUID',
|
||||
'VALUES',
|
||||
'COMMENT',
|
||||
':PARAM',
|
||||
':PARAMS',
|
||||
];
|
||||
|
||||
export const ban = ['use', 'USE'];
|
||||
|
||||
export const operators = [
|
||||
// Bitwise Operator
|
||||
'&',
|
||||
'|',
|
||||
'^',
|
||||
// Math
|
||||
'abs',
|
||||
'floor',
|
||||
'ceil',
|
||||
'round',
|
||||
'sqrt',
|
||||
'cbrt',
|
||||
'hypot',
|
||||
'pow',
|
||||
'exp',
|
||||
'exp2',
|
||||
'log',
|
||||
'log2',
|
||||
'sin',
|
||||
'asin',
|
||||
'cos',
|
||||
'acos',
|
||||
'tan',
|
||||
'atan',
|
||||
'rand32',
|
||||
'rand64',
|
||||
// String
|
||||
'strcasecmp',
|
||||
'lower',
|
||||
'upper',
|
||||
'length',
|
||||
'trim',
|
||||
'ltrim',
|
||||
'rtrim',
|
||||
'left',
|
||||
'right',
|
||||
'lpad',
|
||||
'rpad',
|
||||
'substr',
|
||||
'hash',
|
||||
// Timestamp
|
||||
'now',
|
||||
// Comparison Functions And Operators
|
||||
'=',
|
||||
'/',
|
||||
'==',
|
||||
'!=',
|
||||
'<',
|
||||
'<=',
|
||||
'-',
|
||||
'%',
|
||||
'+',
|
||||
'*',
|
||||
'-',
|
||||
'udf_is_in',
|
||||
// Aggregate
|
||||
'AVG',
|
||||
'COUNT',
|
||||
'MAX',
|
||||
'MIN',
|
||||
'STD',
|
||||
'SUM',
|
||||
// Logical Operator
|
||||
'&&',
|
||||
'!',
|
||||
'||',
|
||||
'XOR',
|
||||
// Order by Function
|
||||
'ORDER',
|
||||
'BY',
|
||||
'DESC',
|
||||
'ASC',
|
||||
// Limit
|
||||
'LIMIT',
|
||||
// Set Operations
|
||||
'UNION',
|
||||
'INTERSECT',
|
||||
'MINUS',
|
||||
// uuid
|
||||
'uuid',
|
||||
// assignment
|
||||
'=>',
|
||||
];
|
||||
|
||||
const nebulaWordsLowercase = nebulaWordsUppercase.map(w => w.toLowerCase());
|
||||
|
||||
export const keyWords = [...nebulaWordsUppercase, ...nebulaWordsLowercase];
|
||||
|
||||
export const maxLineNum = 20;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { POSITIVE_INTEGER_REGEX } from '#app/utils/constant';
|
||||
|
||||
export const hostRulesFn = intl => [
|
||||
{
|
||||
required: true,
|
||||
message: intl.get('formRules.hostRequired'),
|
||||
},
|
||||
];
|
||||
|
||||
export const usernameRulesFn = intl => [
|
||||
{
|
||||
required: true,
|
||||
message: intl.get('formRules.usernameRequired'),
|
||||
},
|
||||
];
|
||||
|
||||
export const passwordRulesFn = intl => [
|
||||
{
|
||||
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 numberRulesFn = intl => [
|
||||
{
|
||||
pattern: POSITIVE_INTEGER_REGEX,
|
||||
message: intl.get('formRules.numberRequired'),
|
||||
},
|
||||
];
|
||||
|
||||
export const replicaRulesFn = (intl, activeMachineNum) => [
|
||||
{
|
||||
pattern: POSITIVE_INTEGER_REGEX,
|
||||
message: intl.get('formRules.numberRequired'),
|
||||
},
|
||||
{
|
||||
validator(_rule, value, callback) {
|
||||
if (value && Number(value) > activeMachineNum) {
|
||||
callback(
|
||||
intl.get('formRules.replicaLimit', { number: activeMachineNum }),
|
||||
);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { _delete, get, post, put } from '../utils/http';
|
||||
|
||||
const execNGQL = post('/api-nebula/db/exec');
|
||||
|
||||
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 createConfigFile = post('/api/import/config');
|
||||
|
||||
const getLog = get('/api/import/log');
|
||||
const finishImport = post('/api/import/finish');
|
||||
|
||||
const getImportWokingDir = get('/api/import/working_dir');
|
||||
|
||||
const deteleFile = params => {
|
||||
const { filename } = params;
|
||||
return _delete(`/api/files/${filename}`)();
|
||||
};
|
||||
const getFiles = get('/api/files');
|
||||
const uploadFiles = (params?, config?) =>
|
||||
put('/api/files')(params, {
|
||||
...config,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
export default {
|
||||
execNGQL,
|
||||
connectDB,
|
||||
disconnectDB,
|
||||
importData,
|
||||
finishImport,
|
||||
handleImportAction,
|
||||
createConfigFile,
|
||||
getLog,
|
||||
getImportWokingDir,
|
||||
deteleFile,
|
||||
getFiles,
|
||||
uploadFiles,
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { INTL_LOCALE_SELECT } from '../config';
|
||||
|
||||
export const LanguageContext = React.createContext({
|
||||
currentLocale: INTL_LOCALE_SELECT.EN_US.NAME,
|
||||
toggleLanguage: (locale: string) => {
|
||||
console.log('Select locale:', locale);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* this folder for React context api
|
||||
*/
|
||||
|
||||
export * from './LanguageContext';
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Nebula Graph Studio</title>
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<!-- Global Event Tracking -->
|
||||
<style>
|
||||
@-webkit-keyframes square-spin {
|
||||
25% {
|
||||
-webkit-transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||
transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||
transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||
transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||
}
|
||||
|
||||
|
||||
100% {
|
||||
-webkit-transform: perspective(100px) rotateX(0) rotateY(0);
|
||||
transform: perspective(100px) rotateX(0) rotateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes square-spin {
|
||||
25% {
|
||||
-webkit-transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||
transform: perspective(100px) rotateX(180deg) rotateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||
transform: perspective(100px) rotateX(180deg) rotateY(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||
transform: perspective(100px) rotateX(0) rotateY(180deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: perspective(100px) rotateX(0) rotateY(0);
|
||||
transform: perspective(100px) rotateX(0) rotateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.square-spin > div {
|
||||
-webkit-animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
|
||||
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #06f;
|
||||
}
|
||||
|
||||
.square-spin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
top: 200px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style='background: #f0f2f5;'>
|
||||
<div id="app" style="height: 100%;">
|
||||
<div class="square-spin">
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { message } from 'antd';
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import App from './App';
|
||||
import { store } from './store';
|
||||
message.config({
|
||||
maxCount: 1,
|
||||
});
|
||||
ReactDom.render(
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch>,
|
||||
RouteComponentProps {}
|
||||
|
||||
class ConfigServer extends React.Component<IProps> {
|
||||
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 (
|
||||
<div className="config-server">
|
||||
<div className="nebula-authorization">
|
||||
<h3>{intl.get('configServer.title')}</h3>
|
||||
<ConfigServerForm onConfig={this.handleConfigServer} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(ConfigServer);
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch> {
|
||||
onSpaceChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
class SpaceSearchInput extends React.Component<IProps> {
|
||||
componentDidMount() {
|
||||
this.getSpaces();
|
||||
}
|
||||
getSpaces = () => {
|
||||
this.props.asyncGetSpaces();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, onSpaceChange, loading } = this.props;
|
||||
return (
|
||||
<Select
|
||||
showSearch={true}
|
||||
placeholder="space name"
|
||||
value={value}
|
||||
defaultActiveFirstOption={false}
|
||||
onChange={onSpaceChange}
|
||||
onFocus={this.getSpaces}
|
||||
className="space-search-input"
|
||||
loading={!!loading}
|
||||
>
|
||||
{this.props.spaces.map(s => (
|
||||
<Option value={s} key={s}>
|
||||
{s}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(SpaceSearchInput);
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch>,
|
||||
RouteComponentProps {}
|
||||
|
||||
// split from semicolon out of quotation marks
|
||||
const SEMICOLON_REG = /((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/;
|
||||
class Console extends React.Component<IProps, IState> {
|
||||
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 (
|
||||
<div className="nebula-console padding-page">
|
||||
<div className="ngql-content">
|
||||
<div className="mirror-wrap">
|
||||
<div className="mirror-nav">
|
||||
{intl.get('common.currentSpace')}:
|
||||
<SpaceSearchInput
|
||||
onSpaceChange={this.props.asyncSwitchSpace}
|
||||
value={currentSpace}
|
||||
/>
|
||||
<Tooltip title={intl.get('common.spaceTip')} placement="right">
|
||||
<Icon type="question-circle" theme="outlined" />
|
||||
</Tooltip>
|
||||
<div className="operation">
|
||||
<Tooltip title={intl.get('common.empty')} placement="bottom">
|
||||
<Icon type="delete" onClick={this.handleEmptyNgql} />
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={intl.get('common.seeTheHistory')}
|
||||
placement="bottom"
|
||||
>
|
||||
<Icon
|
||||
type="history"
|
||||
onClick={() => {
|
||||
this.setState({ history: true });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={intl.get('common.run')} placement="bottom">
|
||||
{!!runGQLLoading ? (
|
||||
<Icon type="loading" />
|
||||
) : (
|
||||
<Icon
|
||||
type="play-circle"
|
||||
theme="twoTone"
|
||||
onClick={() => this.handleRun()}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mirror-content">
|
||||
<Tooltip
|
||||
title={intl.get('console.parameterDisplay')}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
type="file-search"
|
||||
className="btn-drawer"
|
||||
onClick={this.toggleDrawer}
|
||||
/>
|
||||
</Tooltip>
|
||||
{this.state.visible && (
|
||||
<div className="param-box">
|
||||
{Object.entries(paramsMap).map(([k, v]) => (
|
||||
<p key={k}>{`${k} => ${JSON.stringify(v)}`}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<CodeMirror
|
||||
value={currentGQL}
|
||||
onBlur={value => 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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="result-wrap">
|
||||
<OutputBox
|
||||
result={result}
|
||||
value={this.getLocalStorage().pop()}
|
||||
onHistoryItem={e => this.handleHistoryItem(e)}
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
title={
|
||||
<div>
|
||||
<span className="history-title">
|
||||
{intl.get('common.NGQLHistoryList')}
|
||||
</span>
|
||||
<Button type="link" onClick={this.handleEmptyNgqlHistory}>
|
||||
{intl.get('console.deleteHistory')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
visible={history}
|
||||
className="historyList"
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
this.setState({ history: false });
|
||||
}}
|
||||
>
|
||||
{
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.getLocalStorage().reverse()}
|
||||
renderItem={(item: string) => (
|
||||
<List.Item
|
||||
style={{ cursor: 'pointer' }}
|
||||
className="history-list"
|
||||
onClick={() => this.handleHistoryItem(item)}
|
||||
>
|
||||
{this.historyListShow(item)}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(Console);
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.algorithm-query {
|
||||
text-align: center;
|
||||
|
||||
.algorithm-form {
|
||||
.select-algorithm .icon-instruction {
|
||||
position: absolute;
|
||||
right: -25px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch>,
|
||||
FormComponentProps {
|
||||
closeHandler: any;
|
||||
}
|
||||
|
||||
class AlgorithmQuery extends React.Component<IProps> {
|
||||
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 (
|
||||
<div className="algorithm-query">
|
||||
<Form {...formItemLayout} className="algorithm-form">
|
||||
<Form.Item
|
||||
label={intl.get('common.algorithm')}
|
||||
className="select-algorithm"
|
||||
>
|
||||
{getFieldDecorator('type', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Select>
|
||||
{GRAPH_ALOGORITHM(intl).map(item => (
|
||||
<Option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
<Instruction
|
||||
description={intl.get('common.viewDocs')}
|
||||
onClick={this.viewDoc}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider orientation="center">
|
||||
{intl.get('explore.algorithmParams')}
|
||||
</Divider>
|
||||
<Form.Item label={intl.get('explore.srcId')}>
|
||||
{getFieldDecorator('srcId', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Src ID is required',
|
||||
},
|
||||
],
|
||||
})(<Select mode="tags" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.dstId')}>
|
||||
{getFieldDecorator('dstId', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Dst ID is required',
|
||||
},
|
||||
],
|
||||
})(<Select mode="tags" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.relation')}>
|
||||
{getFieldDecorator('relation')(
|
||||
<Select mode="multiple">
|
||||
{edgeTypes.map(e => (
|
||||
<Option value={e} key={e}>
|
||||
{e}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.direction')}>
|
||||
{getFieldDecorator('direction')(
|
||||
<Select allowClear={true}>
|
||||
<Option value="REVERSELY" key="REVERSELY">
|
||||
REVERSELY
|
||||
</Option>
|
||||
<Option value="BIDIRECT" key="BIDIRECT">
|
||||
BIDIRECT
|
||||
</Option>
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.stepLimit')}>
|
||||
{getFieldDecorator('stepLimit', {
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})(<Input type="number" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('explore.quantityLimit')}>
|
||||
{getFieldDecorator('quantityLimit', {
|
||||
rules: [
|
||||
{
|
||||
message: intl.get('formRules.positiveIntegerRequired'),
|
||||
pattern: /^\d+$/,
|
||||
transform(value) {
|
||||
if (value) {
|
||||
return Number(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})(<Input type="number" />)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<GQLCodeMirror currentGQL={currentGQL} />
|
||||
<Button
|
||||
data-track-category="explore"
|
||||
data-track-action="query_by_path"
|
||||
onClick={this.handleInquiry}
|
||||
type="primary"
|
||||
loading={!!loading}
|
||||
>
|
||||
{intl.get('explore.quiry')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(Form.create()(AlgorithmQuery));
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
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 (
|
||||
<div className="query-custom">
|
||||
<img
|
||||
className="logo"
|
||||
src={lang === 'ZH_CN' ? guideZhPng : guideEnPng}
|
||||
/>
|
||||
<p>{intl.get('explore.customQueryDescription')}</p>
|
||||
<div className="btn">
|
||||
<Button>
|
||||
<Link
|
||||
to="/console"
|
||||
data-track-category="navigation"
|
||||
data-track-action="view_console"
|
||||
data-track-label="from_explore_btn"
|
||||
>
|
||||
{intl.get('explore.openInConsole')}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomQuery;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
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<typeof mapState>,
|
||||
ReturnType<typeof mapDispatch>,
|
||||
FormComponentProps {
|
||||
closeHandler: any;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
class IdQuery extends React.Component<IProps, IState> {
|
||||
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 (
|
||||
<Spin spinning={loading}>
|
||||
<div className="import-node">
|
||||
<div className="header">
|
||||
<span>{intl.get('explore.idToBeQueried')}</span>
|
||||
<div className="btn-upload">
|
||||
<Button onClick={this.handleImportSample} loading={sampleLoading}>
|
||||
{intl.get('explore.sampleImport')}
|
||||
</Button>
|
||||
<Upload
|
||||
beforeUpload={() => false}
|
||||
onChange={this.handleFileImport}
|
||||
showUploadList={false}
|
||||
>
|
||||
<Button>{intl.get('explore.fileImport')}</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</div>
|
||||
<Form layout="horizontal">
|
||||
<Form.Item>
|
||||
{getFieldDecorator('ids', {
|
||||
rules: nodeIdRulesFn(intl),
|
||||
})(
|
||||
<TextArea
|
||||
placeholder={intl.get('explore.importPlaceholder')}
|
||||
rows={12}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item className="btn-wrap">
|
||||
<Button
|
||||
type="primary"
|
||||
data-track-category="explore"
|
||||
data-track-action="query_by_id"
|
||||
onClick={this.handleImport}
|
||||
>
|
||||
{intl.get('explore.addConfirm')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapState, mapDispatch)(Form.create<IProps>()(IdQuery));
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { Form, Tabs } from 'antd';
|
||||
import { FormComponentProps } from 'antd/lib/form/Form';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import AlgorithmQuery from './AlgorithmQuery';
|
||||
import CustomQuery from './CustomQuery';
|
||||
import IdQuery from './IdQuery';
|
||||
import IndexQuery from './IndexQuery';
|
||||
|
||||
interface IProps extends FormComponentProps {
|
||||
handler: any;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
class ImportNodes extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tabs defaultActiveKey={'id'}>
|
||||
<Tabs.TabPane tab={intl.get('explore.queryById')} key="id">
|
||||
<IdQuery closeHandler={this.props.handler.hide} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={intl.get('explore.queryByIndex')} key="index">
|
||||
<IndexQuery closeHandler={this.props.handler.hide} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={intl.get('explore.graphAlgorithm')} key="algorithm">
|
||||
<AlgorithmQuery closeHandler={this.props.handler.hide} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={intl.get('explore.queryByCustom')} key="custom">
|
||||
<CustomQuery />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<IProps>()(ImportNodes);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue