Explorar o código

finish editing entity dialog

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang hai 11 meses
pai
achega
a6ec9b4eb6

+ 7 - 0
client/src/http/Data.service.ts

@@ -18,6 +18,13 @@ export class DataService extends BaseModel {
     });
     });
   }
   }
 
 
+  static upsert(collectionName: string, param: InsertDataParam) {
+    return super.create({
+      path: `/collections/${collectionName}/upsert`,
+      data: param,
+    });
+  }
+
   static deleteEntities(collectionName: string, param: DeleteEntitiesReq) {
   static deleteEntities(collectionName: string, param: DeleteEntitiesReq) {
     return super.update({
     return super.update({
       path: `/collections/${collectionName}/entities`,
       path: `/collections/${collectionName}/entities`,

+ 1 - 0
client/src/i18n/cn/button.ts

@@ -55,6 +55,7 @@ const btnTrans = {
   downloadDisabledTooltip: '导出前请先选择数据。',
   downloadDisabledTooltip: '导出前请先选择数据。',
   deleteDisableTooltip: '请至少选择一个要删除的项目。',
   deleteDisableTooltip: '请至少选择一个要删除的项目。',
   editEntityDisabledTooltip: '一次只能编辑一个entity。',
   editEntityDisabledTooltip: '一次只能编辑一个entity。',
+  editEntityDisabledTooltipAutoId: 'auto-id数据无法编辑。',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

+ 2 - 1
client/src/i18n/cn/dialog.ts

@@ -25,7 +25,8 @@ const dialogTrans = {
     '复制Collection不会复制Collection中的数据。它只会使用现有的Schema创建一个新的Collection。',
     '复制Collection不会复制Collection中的数据。它只会使用现有的Schema创建一个新的Collection。',
   flushDialogInfo: `落盘是一个在数据被插入到Milvus后,封闭和索引任何剩余段的过程。这避免了在未封闭的段上进行暴力搜索。  <br /><br />最好在插入会话结束时使用落盘,以防止数据碎片化。 <br /><br /><strong>注意:对于大型数据集,此操作可能需要一些时间。</strong>`,
   flushDialogInfo: `落盘是一个在数据被插入到Milvus后,封闭和索引任何剩余段的过程。这避免了在未封闭的段上进行暴力搜索。  <br /><br />最好在插入会话结束时使用落盘,以防止数据碎片化。 <br /><br /><strong>注意:对于大型数据集,此操作可能需要一些时间。</strong>`,
   emptyDataDialogInfo: `您正在尝试清空数据。此操作无法撤销,请谨慎操作。`,
   emptyDataDialogInfo: `您正在尝试清空数据。此操作无法撤销,请谨慎操作。`,
-  resetPropertyInfo: '您确定要重置属性吗?'
+  resetPropertyInfo: '您确定要重置属性吗?',
+  editEntityInfo: `注意:编辑id字段将创建一个新的实体。`,
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 1 - 0
client/src/i18n/en/button.ts

@@ -55,6 +55,7 @@ const btnTrans = {
   downloadDisabledTooltip: 'Please select data before exporting',
   downloadDisabledTooltip: 'Please select data before exporting',
   deleteDisableTooltip: 'Please select at least one item to delete.',
   deleteDisableTooltip: 'Please select at least one item to delete.',
   editEntityDisabledTooltip: 'Only one entity can be edited at a time.',
   editEntityDisabledTooltip: 'Only one entity can be edited at a time.',
+  editEntityDisabledTooltipAutoId: 'The auto-generated ID entity cannot be edited.',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

+ 2 - 1
client/src/i18n/en/dialog.ts

@@ -25,7 +25,8 @@ const dialogTrans = {
     'Duplicating a collection does not copy the data within the collection. It only creates a new collection using the existing schema.',
     'Duplicating a collection does not copy the data within the collection. It only creates a new collection using the existing schema.',
   flushDialogInfo: `Flush is a process that seals and indexes any remaining segments after data is upserted into Milvus. This avoids brute force searches on unsealed segments.  <br /><br />It's best to use flush at the end of an upsert session to prevent data fragmentation. <br /><br /><strong>Note: that this operation may take some time for large datasets.</strong>`,
   flushDialogInfo: `Flush is a process that seals and indexes any remaining segments after data is upserted into Milvus. This avoids brute force searches on unsealed segments.  <br /><br />It's best to use flush at the end of an upsert session to prevent data fragmentation. <br /><br /><strong>Note: that this operation may take some time for large datasets.</strong>`,
   emptyDataDialogInfo: `You are attempting to empty the data. This action cannot be undone, please proceed with caution.`,
   emptyDataDialogInfo: `You are attempting to empty the data. This action cannot be undone, please proceed with caution.`,
-  resetPropertyInfo: `Are you sure you want to reset the property?`
+  resetPropertyInfo: `Are you sure you want to reset the property?`,
+  editEntityInfo: `NOTE: Edit id field will create a new entity.`
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 1 - 1
client/src/pages/databases/collections/Types.ts

@@ -70,7 +70,7 @@ export interface CreateFieldsProps {
 }
 }
 
 
 export interface InsertDataParam {
 export interface InsertDataParam {
-  partition_name: string;
+  partition_name?: string;
   // e.g. [{vector: [1,2,3], age: 10}]
   // e.g. [{vector: [1,2,3], age: 10}]
   fields_data: any[];
   fields_data: any[];
 }
 }

+ 7 - 3
client/src/pages/databases/collections/data/CollectionData.tsx

@@ -250,7 +250,7 @@ const CollectionData = (props: CollectionDataProps) => {
       },
       },
       label: btnTrans('empty'),
       label: btnTrans('empty'),
       tooltip: btnTrans('emptyTooltip'),
       tooltip: btnTrans('emptyTooltip'),
-      disabled: () => selectedData?.length > 0 ||  total == 0,
+      disabled: () => selectedData?.length > 0 || total == 0,
     },
     },
     {
     {
       type: 'button',
       type: 'button',
@@ -272,8 +272,12 @@ const CollectionData = (props: CollectionDataProps) => {
       label: btnTrans('edit'),
       label: btnTrans('edit'),
       icon: 'edit',
       icon: 'edit',
       tooltip: btnTrans('editEntityTooltip'),
       tooltip: btnTrans('editEntityTooltip'),
-      disabledTooltip: btnTrans('editEntityDisabledTooltip'),
-      disabled: () => selectedData?.length !== 1,
+      disabledTooltip: btnTrans(
+        collection.autoID
+          ? 'editEntityDisabledTooltipAutoId'
+          : 'editEntityDisabledTooltip'
+      ),
+      disabled: () => selectedData?.length !== 1 && collection.autoID,
       hideOnDisable() {
       hideOnDisable() {
         return selectedData?.length === 0;
         return selectedData?.length === 0;
       },
       },

+ 46 - 16
client/src/pages/dialogs/EditEntityDialog.tsx

@@ -1,7 +1,7 @@
-import { FC, useContext, useEffect, useRef } from 'react';
+import { FC, useContext, useEffect, useRef, useState } from 'react';
 import { makeStyles, Theme } from '@material-ui/core';
 import { makeStyles, Theme } from '@material-ui/core';
 import { EditorState } from '@codemirror/state';
 import { EditorState } from '@codemirror/state';
-import { EditorView, keymap } from '@codemirror/view';
+import { EditorView, keymap, ViewUpdate } from '@codemirror/view';
 import { insertTab } from '@codemirror/commands';
 import { insertTab } from '@codemirror/commands';
 import { indentUnit } from '@codemirror/language';
 import { indentUnit } from '@codemirror/language';
 import { basicSetup } from 'codemirror';
 import { basicSetup } from 'codemirror';
@@ -11,21 +11,26 @@ import { useTranslation } from 'react-i18next';
 import { rootContext } from '@/context';
 import { rootContext } from '@/context';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import { CollectionFullObject } from '@server/types';
 import { CollectionFullObject } from '@server/types';
+import { DataService } from '@/http';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   code: {
   code: {
     backgroundColor: '#f5f5f5',
     backgroundColor: '#f5f5f5',
     padding: 4,
     padding: 4,
-    width: '60vw',
-    minHeight: '60vh',
-    maxHeight: '60vh',
+    width: '100%',
+    height: '60vh',
     overflow: 'auto',
     overflow: 'auto',
   },
   },
+  tip: {
+    fontSize: 12,
+    marginBottom: 8,
+  },
 }));
 }));
 
 
 type EditEntityDialogProps = {
 type EditEntityDialogProps = {
   data: { [key: string]: any };
   data: { [key: string]: any };
   collection: CollectionFullObject;
   collection: CollectionFullObject;
+  cb?: () => void;
 };
 };
 
 
 // json linter for cm
 // json linter for cm
@@ -34,6 +39,8 @@ const linterExtension = linter(jsonParseLinter());
 const EditEntityDialog: FC<EditEntityDialogProps> = props => {
 const EditEntityDialog: FC<EditEntityDialogProps> = props => {
   // props
   // props
   const { data, collection } = props;
   const { data, collection } = props;
+  // UI states
+  const [disabled, setDisabled] = useState(true);
   // context
   // context
   const { handleCloseDialog } = useContext(rootContext);
   const { handleCloseDialog } = useContext(rootContext);
   // translations
   // translations
@@ -54,11 +61,13 @@ const EditEntityDialog: FC<EditEntityDialogProps> = props => {
     }
     }
   });
   });
 
 
