Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ddl export #343

Merged
merged 3 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# NebulaGraph Studio

<div align=center>
<img src="./app/static/images/logo_studio.svg" />
</div>

NebulaGraph Studio (Studio for short) is a web-based visualization tool for NebulaGraph. With Studio, you can create a graph schema, import data and edit nGQL statements for data queries.
![](./introduction.png)

Expand Down
19 changes: 19 additions & 0 deletions app/components/CodeMirror/index.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '~@app/common.less';

.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-break: break-all !important;
Expand All @@ -16,3 +18,20 @@
font-family: Roboto-Mono,sans-serif;
}
}

.CodeMirror {
font-family: Roboto-Mono, sans-serif;
.CodeMirror-gutters {
background: @lightBlue;
}
.CodeMirror-linenumber {
display: flex;
justify-content: center;
color: @darkGray;
}
.CodeMirror-scroll {
padding-bottom: 0;
margin-right: 0;
overflow: auto !important;
}
}
5 changes: 4 additions & 1 deletion app/config/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@
"startRebuildIndex": "Start rebuilding the index {name}",
"getSchema": "Get Schema",
"getSchemaTip": "Because there is no strong binding relationship between tags and edges in the current NebulaGraph version, the results will be generated based on randomly obtained data, for reference only.",
"danglingEdge": "Dangling edge"
"danglingEdge": "Dangling edge",
"showDDL": "View Schema DDL",
"downloadNGQL": "Download.nGQL",
"getDDLError": "Failed to get the schema DDL, Please try again"
},
"empty": {
"stats": "No Statistics Data",
Expand Down
5 changes: 4 additions & 1 deletion app/config/locale/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@
"startRebuildIndex": "开始重建索引{name}",
"getSchema": "获取 Schema",
"getSchemaTip": "因为当前 NebulaGraph 版本下不存在点边的强绑定关系,结果将依据随机捞取到的数据生成,仅供参考。",
"danglingEdge": "悬挂边"
"danglingEdge": "悬挂边",
"showDDL": "查看 Schema DDL",
"downloadNGQL": "下载 .NGQL 文件",
"getDDLError": "获取 Schema DDL 失败, 请重试"
},
"empty": {
"stats": "暂无统计数据",
Expand Down
18 changes: 0 additions & 18 deletions app/pages/Console/index.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -134,24 +134,6 @@
}
}
}
:global {
.CodeMirror {
font-family: Roboto-Mono, sans-serif;
.CodeMirror-gutters {
background: @lightBlue;
}
.CodeMirror-linenumber {
display: flex;
justify-content: center;
color: @darkGray;
}
.CodeMirror-scroll {
padding-bottom: 0;
margin-right: 0;
overflow: auto !important;
}
}
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions app/pages/Schema/SchemaConfig/DDLButton/index.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.ddlModal {
:global(.ant-modal-footer) {
border: none;
}
.modalItem {
.duplicateBtn {
border: 1px solid #2F3A4A;
border-radius: 5px;
color: #2F3A4A;
display: flex;
align-items: center;
svg {
width: 18px;
height: 18px;
}
margin-bottom: 12px;
}
}
.footer {
display: flex;
align-items: center;
justify-content: center;
}
}
120 changes: 120 additions & 0 deletions app/pages/Schema/SchemaConfig/DDLButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Button, message, Modal, Spin } from 'antd';
import Icon from '@app/components/Icon';
import React, { useCallback, useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import { observer } from 'mobx-react-lite';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import CodeMirror from '@app/components/CodeMirror';


import { useStore } from '@app/stores';
import { handleKeyword } from '@app/utils/function';
import styles from './index.module.less';

interface IProps {
space: string;
}
const options = {
keyMap: 'sublime',
fullScreen: true,
mode: 'nebula',
readOnly: true,
};
const sleepGql = `:sleep 20;`;
const DDLButton = (props: IProps) => {
const { space } = props;
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const { schema: { getSchemaDDL } } = useStore();
const [ddl, setDDL] = useState('');
const handleJoinGQL = useCallback((data) => data.map(i => i.replaceAll('\n', '')).join(';\n'), []);
const handleOpen = useCallback(async () => {
setVisible(true);
setLoading(true);
const ddlMap = await getSchemaDDL(space);
if(ddlMap) {
const { tags, edges, indexes } = ddlMap;
let content = `# Create Space \n${ddlMap.space.replace(/ON default_zone_(.*)+/gm, '')};\n${sleepGql}\nUSE ${handleKeyword(space)};`;
if(tags.length) {
content += `\n\n# Create Tag: \n${handleJoinGQL(tags)};`;
}
if(edges.length) {
content += `\n\n# Create Edge: \n${handleJoinGQL(edges)};`;
}

if(indexes.length) {
if((tags.length || edges.length)) {
content += `\n${sleepGql}`;
}
content += `\n\n# Create Index: \n${handleJoinGQL(indexes)};`;
}
setDDL(content);
}
setLoading(false);
}, [space]);
const handleCopy = useCallback(() => {
message.success(intl.get('common.copySuccess'));
}, []);

const handleDownload = useCallback(() => {
let url = '#';
const _utf = '\uFEFF';
if (window.Blob && window.URL && window.URL.createObjectURL) {
const csvBlob = new Blob([_utf + ddl], {
type: 'text/csv',
});
url = URL.createObjectURL(csvBlob);
}
url = 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(ddl);
const link = document.createElement('a');
link.href = url;
link.download = `${space}_ddl.ngql`;
link.click();
}, [space, ddl]);
useEffect(() => {
!visible && setDDL('');
}, [visible]);
return (
<>
<Button type="link" onClick={handleOpen}>
{intl.get('schema.showDDL')}
</Button>
<Modal
className={styles.ddlModal}
destroyOnClose={true}
open={visible}
width={'60%'}
bodyStyle={{ minHeight: 200 }}
onCancel={() => setVisible(false)}
title={intl.get('schema.showDDL')}
footer={
!loading && <div className={styles.footer}>
<Button
key="confirm"
type="primary"
onClick={handleDownload}
>
{intl.get('schema.downloadNGQL')}
</Button>
</div>
}
>
<Spin spinning={loading}>
{!loading && <div className={styles.modalItem}>
<CopyToClipboard key={1} text={ddl} onCopy={handleCopy}>
<Button className={styles.duplicateBtn} key="confirm" icon={<Icon type="icon-Duplicate" />}>
{intl.get('common.duplicate')}
</Button>
</CopyToClipboard>
<CodeMirror
value={ddl}
options={options}
/>
</div>}
</Spin>
</Modal>
</>
);
};

export default observer(DDLButton);
8 changes: 3 additions & 5 deletions app/pages/Schema/SchemaConfig/Edit/CommonEdit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,15 @@ const ConfigEdit = (props: IProps) => {
}
setEditName(_editName);
setLoading(true);
const { code, data } = await getTagOrEdgeDetail(editType, _editName);
const createGql = await getTagOrEdgeDetail(editType, _editName);
const { code: propCode, data: propData } = await getTagOrEdgeInfo(
editType,
_editName,
);
setLoading(false);
if (code === 0) {
const key = editType === 'tag' ? 'Create Tag' : 'Create Edge';
const info = data.tables[0][key];
if (createGql) {
const fieldInfo = propCode === 0 ? propData.tables : [];
handleData(_editName, info, fieldInfo);
handleData(_editName, createGql, fieldInfo);
}
};

Expand Down
29 changes: 17 additions & 12 deletions app/pages/Schema/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cls from 'classnames';
import { Link, useHistory } from 'react-router-dom';
import styles from './index.module.less';
import Search from './SchemaConfig/List/Search';
import DDLButton from './SchemaConfig/DDLButton';

interface IOperations {
space: string;
Expand All @@ -26,17 +27,8 @@ const Operations = (props: IOperations) => {
};
const items = [
{
key: 'delete',
label: <Popconfirm
onConfirm={() => onDelete(space)}
title={intl.get('common.ask')}
okText={intl.get('common.ok')}
cancelText={intl.get('common.cancel')}
>
<Button type="link" danger>
{intl.get('schema.deleteSpace')}
</Button>
</Popconfirm>
key: 'ddl',
label: <DDLButton space={space} />
},
{
key: 'clone',
Expand All @@ -62,7 +54,20 @@ const Operations = (props: IOperations) => {
{intl.get('schema.cloneSpace')}
</Button>
</Popover>
}
},
{
key: 'delete',
label: <Popconfirm
onConfirm={() => onDelete(space)}
title={intl.get('common.ask')}
okText={intl.get('common.ok')}
cancelText={intl.get('common.cancel')}
>
<Button type="link" danger>
{intl.get('schema.deleteSpace')}
</Button>
</Popconfirm>
},
];
return <Menu className={styles.operationsSpace} items={items} />;
};
Expand Down
23 changes: 9 additions & 14 deletions app/stores/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { action, makeObservable, observable, runInAction } from 'mobx';
import service from '@app/config/service';
import { IBasicConfig, IEdgeConfig, ITaskItem, IVerticesConfig } from '@app/interfaces/import';
import { configToJson } from '@app/utils/import';
import { ISchemaEnum } from '@app/interfaces/schema';
import { getRootStore } from '.';

const handlePropertyMap = (item, defaultValueFields) => {
Expand Down Expand Up @@ -139,14 +140,11 @@ export class ImportStore {
const { schema } = this.rootStore;
const { getTagOrEdgeInfo, getTagOrEdgeDetail } = schema;
const { tag, tagIndex, configIndex } = payload;
const { code, data } = await getTagOrEdgeInfo('tag', tag);
const createTag = await getTagOrEdgeDetail('tag', tag);
const { code, data } = await getTagOrEdgeInfo(ISchemaEnum.Tag, tag);
const createTagGQL = await getTagOrEdgeDetail(ISchemaEnum.Tag, tag);
const defaultValueFields: any[] = [];
if (!!createTag) {
const res =
(createTag.data.tables && createTag.data.tables[0]['Create Tag']) ||
'';
const fields = res.split(/\n|\r\n/);
if (createTagGQL) {
const fields = createTagGQL.split(/\n|\r\n/);
fields.forEach(field => {
const fieldArr = field.trim().split(/\s|\s+/);
if (fieldArr.includes('default') || fieldArr.includes('DEFAULT')) {
Expand Down Expand Up @@ -180,14 +178,11 @@ export class ImportStore {
} else {
const { schema } = this.rootStore;
const { getTagOrEdgeInfo, getTagOrEdgeDetail, spaceVidType } = schema;
const { code, data } = await getTagOrEdgeInfo('edge', edgeType);
const createTag = await getTagOrEdgeDetail('edge', edgeType);
const { code, data } = await getTagOrEdgeInfo(ISchemaEnum.Edge, edgeType);
const createEdgeGQL = await getTagOrEdgeDetail(ISchemaEnum.Edge, edgeType);
const defaultValueFields: any[] = [];
if (!!createTag) {
const res =
(createTag.data.tables && createTag.data.tables[0]['Create Edge']) ||
'';
const fields = res.split(/\n|\r\n/);
if (createEdgeGQL) {
const fields = createEdgeGQL.split(/\n|\r\n/);
fields.forEach(field => {
const fieldArr = field.trim().split(/\s|\s+/);
if (field.includes('default') || fieldArr.includes('DEFAULT')) {
Expand Down
Loading