diff --git a/app/config/locale/en-US.ts b/app/config/locale/en-US.ts
index 5af3cef0..8647446c 100644
--- a/app/config/locale/en-US.ts
+++ b/app/config/locale/en-US.ts
@@ -410,6 +410,7 @@ export default {
statError: 'Stat failed, Please try again.',
statFinished: 'Statistics end',
deleteSpace: 'Delete Graph Space',
+ clearSpace: 'Clear Graph Space',
cloneSpace: 'Clone Graph Space',
length: 'Length',
selectVidTypeTip: 'Please select the type',
diff --git a/app/config/locale/zh-CN.ts b/app/config/locale/zh-CN.ts
index 9f2d8dbb..f0e3335d 100644
--- a/app/config/locale/zh-CN.ts
+++ b/app/config/locale/zh-CN.ts
@@ -391,6 +391,7 @@ export default {
statError: '统计失败,请重试',
statFinished: '统计结束',
deleteSpace: '删除图空间',
+ clearSpace: '清空图空间',
cloneSpace: '克隆图空间',
length: '长度',
selectVidTypeTip: '选择 Vid 类型',
diff --git a/app/interfaces/schema.ts b/app/interfaces/schema.ts
index 37029e3c..4468fb90 100644
--- a/app/interfaces/schema.ts
+++ b/app/interfaces/schema.ts
@@ -13,13 +13,14 @@ export interface IField {
}
export interface ISpace {
- serialNumber: number;
+ Charset?: string;
+ Collate?: string;
+ Comment?: string;
+ ID?: number;
Name: string;
- ID: number;
- Charset: string;
- Collate: string;
- 'Partition Number': string;
- 'Replica Factor': string;
+ 'Partition Number'?: number;
+ 'Replica Factor'?: number;
+ 'Vid Type'?: string;
}
export interface ITag {
@@ -68,7 +69,7 @@ export interface IAlterForm {
export enum ISchemaEnum {
Tag = 'tag',
- Edge ='edge',
+ Edge = 'edge',
}
export enum IJobStatus {
Queue = 'QUEUE',
@@ -77,4 +78,4 @@ export enum IJobStatus {
Failed = 'FAILED',
Stopped = 'STOPPED',
Removed = 'REMOVED',
-}
\ No newline at end of file
+}
diff --git a/app/pages/Schema/SchemaConfig/DDLButton/index.module.less b/app/pages/Schema/SchemaConfig/DDLModal/index.module.less
similarity index 100%
rename from app/pages/Schema/SchemaConfig/DDLButton/index.module.less
rename to app/pages/Schema/SchemaConfig/DDLModal/index.module.less
diff --git a/app/pages/Schema/SchemaConfig/DDLButton/index.tsx b/app/pages/Schema/SchemaConfig/DDLModal/index.tsx
similarity index 62%
rename from app/pages/Schema/SchemaConfig/DDLButton/index.tsx
rename to app/pages/Schema/SchemaConfig/DDLModal/index.tsx
index c166f0f0..a283de65 100644
--- a/app/pages/Schema/SchemaConfig/DDLButton/index.tsx
+++ b/app/pages/Schema/SchemaConfig/DDLModal/index.tsx
@@ -5,7 +5,6 @@ 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 { useI18n } from '@vesoft-inc/i18n';
@@ -13,6 +12,8 @@ import styles from './index.module.less';
interface IProps {
space: string;
+ open: boolean;
+ onCancel?: () => void;
}
const options = {
keyMap: 'sublime',
@@ -22,29 +23,33 @@ const options = {
};
const sleepGql = `:sleep 20;`;
const DDLButton = (props: IProps) => {
- const { space } = props;
+ const { space, open, onCancel } = props;
const { intl } = useI18n();
- const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
- const { schema: { getSchemaDDL } } = useStore();
+ const {
+ schema: { getSchemaDDL },
+ } = useStore();
const [ddl, setDDL] = useState('');
- const handleJoinGQL = useCallback((data) => data.map(i => i.replaceAll('\n', '')).join(';\n'), []);
+ 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) {
+ 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) {
+ 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) {
+ if (edges.length) {
content += `\n\n# Create Edge: \n${handleJoinGQL(edges)};`;
}
- if(indexes.length) {
- if((tags.length || edges.length)) {
+ if (indexes.length) {
+ if (tags.length || edges.length) {
content += `\n${sleepGql}`;
}
content += `\n\n# Create Index: \n${handleJoinGQL(indexes)};`;
@@ -53,6 +58,7 @@ const DDLButton = (props: IProps) => {
}
setLoading(false);
}, [space]);
+
const handleCopy = useCallback(() => {
message.success(intl.get('common.copySuccess'));
}, []);
@@ -72,50 +78,47 @@ const DDLButton = (props: IProps) => {
link.download = `${space}_ddl.ngql`;
link.click();
}, [space, ddl]);
+
useEffect(() => {
- !visible && setDDL('');
- }, [visible]);
+ if (open) {
+ handleOpen();
+ } else {
+ setDDL('');
+ }
+ }, [open]);
+
return (
- <>
-
- setVisible(false)}
- title={intl.get('schema.showDDL')}
- footer={
- !loading &&
-
- }
- >
-
- {!loading &&
+ )
+ }
+ >
+
+ {!loading && (
+
}>
{intl.get('common.duplicate')}
-
-
}
-
-
- >
+
+
+ )}
+
+
);
};
diff --git a/app/pages/Schema/SchemaConfig/List/Search/index.tsx b/app/pages/Schema/SchemaConfig/List/Search/index.tsx
index 27e8bc99..63dfc53b 100644
--- a/app/pages/Schema/SchemaConfig/List/Search/index.tsx
+++ b/app/pages/Schema/SchemaConfig/List/Search/index.tsx
@@ -10,7 +10,7 @@ import { useStore } from '@app/stores';
import styles from './index.module.less';
interface IProps {
- onSearch: (value) => void;
+ onSearch: (value: string) => void;
type: string;
}
@@ -19,18 +19,18 @@ const Search = (props: IProps) => {
const { intl } = useI18n();
const location = useLocation();
const [value, setValue] = useState('');
- const { schema: { currentSpace } } = useStore();
+ const {
+ schema: { currentSpace },
+ } = useStore();
useEffect(() => {
setValue('');
onSearch('');
}, [location.pathname, currentSpace]);
- const onChange = useCallback(e => {
+ const onChange = useCallback((e) => {
setValue(e.target.value);
search(e.target.value);
}, []);
- const search = useCallback(debounce((value) => {
- onSearch(value);
- }, 500), []);
+ const search = useCallback(debounce(onSearch, 500), [onSearch]);
return (
diff --git a/app/pages/Schema/index.module.less b/app/pages/Schema/index.module.less
index 3f20050e..741a3edc 100644
--- a/app/pages/Schema/index.module.less
+++ b/app/pages/Schema/index.module.less
@@ -101,8 +101,7 @@
}
.clonePopover {
- :global(.ant-popover-content) {
- position: relative;
- top: -100px;
+ &:global(.ant-popover) {
+ z-index: 1050;
}
}
\ No newline at end of file
diff --git a/app/pages/Schema/index.tsx b/app/pages/Schema/index.tsx
index a01d67e9..d396b9c5 100644
--- a/app/pages/Schema/index.tsx
+++ b/app/pages/Schema/index.tsx
@@ -1,15 +1,17 @@
-import { Button, Popconfirm, Table, message, Popover, Form, Input, Dropdown, Tooltip } from 'antd';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Button, Popconfirm, Table, message, Popover, Form, Input, Dropdown, Tooltip, TableColumnType } from 'antd';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { observable } from 'mobx';
import { useI18n } from '@vesoft-inc/i18n';
import Icon from '@app/components/Icon';
import { trackPageView } from '@app/utils/stat';
-import { observer } from 'mobx-react-lite';
+import { observer, useLocalObservable } from 'mobx-react-lite';
import { useStore } from '@app/stores';
+import { ISpace } from '@app/interfaces/schema';
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';
+import DDLModal from './SchemaConfig/DDLModal';
interface ICloneOperations {
space: string;
@@ -25,64 +27,152 @@ const CloneSpacePopover = (props: ICloneOperations) => {
onClone(name, space);
setVisible(false);
};
+ const stopPropagation = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+ const onCloneClick = useCallback((e: React.MouseEvent) => {
+ stopPropagation(e);
+ setVisible(true);
+ }, []);
return (
setVisible(visible)}
content={
-
-
-
-
-
- {intl.get('common.confirm')}
-
-
-
+
+
+
+
+
+
+ {intl.get('common.confirm')}
+
+
+
+
}
>
- setVisible(true)}>
+
{intl.get('schema.cloneSpace')}
);
};
+const DangerButton = (props: { onConfirm: () => void; text: React.ReactNode }) => {
+ const { text, onConfirm } = props;
+ const [open, setOpen] = useState(false);
+ const { intl } = useI18n();
+ const domRef = useRef
();
+
+ const onClick = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }, []);
+
+ return (
+ domRef.current}
+ trigger={['click']}
+ open={open}
+ onOpenChange={setOpen}
+ placement="left"
+ >
+
+
+ {text}
+
+
+
+ );
+};
+
const Schema = () => {
const { schema, moduleConfiguration } = useStore();
- const [loading, setLoading] = useState(false);
- const [searchVal, setSearchVal] = useState('');
+ const state = useLocalObservable(
+ () => {
+ const initState = {
+ loading: false,
+ searchVal: '',
+ spaces: [] as ISpace[],
+ current: 1,
+ pageSize: 10,
+ ddlModal: {
+ open: false,
+ space: '',
+ },
+ };
+ return {
+ ...initState,
+ setState(obj: Partial) {
+ Object.assign(this, obj);
+ },
+ get spacesFiltered(): ISpace[] {
+ const { searchVal, spaces } = this;
+ return searchVal ? spaces.filter((i) => i.Name.includes(searchVal)) : spaces;
+ },
+ };
+ },
+ { spaces: observable.ref },
+ );
+ const { loading, current, pageSize, spacesFiltered, ddlModal, setState } = state;
const history = useHistory();
const { intl } = useI18n();
- const { currentSpace, switchSpace, getSpacesList, deleteSpace, spaceList, cloneSpace } = schema;
+ const { currentSpace, switchSpace, clearSpace, deleteSpace, cloneSpace, getSpaces, getSpaceInfo } = schema;
const activeSpace = location.hash.slice(1);
useEffect(() => {
trackPageView('/schema');
- getSpaces();
+ init();
}, []);
const handleDeleteSpace = async (name: string) => {
- setLoading(true);
+ const { setState, spaces, current, spacesFiltered } = state;
+ setState({ loading: true });
const res = await deleteSpace(name);
- setLoading(false);
+ setState({ loading: false });
+
+ if (res.code !== 0) {
+ return;
+ }
+
+ const nextSpaces = spaces.filter((s) => s.Name !== name);
+ const nextState = { current, spaces: nextSpaces };
+ if (spacesFiltered.at(-1)?.Name === name && spacesFiltered.length % pageSize === 1) {
+ nextState.current = current > 1 ? current - 1 : current;
+ }
+ const spaces2Render = getSpaces2Render(nextState);
+ await fillSpaces(spaces2Render);
+
+ message.success(intl.get('common.deleteSuccess'));
+ if (currentSpace === name) {
+ schema.update({ currentSpace: '' });
+ localStorage.removeItem('currentSpace');
+ }
+
+ setState(nextState);
+ };
+
+ const handleClearSpace = async (name: string) => {
+ const { setState } = state;
+ setState({ loading: true });
+ const res = await clearSpace(name);
+ setState({ loading: false });
if (res.code === 0) {
- message.success(intl.get('common.deleteSuccess'));
- await getSpaces();
- if (currentSpace === name) {
- schema.update({
- currentSpace: '',
- });
- localStorage.removeItem('currentSpace');
- }
+ message.success(intl.get('common.success'));
}
};
@@ -90,24 +180,87 @@ const Schema = () => {
const ok = await switchSpace(space);
ok && history.push(`/schema/tag/list`);
}, []);
- const getSpaces = useCallback(async () => {
- setLoading(true);
- await getSpacesList();
- setLoading(false);
+
+ const init = useCallback(async () => {
+ const { setState } = state;
+ setState({ loading: true });
+ const { code, data } = await getSpaces();
+ if (code !== 0) {
+ setState({ loading: false });
+ return;
+ }
+
+ const spaceNames: string[] = data?.sort() || [];
+ const activeSpace = location.hash.slice(1);
+ if (activeSpace) {
+ const index = spaceNames.indexOf(activeSpace);
+ if (index > -1) {
+ spaceNames.splice(index, 1);
+ spaceNames.unshift(activeSpace);
+ }
+ }
+ const spaces: ISpace[] = spaceNames.map((Name) => ({ Name }));
+ const nextState = { spaces, current: 1, searchVal: '' };
+ const spaces2Render = getSpaces2Render(nextState);
+ await fillSpaces(spaces2Render);
+ setState({ ...nextState, loading: false });
+ }, []);
+
+ const fillSpaces = useCallback(async (spaces: ISpace[]) => {
+ setState({ loading: true });
+ await Promise.all(
+ spaces.map(
+ (s) =>
+ !s.ID &&
+ getSpaceInfo(s.Name).then((res) => {
+ res.code === 0 && Object.assign(s, res?.data?.tables?.[0]);
+ }),
+ ),
+ );
+ setState({ loading: false });
+ return spaces;
+ }, []);
+
+ const onSearch = useCallback(async (searchVal: string) => {
+ const spaces2Render = getSpaces2Render({ searchVal, current: 1 });
+ await fillSpaces(spaces2Render);
+ state.setState({ searchVal, current: 1 });
+ }, []);
+
+ const onPageChange = useCallback(async (current: number) => {
+ const spaces2Render = getSpaces2Render({ current });
+ await fillSpaces(spaces2Render);
+ state.setState({ current });
+ }, []);
+
+ const getSpaces2Render = useCallback((params?: Partial) => {
+ const {
+ searchVal: _searchVal = state.searchVal,
+ current: _current = state.current,
+ pageSize: _pageSize = state.pageSize,
+ spaces: _spaces = state.spaces,
+ } = params || {};
+ const nextSpaces = _searchVal ? _spaces.filter((i) => i.Name.includes(_searchVal)) : _spaces;
+ const spaceList = nextSpaces.slice((_current - 1) * _pageSize, _current * _pageSize);
+ return spaceList;
}, []);
const handleCloneSpace = useCallback(async (name: string, oldSpace: string) => {
const { code } = await cloneSpace(name, oldSpace);
if (code === 0) {
message.success(intl.get('schema.createSuccess'));
- getSpaces();
+ init();
}
}, []);
- const columns = [
+
+ const closeDDLModal = useCallback(() => setState({ ddlModal: { open: false, space: '' } }), []);
+
+ const columns: TableColumnType[] = [
{
title: intl.get('schema.No'),
- dataIndex: 'serialNumber',
- align: 'center' as const,
+ dataIndex: 'index',
+ align: 'center',
+ render: (_value, _record, index) => (current - 1) * pageSize + index + 1,
},
{
title: intl.get('schema.spaceName'),
@@ -172,63 +325,74 @@ const Schema = () => {
dataIndex: 'operation',
width: 180,
render: (_, space) => {
- if (space.ID) {
- return (
-
-
viewSpaceDetail(space.Name)}
- data-track-category="navigation"
- data-track-action="view_space_list"
- data-track-label="from_space_list"
- >
- {intl.get('common.schema')}
-
-
,
- },
- {
- key: 'clone',
- label:
,
- },
- {
- key: 'delete',
- label: (
-
handleDeleteSpace(space.Name)}
- title={intl.get('common.ask')}
- okText={intl.get('common.ok')}
- cancelText={intl.get('common.cancel')}
- >
-
- {intl.get('schema.deleteSpace')}
-
-
- ),
- },
- ],
- }}
- placement="bottomLeft"
- >
-
-
-
- );
+ if (!space.ID) {
+ return null;
}
+ return (
+
+ viewSpaceDetail(space.Name)}
+ data-track-category="navigation"
+ data-track-action="view_space_list"
+ data-track-label="from_space_list"
+ >
+ {intl.get('common.schema')}
+
+ setState({ ddlModal: { open: true, space: space.Name } })}>
+ {intl.get('schema.showDDL')}
+
+ ),
+ },
+ {
+ key: 'clone',
+ label: ,
+ },
+ {
+ key: 'clear',
+ label: (
+ handleClearSpace(space.Name)}
+ text={intl.get('schema.clearSpace')}
+ />
+ ),
+ },
+ {
+ key: 'delete',
+ label: (
+ handleDeleteSpace(space.Name)}
+ text={intl.get('schema.deleteSpace')}
+ />
+ ),
+ },
+ ],
+ }}
+ placement="bottomRight"
+ >
+
+
+
+ );
},
},
];
- const data = useMemo(() => spaceList.filter((item) => item.Name.includes(searchVal)), [spaceList, searchVal]);
+
+ const spaces2Render = spacesFiltered.slice((current - 1) * pageSize, current * pageSize);
+
return (
{intl.get('schema.spaceList')}
-
+
{!moduleConfiguration.schema?.disableCreateSpace && (
{
(item.Name === activeSpace ? styles.active : '')}
+ pagination={{
+ current,
+ pageSize,
+ total: spacesFiltered.length,
+ onChange: onPageChange,
+ }}
/>
+
);
};
diff --git a/app/stores/schema.ts b/app/stores/schema.ts
index cad1bb11..522ba233 100644
--- a/app/stores/schema.ts
+++ b/app/stores/schema.ts
@@ -145,23 +145,18 @@ export class SchemaStore {
return code === 0 ? data.tables[0]['Create Space'] : null;
};
- getSpacesList = async () => {
- const res = await this.getSpaces();
- const activeSpace = location.hash.slice(1);
- if (res.data) {
- const spaces: ISpace[] = [];
- await Promise.all(
- res.data.map(async (item, i) => {
- const { code, data } = await this.getSpaceInfo(item);
- if (code === 0) {
- const space = (data.tables && data.tables[0]) || {};
- space.serialNumber = space.Name === activeSpace ? 0 : i + 1;
- spaces.push(space);
- }
- }),
- );
- this.update({ spaceList: spaces.sort((a, b) => a.serialNumber - b.serialNumber) });
- }
+ getSpacesList = async (spaces: string[]) => {
+ const result: ISpace[] = [];
+ await Promise.all(
+ spaces.map(async (item, index) => {
+ const { code, data } = await this.getSpaceInfo(item);
+ if (code === 0) {
+ const space = (data.tables && data.tables[0]) || {};
+ result[index] = space;
+ }
+ }),
+ );
+ return result;
};
deleteSpace = async (space: string) => {
@@ -193,6 +188,20 @@ export class SchemaStore {
)) as any;
return { code, data };
};
+ clearSpace = async (space: string) => {
+ const { code, data } = (await service.execNGQL(
+ {
+ gql: `CLEAR SPACE IF EXISTS ${handleKeyword(space)}`,
+ },
+ {
+ trackEventConfig: {
+ category: 'schema',
+ action: 'clear_space',
+ },
+ },
+ )) as any;
+ return { code, data };
+ };
createSpace = async (gql: string) => {
const { code, data, message } = await service.execNGQL(
diff --git a/app/utils/websocket.ts b/app/utils/websocket.ts
index 10de90be..8df52c44 100644
--- a/app/utils/websocket.ts
+++ b/app/utils/websocket.ts
@@ -267,6 +267,9 @@ export class NgqlRunner {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
const flag = await this.reConnect();
if (!flag) {
+ if (config.noTip !== true) {
+ message.error('WebSocket reconnect failed');
+ }
return Promise.resolve({ code: -1, message: 'WebSocket reconnect failed' });
}
}
diff --git a/package-lock.json b/package-lock.json
index 84e40889..1e975b74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8283,9 +8283,9 @@
}
},
"node_modules/typescript": {
- "version": "5.1.3",
- "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.1.3.tgz",
- "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -14875,9 +14875,9 @@
"dev": true
},
"typescript": {
- "version": "5.1.3",
- "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.1.3.tgz",
- "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true
},
"universalify": {