+  const originalData = JSON.stringify(sortedData, null, 4);
+
   // create editor
   // create editor
   useEffect(() => {
   useEffect(() => {
     if (!editor.current) {
     if (!editor.current) {
       const startState = EditorState.create({
       const startState = EditorState.create({
-        doc: JSON.stringify(sortedData, null, 4),
+        doc: originalData,
         extensions: [
         extensions: [
           basicSetup,
           basicSetup,
           json(),
           json(),
@@ -79,8 +88,21 @@ const EditEntityDialog: FC<EditEntityDialogProps> = props => {
               width: '80%',
               width: '80%',
             },
             },
           }),
           }),
-
           EditorView.lineWrapping,
           EditorView.lineWrapping,
+          EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
+            if (viewUpdate.docChanged) {
+              const d = jsonParseLinter()(view);
+              if (d.length !== 0) {
+                setDisabled(true);
+                return;
+              }
+
+              const doc = viewUpdate.state.doc;
+              const value = doc.toString();
+
+              setDisabled(value === originalData);
+            }
+          }),
         ],
         ],
       });
       });
 
 
@@ -99,8 +121,12 @@ const EditEntityDialog: FC<EditEntityDialogProps> = props => {
   }, [JSON.stringify(data)]);
   }, [JSON.stringify(data)]);
 
 
   // handle confirm
   // handle confirm
