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 && ( +
- -
} -
- - + +
+ )} +
+
); }; 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={ -
- - - - - - -
+
+
+ + + + + + +
+
} > -
); }; +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" + > + + + + + ); +}; + 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 ( -
- - , - }, - { - key: 'clone', - label: , - }, - { - key: 'delete', - label: ( - handleDeleteSpace(space.Name)} - title={intl.get('common.ask')} - okText={intl.get('common.ok')} - cancelText={intl.get('common.cancel')} - > - - - ), - }, - ], - }} - placement="bottomLeft" - > - - -
- ); + if (!space.ID) { + return null; } + return ( +
+ + 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": {