diff --git a/app/App.less b/app/app.less similarity index 93% rename from app/App.less rename to app/app.less index 7511fd52..3c859998 100644 --- a/app/App.less +++ b/app/app.less @@ -55,7 +55,7 @@ text-align: center; border: none; white-space: nowrap; - color: #465b7a; + color: @darkBlue; &:not(.ant-radio-button-wrapper-checked) { background: none; @@ -70,6 +70,8 @@ .ant-btn.warning-btn { color: @red; border-color: @red; + display: inline-flex; + justify-content: center; svg { width: 20px; @@ -85,6 +87,8 @@ .ant-btn.primary-btn { color: @blue; border-color: @blue; + display: inline-flex; + justify-content: center; svg { width: 20px; @@ -100,6 +104,8 @@ .ant-btn.cancel-btn { color: @darkGray; border-color: @darkGray; + display: inline-flex; + justify-content: center; svg { width: 20px; diff --git a/app/common.less b/app/common.less index 2fd5a506..6cc35714 100644 --- a/app/common.less +++ b/app/common.less @@ -6,3 +6,4 @@ @blue: #2F80ED; @lightGray: #E9EDEF; @lightBlue: #F3F6F9; +@darkBlue: #465B7A; diff --git a/app/components/Breadcrumb/index.less b/app/components/Breadcrumb/index.less index b4908eb3..4ff563ac 100644 --- a/app/components/Breadcrumb/index.less +++ b/app/components/Breadcrumb/index.less @@ -1,3 +1,4 @@ +@import '~@app/common.less'; .nebula-breadcrumb { height: 60px; background-color: #fff; @@ -32,7 +33,7 @@ } & > span:last-child { a, span { - color: #465B7A; + color: @darkBlue; font-weight: 600; } } diff --git a/app/components/Instruction/index.less b/app/components/Instruction/index.less new file mode 100644 index 00000000..8fe6310a --- /dev/null +++ b/app/components/Instruction/index.less @@ -0,0 +1,4 @@ +.icon-instruction { + margin: 0 2px; + color: rgba(140, 140, 140, 1); +} diff --git a/app/components/Instruction/index.tsx b/app/components/Instruction/index.tsx new file mode 100644 index 00000000..ccbf92fa --- /dev/null +++ b/app/components/Instruction/index.tsx @@ -0,0 +1,19 @@ +import { Tooltip } from 'antd'; +import Icon from '@app/components/Icon'; +import React from 'react'; + +import './index.less'; + +const Instruction = (props: { description: string; onClick?: () => void }) => { + return ( + + + + ); +}; + +export default Instruction; diff --git a/app/config/locale/en-US.json b/app/config/locale/en-US.json index 2a3f3b7c..b7ad7aaa 100644 --- a/app/config/locale/en-US.json +++ b/app/config/locale/en-US.json @@ -337,6 +337,7 @@ "indexType": "Index Type", "indexName": "Index Name", "indexFields": "Indexed Properties", + "associateName": "Associated {type} name", "dragSorting": "(Drag to Sort)", "selectFields": "Choose Property", "indexedLength": "Indexed length", @@ -346,22 +347,8 @@ "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?" - }, - "menu": { - "use": "Use Manual", - "release": "New Version", - "forum": "Help Forum", - "nGql": "nGQL" - }, - "link": { - "nGQLHref": "https://docs.nebula-graph.io/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", - "mannualHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", - "versionLogHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-release-note/", - "forumLink": "https://discuss.nebula-graph.io/" - }, - "_schema": { - "spaceList": "Graph Space List", + "leavePagePrompt": "You have unsaved changes to the record on this tab. If you leave this tab without saving the changes, they will be lost. Are you sure that you want to leave?", + "rebuild": "Rebuild", "createSpace": "Create Space", "No": "No", "spaceName": "Name", @@ -374,12 +361,10 @@ "group": "Group", "comment": "Comment", "operations": "Operations", - "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.", "spaceNameEnter": "Please enter the space name", "propertyCount": "Property Num", "configTypeList": "{space} {type} List", "configTypeAction": "{action} {type}", - "defineFields": "Define Properties", "timestampFormat": "Supported data inserting methods:
1. call function now()
2. call function timestamp(), for example: timestamp('2021-07-05T06:18:43.984000')
3. Input the timestamp directly, namely the number of seconds from 1970-01-01 00:00:00", "dateFormat": "Supported data inserting methods:
Call function date(), for example: date('2021-03-17')", "timeFormat": "Supported data inserting methods:
Call function time(), for example: time('17:53:59')", @@ -390,5 +375,17 @@ "geography(polygon)Format": "Supported data inserting methods:
Call function ST_GeogFromText('POLYGON()'), for example:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", "durationFormat": "Supported data inserting methods:
Call function duration(), for example:duration({years: 1, seconds: 0})", "setTTL": "Set TTL (Time To Live)" + }, + "menu": { + "use": "Use Manual", + "release": "New Version", + "forum": "Help Forum", + "nGql": "nGQL" + }, + "link": { + "nGQLHref": "https://docs.nebula-graph.io/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", + "mannualHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", + "versionLogHref": "https://docs.nebula-graph.io/3.0.0/nebula-studio/about-studio/st-ug-release-note/", + "forumLink": "https://discuss.nebula-graph.io/" } } diff --git a/app/config/locale/zh-CN.json b/app/config/locale/zh-CN.json index f8c8363a..25fb3723 100644 --- a/app/config/locale/zh-CN.json +++ b/app/config/locale/zh-CN.json @@ -333,6 +333,7 @@ "indexType": "索引类型", "indexName": "索引名称", "indexFields": "索引属性", + "associateName": "关联 {type} 名称", "dragSorting": "(可拖拽排序)", "selectFields": "选择关联的属性", "indexedLength": "索引长度", @@ -342,22 +343,8 @@ "backToEdgeList": "返回边类型列表", "backToIndexList": "返回索引列表", "leavePage": "是否离开当前页面", - "leavePagePrompt": "离开当前页面后,未保存的记录将丢失" - }, - "menu": { - "use": "使用手册", - "release": "新发布", - "forum": "求助论坛", - "nGql": "nGQL" - }, - "link": { - "nGQLHref": "https://docs.nebula-graph.com.cn/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", - "mannualHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", - "versionLogHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-release-note/", - "forumLink": "https://discuss.nebula-graph.com.cn/" - }, - "_schema": { - "spaceList": "图空间列表", + "leavePagePrompt": "离开当前页面后,未保存的记录将丢失", + "rebuild": "重建索引", "createSpace": "创建图空间", "No": "序号", "spaceName": "名称", @@ -370,12 +357,10 @@ "group": "Group", "comment": "Comment", "operations": "操作", - "useSpaceErrTip": "图空间未找到。立刻尝试使用刚创建的图空间可能会失败,因为创建是异步实现的。为确保数据同步,后续操作能顺利进行,请等待 2 个心跳周期(20 秒)。", "spaceNameEnter": "请输入图空间名称", "propertyCount": "属性数量", "configTypeList": "{space} {type}列表", "configTypeAction": "{action}{type}", - "defineFields": "定义属性", "timestampFormat": "时间类型支持插入方式:
1. 调用函数 now()
2. 调用函数 timestamp(),例如:timestamp('2021-07-05T06:18:43.984000')
3. 直接输入时间戳,即从 1970-01-01 00:00:00 开始的秒数", "dateFormat": "日期类型支持插入方式:
调用函数 date(),例如:date('2021-03-17')", "timeFormat": "时间类型支持插入方式:
调用函数 time(),例如time('17:53:59')", @@ -386,5 +371,17 @@ "geography(polygon)Format": "geo(polygon) 类型支持插入方式:
调用函数 ST_GeogFromText('POLYGON()'),例如:ST_GeogFromText('POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))')", "durationFormat": "duration 类型支持插入方式:
调用函数 duration(),例如:duration({years: 1, seconds: 0})", "setTTL": "设置TTL(存活时间)" + }, + "menu": { + "use": "使用手册", + "release": "新发布", + "forum": "求助论坛", + "nGql": "nGQL" + }, + "link": { + "nGQLHref": "https://docs.nebula-graph.com.cn/3.0.0/3.ngql-guide/1.nGQL-overview/1.overview/", + "mannualHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-what-is-graph-studio/", + "versionLogHref": "https://docs.nebula-graph.com.cn/3.0.0/nebula-studio/about-studio/st-ug-release-note/", + "forumLink": "https://discuss.nebula-graph.com.cn/" } } \ No newline at end of file diff --git a/app/interfaces/schema.ts b/app/interfaces/schema.ts index e44d59f8..5fb1b8ee 100644 --- a/app/interfaces/schema.ts +++ b/app/interfaces/schema.ts @@ -65,3 +65,12 @@ export interface IAlterForm { action: AlterType; config: IAlterConfig; } + +export enum IJobStatus { + queue = 'QUEUE', + running = 'RUNNING', + finished = 'FINISHED', + failed = 'FAILED', + stopped = 'STOPPED', + removed = 'REMOVED', +} \ No newline at end of file diff --git a/app/pages/Import/TaskCreate/SchemaConfig/index.less b/app/pages/Import/TaskCreate/SchemaConfig/index.less index 193a7f8e..c479f756 100644 --- a/app/pages/Import/TaskCreate/SchemaConfig/index.less +++ b/app/pages/Import/TaskCreate/SchemaConfig/index.less @@ -9,7 +9,7 @@ display: flex; align-items: center; font-size: 14px; - color: #465B7A; + color: @darkBlue; } .btn-preview { font-weight: bold; diff --git a/app/pages/Schema/SchemaConfig/Create/CommonCreate/PropertiesForm.tsx b/app/pages/Schema/SchemaConfig/Create/CommonCreate/PropertiesForm.tsx index 0bad1491..9e7ad2ae 100644 --- a/app/pages/Schema/SchemaConfig/Create/CommonCreate/PropertiesForm.tsx +++ b/app/pages/Schema/SchemaConfig/Create/CommonCreate/PropertiesForm.tsx @@ -65,7 +65,7 @@ const PropertiesForm = (props: IProps) => {
- {intl.get('_schema.defineFields')} + {intl.get('schema.defineFields')} @@ -164,7 +164,7 @@ const PropertiesForm = (props: IProps) => { diff --git a/app/pages/Schema/SchemaConfig/Create/CommonCreate/TTLForm.tsx b/app/pages/Schema/SchemaConfig/Create/CommonCreate/TTLForm.tsx index 2f58d5d6..784cc215 100644 --- a/app/pages/Schema/SchemaConfig/Create/CommonCreate/TTLForm.tsx +++ b/app/pages/Schema/SchemaConfig/Create/CommonCreate/TTLForm.tsx @@ -33,7 +33,7 @@ const formRef = ((props: IProps) => {
- {intl.get('_schema.setTTL')} + {intl.get('schema.setTTL')} diff --git a/app/pages/Schema/SchemaConfig/Create/IndexCreate/DraggableTags.tsx b/app/pages/Schema/SchemaConfig/Create/IndexCreate/DraggableTags.tsx new file mode 100644 index 00000000..332a4301 --- /dev/null +++ b/app/pages/Schema/SchemaConfig/Create/IndexCreate/DraggableTags.tsx @@ -0,0 +1,88 @@ +import { Tag } from 'antd'; +import React, { Component } from 'react'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; + +const reorder = (list: string[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + + const [removed] = result.splice(startIndex, 1); + + result.splice(endIndex, 0, removed); + return result; +}; + +const getItemStyle = (_isDragging, draggableStyle) => ({ + ...draggableStyle, +}); + +interface IProps { + data: string[]; + updateData: (data: string[]) => void; + removeData: (field: string) => void; +} + +export default class DraggableTags extends Component { + constructor(props) { + super(props); + this.onDragEnd = this.onDragEnd.bind(this); + } + + onDragEnd(result) { + if (!result.destination) { + return; + } + // adjust tag order when drop the element + const items: string[] = reorder( + this.props.data, + result.source.index, + result.destination.index, + ); + this.props.updateData(items); + } + + render() { + const list = this.props.data.map(item => ({ + id: `field-${item}`, + content: ( + this.props.removeData(item)}> + {item} + + ), + })); + return ( + + + {(provided, _snapshot) => ( +
+ {list.map((item, index) => ( + + {(provided, snapshot) => ( +
+ {item.content} +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ ); + } +} diff --git a/app/pages/Schema/SchemaConfig/Create/IndexCreate/FieldSelectModal.tsx b/app/pages/Schema/SchemaConfig/Create/IndexCreate/FieldSelectModal.tsx new file mode 100644 index 00000000..5d69574b --- /dev/null +++ b/app/pages/Schema/SchemaConfig/Create/IndexCreate/FieldSelectModal.tsx @@ -0,0 +1,108 @@ +import { Button, Input, Modal, Select, message } from 'antd'; +import React, { useState } from 'react'; +import intl from 'react-intl-universal'; +import { observer } from 'mobx-react-lite'; +import { IField } from '@app/interfaces/schema'; +import { POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; +import Instruction from '@app/components/Instruction'; +const Option = Select.Option; + +import './index.less'; + +interface IProps { + visible: boolean; + source: IField[]; + onClose: () => void; + onAddField: (field: string) => void; +} + +const FieldSelectModal = (props: IProps) => { + const { source, onAddField, onClose, visible } = props; + const [selectedField, setSelectedField] = useState(null); + const [indexLength, setIndexLength] = useState(''); + const handleFieldAdd = () => { + if ( + selectedField?.Type === 'string' && + !indexLength.match(POSITIVE_INTEGER_REGEX) + ) { + return message.warning(intl.get('schema.indexedLengthRequired')); + } + const newField = + selectedField?.Type === 'string' + ? selectedField.Field + `(${indexLength})` + : selectedField!.Field; + onAddField(newField); + handleClose(); + }; + + const handleClose = () => { + setSelectedField(null); + setIndexLength(''); + onClose(); + }; + + const handleFieldSelect = (value: string) => { + const selectedData = source.filter( + field => field.Field === value, + )[0]; + setSelectedField(selectedData); + if(selectedData.Type.startsWith('fixed_string')) { + setIndexLength(selectedData.Type.replace(/[fixed_string(|)]/g, '')); + } + }; + return ( + + + + + } + > +
+ {intl.get('schema.selectFields')}: + +
+ {/* string & fixed string should supply length parameter */} + {selectedField?.Type.includes('string') && ( +
+ {intl.get('schema.indexedLength')}: + setIndexLength(e.target.value)} + /> + +
+ )} +
+ ); +}; + +export default observer(FieldSelectModal); diff --git a/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.less b/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.less new file mode 100644 index 00000000..660d0f44 --- /dev/null +++ b/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.less @@ -0,0 +1,54 @@ +@import '~@app/common.less'; +.index-create-page { + .btn-field-add { + margin-bottom: 20px; + } + .view-row { + padding-top: 24px; + border-top: 1px solid @gray; + } + .drag-item { + display: flex; + align-items: center; + border: none; + border-radius: 3px; + background: @gray; + height: 38px; + font-family: Robote-Reqular, sans-serif; + font-weight: 500; + font-size: 14px; + padding: 12px 12px 12px 16px; + .ant-tag-close-icon { + margin-left: 10px; + color: @darkBlue; + svg { + width: 14px; + height: 14px; + } + } + } +} + +.modal-field-add { + .ant-modal-body { + text-align: center; + } + + .modal-item { + margin-bottom: 20px; + } + + .select-field { + width: 200px; + margin-left: 10px; + } + + .input-index-length { + width: 140px; + margin-left: 10px; + } + + .ant-modal-footer { + border-top: none; + } +} \ No newline at end of file diff --git a/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.tsx b/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.tsx new file mode 100644 index 00000000..1d4960dd --- /dev/null +++ b/app/pages/Schema/SchemaConfig/Create/IndexCreate/index.tsx @@ -0,0 +1,232 @@ +import { Button, Col, Form, Input, Row, Select, message } from 'antd'; +import React, { useEffect, useState } from 'react'; +import intl from 'react-intl-universal'; +import { observer } from 'mobx-react-lite'; +import { nameRulesFn } from '@app/config/rules'; +import { useHistory, useParams } from 'react-router-dom'; +import GQLCodeMirror from '@app/components/GQLCodeMirror'; +import { getIndexCreateGQL } from '@app/utils/gql'; +import { useStore } from '@app/stores'; +import { IField, IndexType } from '@app/interfaces/schema'; +import Icon from '@app/components/Icon'; +import { trackPageView } from '@app/utils/stat'; + +import FieldSelectModal from './FieldSelectModal'; +import DraggableTags from './DraggableTags'; +const Option = Select.Option; + +import './index.less'; + + +const formItemLayout = { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 11, + }, +}; + +const IndexCreate = () => { + const history = useHistory(); + const { space } = useParams() as {space :string }; + const [loading, setLoading] = useState(false); + const { schema: { createIndex, getTags, getEdges, getTagOrEdgeInfo } } = useStore(); + const [gql, setGql] = useState(''); + const [form] = Form.useForm(); + const [typeList, setTypeList] = useState([]); + const [fieldList, setFieldList] = useState([]); + const [visible, setVisible] = useState(false); + const updateGql = () => { + const { name, type, associate, fields, comment } = form.getFieldsValue(); + const currentGQL = name ? getIndexCreateGQL({ + type, + name, + associate, + fields, + comment, + }) : ''; + setGql(currentGQL); + }; + const handleCreate = async(values) => { + const { name, type, associate, fields, comment } = values; + setLoading(true); + const res = await createIndex({ + name, + type, + associate, + fields, + comment, + }); + setLoading(false); + if (res.code === 0) { + message.success(intl.get('schema.createSuccess')); + history.push(`/schema/${space}/index/list/${type}`); + } + }; + + const getAssociatedList = async(type?: IndexType) => { + const associatedType = type ? type : form.getFieldValue('type'); + const data = + associatedType === 'tag' + ? await getTags() + : await getEdges(); + if (data) { + setTypeList(data); + form.setFieldsValue({ + associate: '', + fields: [], + }); + } + }; + + const getFieldList = async value => { + const type = form.getFieldValue('type'); + const res = await getTagOrEdgeInfo(type, value); + if (res.code === 0) { + setFieldList(res.data.tables); + form.setFieldsValue({ + fields: [], + }); + } + }; + + const updateFields = (data: string[]) => { + form.setFieldsValue({ + fields: data, + }); + updateGql(); + }; + + const removeField = (field: string) => { + const fields = form.getFieldValue('fields'); + form.setFieldsValue({ + fields: fields.filter(i => i !== field), + }); + updateGql(); + }; + useEffect(() => { + trackPageView('/schema/config/index/create'); + getAssociatedList('tag'); + }, []); + return ( +
+
+ + + + + + + + + {({ getFieldValue }) => { + const type = getFieldValue('type'); + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + {intl.get('schema.indexFields')} + + {intl.get('schema.dragSorting')} + + }> + + + + + {({ getFieldValue }) => { + const fields = getFieldValue('fields') || []; + const filterList = fieldList.filter( + item => !fields.some(field => field.startsWith(item.Field)), + ); + return <> + + setVisible(false)} + onAddField={newField => updateFields([...fields, newField])} + /> + ; + }} + + + +
+ +
+
+ +
+ + +
+
+
+
+ ); +}; + +export default observer(IndexCreate); diff --git a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesForm.tsx b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesForm.tsx index f137dc5b..61b6d738 100644 --- a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesForm.tsx +++ b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesForm.tsx @@ -148,7 +148,7 @@ const PropertiesForm = (props: IProps) => { onFinish={handlePropertyUpdate}> - {intl.get('_schema.defineFields')} + {intl.get('schema.defineFields')} diff --git a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesRow.tsx b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesRow.tsx index 57005c54..d080b265 100644 --- a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesRow.tsx +++ b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/PropertiesRow.tsx @@ -139,7 +139,7 @@ export const EditRow = (props: IEditProps) => { diff --git a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/TTLForm.tsx b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/TTLForm.tsx index a6ba61a2..3b33f74b 100644 --- a/app/pages/Schema/SchemaConfig/Edit/CommonEdit/TTLForm.tsx +++ b/app/pages/Schema/SchemaConfig/Edit/CommonEdit/TTLForm.tsx @@ -137,7 +137,7 @@ const formRef = ((props: IProps) => { layout="vertical"> - {intl.get('_schema.setTTL')} + {intl.get('schema.setTTL')} {!isEdit && diff --git a/app/pages/Schema/SchemaConfig/List/CommonLayout/index.less b/app/pages/Schema/SchemaConfig/List/CommonLayout/index.less index 2a1c5cf0..a2f98198 100644 --- a/app/pages/Schema/SchemaConfig/List/CommonLayout/index.less +++ b/app/pages/Schema/SchemaConfig/List/CommonLayout/index.less @@ -26,8 +26,9 @@ } } .operation { + display: flex; button { - width: 70px; + min-width: 70px; } button:not(:last-child) { margin-right: 15px; diff --git a/app/pages/Schema/SchemaConfig/List/Edge/index.tsx b/app/pages/Schema/SchemaConfig/List/Edge/index.tsx index 2299014c..31e440ba 100644 --- a/app/pages/Schema/SchemaConfig/List/Edge/index.tsx +++ b/app/pages/Schema/SchemaConfig/List/Edge/index.tsx @@ -70,7 +70,7 @@ const EdgeList = () => { width: '33%', }, { - title: intl.get('_schema.propertyCount'), + title: intl.get('schema.propertyCount'), dataIndex: 'count', width: '33%', render: (_, edge) => edge.fields?.length || 0 @@ -82,30 +82,28 @@ const EdgeList = () => { if (edge.name) { return (
-
- - { - handleDeleteEdge(edge.name); - }} - title={intl.get('common.ask')} - okText={intl.get('common.ok')} - cancelText={intl.get('common.cancel')} + - -
+ + + + { + handleDeleteEdge(edge.name); + }} + title={intl.get('common.ask')} + okText={intl.get('common.ok')} + cancelText={intl.get('common.cancel')} + > + +
); } diff --git a/app/pages/Schema/SchemaConfig/List/Index/index.tsx b/app/pages/Schema/SchemaConfig/List/Index/index.tsx index 2e264e11..7daaef51 100644 --- a/app/pages/Schema/SchemaConfig/List/Index/index.tsx +++ b/app/pages/Schema/SchemaConfig/List/Index/index.tsx @@ -1,5 +1,5 @@ import { Button, Popconfirm, Radio, Table, message } from 'antd'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import intl from 'react-intl-universal'; import { Link, useHistory, useParams } from 'react-router-dom'; import Icon from '@app/components/Icon'; @@ -40,25 +40,54 @@ function renderIndexInfo(index: IIndexList) { const IndexList = () => { const { space, module } = useParams() as {space :string, module: string}; - const { schema: { indexList, deleteIndex, getIndexList } } = useStore(); + const { schema: { indexList, deleteIndex, getIndexList, rebuildIndex, getRebuildIndexes } } = useStore(); const [searchVal, setSearchVal] = useState(''); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [indexType, setIndexType] = useState(module as any || 'tag'); + const [rebuildList, setRebuildList] = useState(null); + const rebuildTimer = useRef(null); const history = useHistory(); const getData = async() => { setLoading(true); await getIndexList(indexType); + await getRebuildData(indexType); setSearchVal(''); setLoading(false); }; - const handleDeleteIndex = async(type: IndexType, name: string) => { + const handleDeleteIndex = async(event, type: IndexType, name: string) => { + event.stopPropagation(); const res = await deleteIndex({ type, name }); if (res.code === 0) { message.success(intl.get('common.deleteSuccess')); getData(); } }; + + const getRebuildData = async(type: IndexType) => { + rebuildTimer.current && clearTimeout(rebuildTimer.current); + const data = await getRebuildIndexes(type); + if (data && data.length > 0) { + setRebuildList(data); + rebuildTimer.current = setTimeout(() => { + getRebuildData(type); + }, 2000); + } else { + setRebuildList([]); + rebuildTimer.current && clearTimeout(rebuildTimer.current); + } + }; + + const handleRebuild = async(event, type: IndexType, name: string) => { + event.stopPropagation(); + const res = await rebuildIndex({ + type, + name, + }); + if (res.code === 0) { + getRebuildData(type); + } + }; const columns = useMemo(() => [ { title: intl.get('common.name'), @@ -80,47 +109,48 @@ const IndexList = () => { title: intl.get('common.operation'), dataIndex: 'operation', render: (_, index) => { + const isRebuild = rebuildList?.some(i => i === index.name); if (index.name) { return (
-
- + { + handleDeleteIndex(e, indexType, index.name); + }} + title={intl.get('common.ask')} + okText={intl.get('common.ok')} + cancelText={intl.get('common.cancel')} + > + - { - handleDeleteIndex(indexType, index.name); - }} - title={intl.get('common.ask')} - okText={intl.get('common.ok')} - cancelText={intl.get('common.cancel')} - > - - -
+
); } }, }, - ], [indexType, Cookie.get('lang')]); + ], [indexType, Cookie.get('lang'), rebuildList]); const handleTabChange = e => { + rebuildTimer.current && clearTimeout(rebuildTimer.current); setIndexType(e.target.value); + setRebuildList([]); history.replace(`/schema/${space}/index/list/${e.target.value}`); }; useEffect(() => { + rebuildTimer.current && clearTimeout(rebuildTimer.current); module && setIndexType(module as IndexType); getData(); + return () => { + rebuildTimer.current && clearTimeout(rebuildTimer.current); + }; }, [module, space]); useEffect(() => { setData(sortByFieldAndFilter({ diff --git a/app/pages/Schema/SchemaConfig/List/Tag/index.tsx b/app/pages/Schema/SchemaConfig/List/Tag/index.tsx index 21fc06fa..ff2b4c72 100644 --- a/app/pages/Schema/SchemaConfig/List/Tag/index.tsx +++ b/app/pages/Schema/SchemaConfig/List/Tag/index.tsx @@ -71,7 +71,7 @@ const TagList = () => { width: '33%', }, { - title: intl.get('_schema.propertyCount'), + title: intl.get('schema.propertyCount'), dataIndex: 'count', width: '33%', render: (_, tag) => tag.fields?.length || 0 @@ -85,30 +85,28 @@ const TagList = () => { } return (
-
- - { - handleDeleteTag(tag.name); - }} - title={intl.get('common.ask')} - okText={intl.get('common.ok')} - cancelText={intl.get('common.cancel')} + - -
+ + + + { + handleDeleteTag(tag.name); + }} + title={intl.get('common.ask')} + okText={intl.get('common.ok')} + cancelText={intl.get('common.cancel')} + > + +
); }, diff --git a/app/pages/Schema/SchemaConfig/index.less b/app/pages/Schema/SchemaConfig/index.less index 2ffe73bf..a7c29aa7 100644 --- a/app/pages/Schema/SchemaConfig/index.less +++ b/app/pages/Schema/SchemaConfig/index.less @@ -16,7 +16,7 @@ border: none; } .ant-select-arrow { - color: #465B7A; + color: @darkBlue; } } } diff --git a/app/pages/Schema/SchemaConfig/index.tsx b/app/pages/Schema/SchemaConfig/index.tsx index f473d3d5..50da781d 100644 --- a/app/pages/Schema/SchemaConfig/index.tsx +++ b/app/pages/Schema/SchemaConfig/index.tsx @@ -8,6 +8,7 @@ import TagList from './List/Tag'; import EdgeList from './List/Edge'; import IndexList from './List/Index/index'; import CommonCreate from './Create/CommonCreate'; +import IndexCreate from './Create/IndexCreate'; import CommonEdit from './Edit/CommonEdit'; import { observer } from 'mobx-react-lite'; import { useStore } from '@app/stores'; @@ -26,7 +27,7 @@ const SchemaConfig = () => { return [ { path: '/schema', - breadcrumbName: intl.get('_schema.spaceList'), + breadcrumbName: intl.get('schema.spaceList'), }, { path: '#', @@ -37,15 +38,15 @@ const SchemaConfig = () => { return [ { path: '/schema', - breadcrumbName: intl.get('_schema.spaceList'), + breadcrumbName: intl.get('schema.spaceList'), }, { path: `/schema/${space}/${type}/list`, - breadcrumbName: intl.get('_schema.configTypeList', { space, type: intl.get(`common.${type}`) }), + breadcrumbName: intl.get('schema.configTypeList', { space, type: intl.get(`common.${type}`) }), }, { path: '#', - breadcrumbName: intl.get('_schema.configTypeAction', { + breadcrumbName: intl.get('schema.configTypeAction', { action: intl.get(`common.${action}`), type: intl.get(`common.${type}`) }), }, @@ -133,11 +134,11 @@ const SchemaConfig = () => { render={() => } /> - {/* */} + component={IndexCreate} + />
diff --git a/app/pages/Schema/SpaceCreate/index.tsx b/app/pages/Schema/SpaceCreate/index.tsx index 847208cd..ac96023f 100644 --- a/app/pages/Schema/SpaceCreate/index.tsx +++ b/app/pages/Schema/SpaceCreate/index.tsx @@ -44,11 +44,11 @@ const SpaceCreate = () => { const routes = useMemo(() => [ { path: '/schema', - breadcrumbName: intl.get('_schema.spaceList'), + breadcrumbName: intl.get('schema.spaceList'), }, { path: '#', - breadcrumbName: intl.get('_schema.createSpace'), + breadcrumbName: intl.get('schema.createSpace'), }, ], [Cookie.get('lang')]); @@ -103,7 +103,7 @@ const SpaceCreate = () => { - + diff --git a/app/pages/Schema/index.tsx b/app/pages/Schema/index.tsx index da24dd1e..da8ecb8b 100644 --- a/app/pages/Schema/index.tsx +++ b/app/pages/Schema/index.tsx @@ -37,7 +37,7 @@ const Schema = () => { if (!err) { history.push(`/schema/${space}/tag/list`); } else if (err && err.toLowerCase().includes('spacenotfound')) { - message.warning(intl.get('_schema.useSpaceErrTip')); + message.warning(intl.get('schema.useSpaceErrTip')); } }; const getSpaces = async() => { @@ -47,12 +47,12 @@ const Schema = () => { }; const columns = [ { - title: intl.get('_schema.No'), + title: intl.get('schema.No'), dataIndex: 'serialNumber', align: 'center' as const, }, { - title: intl.get('_schema.spaceName'), + title: intl.get('schema.spaceName'), dataIndex: 'Name', render: value => ( rootStoreRef.current; export const storeContext = createContext(rootStore); diff --git a/app/stores/schema.ts b/app/stores/schema.ts index c60260bd..050c31a6 100644 --- a/app/stores/schema.ts +++ b/app/stores/schema.ts @@ -1,6 +1,6 @@ import { action, makeAutoObservable, observable } from 'mobx'; import service from '@app/config/service'; -import { IAlterForm, IEdge, IIndexList, ISchemaType, ISpace, ITag, ITree, IndexType } from '@app/interfaces/schema'; +import { IAlterForm, IEdge, IIndexList, IJobStatus, ISchemaType, ISpace, ITag, ITree, IndexType } from '@app/interfaces/schema'; import { handleKeyword } from '@app/utils/function'; import { findIndex } from 'lodash'; import { @@ -250,7 +250,7 @@ export class SchemaStore { } // tags - async getTags() { + getTags = async() => { const { code, data } = (await service.execNGQL({ gql: ` SHOW TAGS; @@ -382,10 +382,11 @@ export class SchemaStore { `, })) as any; if (code === 0) { + const key = type === 'tag' ? 'By Tag' : 'By Edge'; const indexes = data.tables.map(item => { return { name: item['Index Name'], - owner: item['By Tag'], + owner: item[key], }; }); this.update({ @@ -521,6 +522,7 @@ export class SchemaStore { )) as any; return { code, data }; } + createIndex = async(payload: { type: IndexType; name: string; @@ -542,6 +544,39 @@ export class SchemaStore { )) as any; return { code, data, message }; } + + rebuildIndex = async(payload: { type: IndexType; name: string }) => { + const { type, name } = payload; + const { code, data } = (await service.execNGQL( + { + gql: ` + REBUILD ${type} INDEX ${handleKeyword(name)} + `, + }, + { + trackEventConfig: { + category: 'schema', + action: 'rebuild_index', + }, + }, + )) as any; + return { code, data }; + } + + getRebuildIndexes = async(type: IndexType) => { + const { code, data } = (await service.execNGQL({ + gql: ` + SHOW ${type} INDEX STATUS + `, + })) as any; + if (code === 0) { + return data.tables + .filter(item => item['Index Status'] !== IJobStatus.finished) + .map(item => item.Name); + } + return null; + } + } const schemaStore = new SchemaStore(); diff --git a/app/utils/gql.ts b/app/utils/gql.ts index 843b0e76..8b93021e 100644 --- a/app/utils/gql.ts +++ b/app/utils/gql.ts @@ -218,13 +218,13 @@ export const getIndexCreateGQL = (params: { name: string; associate: string; comment?: string; - fields: string[]; + fields?: string[]; }) => { const { type, name, associate, fields, comment } = params; const combine = associate - ? `on ${handleKeyword(associate)}(${fields.join(', ')})` + ? `on ${handleKeyword(associate)}(${fields?.join(', ')})` : ''; - const gql = `CREATE ${type} INDEX ${handleKeyword(name)} ${combine} ${ + const gql = `CREATE ${type.toUpperCase()} INDEX ${handleKeyword(name)} ${combine} ${ comment ? `COMMENT = "${comment}"` : '' }`; return gql;