-  const handleConfirm = () => {
-    console.log(JSON.parse(editor.current?.state.doc.toString()!));
+  const handleConfirm = async () => {
+    await DataService.upsert(collection.collection_name, {
+      fields_data: [JSON.parse(editor.current?.state.doc.toString()!)],
+    });
+
+    props.cb && props.cb();
     handleCloseDialog();
     handleCloseDialog();
   };
   };
 
 
@@ -109,14 +135,18 @@ const EditEntityDialog: FC<EditEntityDialogProps> = props => {
       title={dialogTrans('editEntityTitle')}
       title={dialogTrans('editEntityTitle')}
       handleClose={handleCloseDialog}
       handleClose={handleCloseDialog}
       children={
       children={
-        <div
-          className={`${classes.code} cm-editor`}
-          ref={editorEl}
-          onClick={() => {
-            if (editor.current) editor.current.focus();
-          }}
-        ></div>
+        <>
+          <div className={classes.tip}>{dialogTrans('editEntityInfo')}</div>
+          <div
+            className={`${classes.code} cm-editor`}
+            ref={editorEl}
+            onClick={() => {
+              if (editor.current) editor.current.focus();
+            }}
+          ></div>
+        </>
       }
       }
+      confirmDisabled={disabled}
       confirmLabel={btnTrans('edit')}
       confirmLabel={btnTrans('edit')}
       handleConfirm={handleConfirm}
       handleConfirm={handleConfirm}
       showCancel={true}
       showCancel={true}

+ 19 - 0
server/src/collections/collections.controller.ts

@@ -91,6 +91,11 @@ export class CollectionController {
       dtoValidationMiddleware(InsertDataDto),
       dtoValidationMiddleware(InsertDataDto),
       this.insert.bind(this)
       this.insert.bind(this)
     );
     );
+    this.router.post(
+      '/:name/upsert',
+      dtoValidationMiddleware(InsertDataDto),
+      this.upsert.bind(this)
+    );
 
 
     // insert sample data
     // insert sample data
     this.router.post(
     this.router.post(
@@ -309,6 +314,20 @@ export class CollectionController {
     }
     }
   }
   }
 
 
+  async upsert(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.upsert(req.clientId, {
+        collection_name: name,
+        ...data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async importSample(req: Request, res: Response, next: NextFunction) {
   async importSample(req: Request, res: Response, next: NextFunction) {
     const data = req.body;
     const data = req.body;
     try {
     try {

+ 8 - 0
server/src/collections/collections.service.ts

@@ -244,6 +244,14 @@ export class CollectionsService {
     return res;
     return res;
   }
   }
 
 
+  async upsert(clientId: string, data: InsertReq) {
+    const { milvusClient } = clientCache.get(clientId);
+    const res = await milvusClient.upsert(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
+
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.deleteEntities(data);
     const res = await milvusClient.deleteEntities(data);