Browse Source

Attu 517 support edit properties (#539)

* init

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* part1

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* part2

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* finish

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* upgrade

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* Revert "upgrade"

This reverts commit bc8cee17fcc1005ac47645846bbd943b8889cece.

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
ryjiang 1 year ago
parent
commit
945cf05c6f

+ 5 - 1
client/src/components/customDialog/DeleteDialogTemplate.tsx

@@ -91,7 +91,11 @@ const DeleteTemplate: FC<DeleteDialogContentType> = props => {
         </CustomDialogTitle>
         </CustomDialogTitle>
 
 
         <DialogContent>
         <DialogContent>
-          <Typography variant="body1" className={classes.info}>{text}</Typography>
+          <Typography
+            variant="body1"
+            className={classes.info}
+            dangerouslySetInnerHTML={{ __html: text }}
+          ></Typography>
           <Typography variant="body1" className={classes.mb}>
           <Typography variant="body1" className={classes.mb}>
             {dialogTrans('deleteTipAction')}
             {dialogTrans('deleteTipAction')}
             <strong className={classes.btnLabel}>{` ${(
             <strong className={classes.btnLabel}>{` ${(

+ 16 - 0
client/src/components/icons/Icons.tsx

@@ -827,6 +827,22 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       ></path>
       ></path>
     </svg>
     </svg>
   ),
   ),
+  reset: (props = {}) => (
+    <svg
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M4.85355 2.14645C5.04882 2.34171 5.04882 2.65829 4.85355 2.85355L3.70711 4H9C11.4853 4 13.5 6.01472 13.5 8.5C13.5 10.9853 11.4853 13 9 13H5C4.72386 13 4.5 12.7761 4.5 12.5C4.5 12.2239 4.72386 12 5 12H9C10.933 12 12.5 10.433 12.5 8.5C12.5 6.567 10.933 5 9 5H3.70711L4.85355 6.14645C5.04882 6.34171 5.04882 6.65829 4.85355 6.85355C4.65829 7.04882 4.34171 7.04882 4.14645 6.85355L2.14645 4.85355C1.95118 4.65829 1.95118 4.34171 2.14645 4.14645L4.14645 2.14645C4.34171 1.95118 4.65829 1.95118 4.85355 2.14645Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </svg>
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

+ 2 - 1
client/src/components/icons/Types.ts

@@ -52,4 +52,5 @@ export type IconsType =
   | 'discord'
   | 'discord'
   | 'star'
   | 'star'
   | 'magic'
   | 'magic'
-  | 'code';
+  | 'code'
+  | 'reset';

+ 22 - 1
client/src/context/Data.tsx

@@ -64,6 +64,9 @@ export const dataContext = createContext<DataContextType>({
   dropAlias: async () => {
   dropAlias: async () => {
     return {} as CollectionFullObject;
     return {} as CollectionFullObject;
   },
   },
+  setProperty: async () => {
+    return {} as CollectionFullObject;
+  },
 });
 });
 
 
 const { Provider } = dataContext;
 const { Provider } = dataContext;
@@ -118,7 +121,7 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
       detectLoadingIndexing(updateCollections);
       detectLoadingIndexing(updateCollections);
       // update single collection
       // update single collection
       setCollections(prev => {
       setCollections(prev => {
-        // update exsit collection
+        // update exist collection
         const newCollections = prev.map(v => {
         const newCollections = prev.map(v => {
           const collectionToUpdate = updateCollections.find(c => c.id === v.id);
           const collectionToUpdate = updateCollections.find(c => c.id === v.id);
 
 
@@ -322,6 +325,23 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
     return data;
     return data;
   };
   };
 
 
+  // API: set property
+  const setProperty = async (
+    collectionName: string,
+    key: string,
+    value: any
+  ) => {
+    // set property
+    const newCollection = await CollectionService.setProperty(collectionName, {
+      [key]: value,
+    });
+
+    // update existing collection
+    updateCollections([newCollection]);
+
+    return newCollection;
+  };
+
   useEffect(() => {
   useEffect(() => {
     if (isAuth) {
     if (isAuth) {
       // update database get from auth
       // update database get from auth
@@ -409,6 +429,7 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
         dropIndex,
         dropIndex,
         createAlias,
         createAlias,
         dropAlias,
         dropAlias,
+        setProperty,
       }}
       }}
     >
     >
       {props.children}
       {props.children}

+ 5 - 0
client/src/context/Types.ts

@@ -137,4 +137,9 @@ export type DataContextType = {
     collectionName: string,
     collectionName: string,
     alias: string
     alias: string
   ) => Promise<CollectionFullObject>;
   ) => Promise<CollectionFullObject>;
+  setProperty: (
+    collectionName: string,
+    key: string,
+    value: any
+  ) => Promise<CollectionFullObject>;
 };
 };

+ 9 - 0
client/src/hooks/Form.ts

@@ -64,8 +64,17 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
       errText: '',
       errText: '',
     };
     };
 
 
+    const hasRequire = rules.some(r => r.rule === 'require');
+
+    // if value is empty, and no require rule, skip this check
+    if (!checkEmptyValid(value) && !hasRequire) {
+      setDisabled(false);
+      return validDetail;
+    }
+
     for (let i = 0; i < rules.length; i++) {
     for (let i = 0; i < rules.length; i++) {
       const rule = rules[i];
       const rule = rules[i];
+
       const checkResult = getCheckResult({
       const checkResult = getCheckResult({
         value,
         value,
         extraParam: rule.extraParam,
         extraParam: rule.extraParam,

+ 11 - 1
client/src/http/Collection.service.ts

@@ -12,7 +12,10 @@ import {
   IndexObject,
   IndexObject,
 } from '@server/types';
 } from '@server/types';
 import { ManageRequestMethods } from '../types/Common';
 import { ManageRequestMethods } from '../types/Common';
-import { IndexCreateParam, IndexManageParam } from '@/pages/databases/collections/overview/Types';
+import {
+  IndexCreateParam,
+  IndexManageParam,
+} from '@/pages/databases/collections/overview/Types';
 
 
 export class CollectionService extends BaseModel {
 export class CollectionService extends BaseModel {
   static getCollections(data?: {
   static getCollections(data?: {
@@ -61,6 +64,13 @@ export class CollectionService extends BaseModel {
     });
     });
   }
   }
 
 
+  static setProperty(collectionName: string, params: { [key: string]: any }) {
+    return super.update<CollectionFullObject>({
+      path: `/collections/${collectionName}/properties`,
+      data: { properties: params },
+    });
+  }
+
   static duplicateCollection(
   static duplicateCollection(
     collectionName: string,
     collectionName: string,
     params: { new_collection_name: string }
     params: { new_collection_name: string }

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

@@ -35,6 +35,7 @@ const btnTrans = {
   star: '给我一颗小星星',
   star: '给我一颗小星星',
   applyFilter: '应用过滤器',
   applyFilter: '应用过滤器',
   createIndex: '创建索引',
   createIndex: '创建索引',
+  edit: '编辑',
 
 
   // tips
   // tips
   loadColTooltip: '加载Collection',
   loadColTooltip: '加载Collection',

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

@@ -88,6 +88,7 @@ const collectionTrans = {
   dataTab: '数据',
   dataTab: '数据',
   previewTab: '数据预览',
   previewTab: '数据预览',
   segmentsTab: '数据段(Segments)',
   segmentsTab: '数据段(Segments)',
+  propertiesTab: '属性',
   startTip: '开始你的数据查询',
   startTip: '开始你的数据查询',
   exprPlaceHolder: '请输入你的数据查询,例如 id > 0',
   exprPlaceHolder: '请输入你的数据查询,例如 id > 0',
 
 

+ 2 - 0
client/src/i18n/cn/common.ts

@@ -39,6 +39,8 @@ const commonTrans = {
     segments: '数据段',
     segments: '数据段',
     partition: '分区',
     partition: '分区',
     partitions: '分区',
     partitions: '分区',
+    property: '属性',
+    properties: '属性',
     results: '结果',
     results: '结果',
     of: '的',
     of: '的',
     nextLabel: '下一页',
     nextLabel: '下一页',

+ 4 - 0
client/src/i18n/cn/dialog.ts

@@ -1,4 +1,5 @@
 const dialogTrans = {
 const dialogTrans = {
+  value: '值',
   deleteTipAction: '输入',
   deleteTipAction: '输入',
   deleteTipPurpose: '以确认。',
   deleteTipPurpose: '以确认。',
   deleteTitle: `删除 {{type}}`,
   deleteTitle: `删除 {{type}}`,
@@ -15,12 +16,15 @@ const dialogTrans = {
 
 
   createTitle: `在 "{{name}}" 上创建 {{type}}`,
   createTitle: `在 "{{name}}" 上创建 {{type}}`,
   emptyTitle: `清空{{type}}的数据`,
   emptyTitle: `清空{{type}}的数据`,
+  editPropertyTitle: `编辑属性 {{type}}`,
+  resetPropertyTitle: `重置属性 {{type}}`,
 
 
   // info
   // info
   duplicateCollectionInfo:
   duplicateCollectionInfo:
     '复制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: '您确定要重置属性吗?'
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 7 - 0
client/src/i18n/cn/properties.ts

@@ -0,0 +1,7 @@
+const propertiesTrans = {
+  property: '属性',
+  value: '值',
+  description: '描述',
+};
+
+export default propertiesTrans;

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

@@ -8,6 +8,7 @@ const successTrans = {
   rename: `{{name}}重命名成功。`,
   rename: `{{name}}重命名成功。`,
   duplicate: `{{name}}复制成功.`,
   duplicate: `{{name}}复制成功.`,
   empty: `{{name}}清空已经开始.`,
   empty: `{{name}}清空已经开始.`,
+  reset: `{{name}}重置成功。`,
 };
 };
 
 
 export default successTrans;
 export default successTrans;

+ 2 - 0
client/src/i18n/cn/warning.ts

@@ -3,6 +3,8 @@ const warningTrans = {
   requiredOnly: '必需的',
   requiredOnly: '必需的',
   positive: '{{name}} 应为正数',
   positive: '{{name}} 应为正数',
   integer: '{{name}} 应为整数',
   integer: '{{name}} 应为整数',
+  number: '{{name}} 应为数字',
+  bool: '{{name}} 应为布尔值`true`或`false`',
   range: '范围是 {{min}} ~ {{max}}',
   range: '范围是 {{min}} ~ {{max}}',
   specValueOrRange:
   specValueOrRange:
     '{{name}} 应为 {{specValue}},或在范围 {{min}} ~ {{max}} 内',
     '{{name}} 应为 {{specValue}},或在范围 {{min}} ~ {{max}} 内',

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

@@ -35,6 +35,7 @@ const btnTrans = {
   star: 'Give me a Star',
   star: 'Give me a Star',
   applyFilter: 'Apply Filters',
   applyFilter: 'Apply Filters',
   createIndex: 'Create Index',
   createIndex: 'Create Index',
+  edit: 'Edit',
 
 
   // tips
   // tips
   loadColTooltip: 'Load Collection',
   loadColTooltip: 'Load Collection',

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

@@ -90,6 +90,7 @@ const collectionTrans = {
   dataTab: 'Data',
   dataTab: 'Data',
   previewTab: 'Data Preview',
   previewTab: 'Data Preview',
   segmentsTab: 'Segments',
   segmentsTab: 'Segments',
+  propertiesTab: 'Properties',
   startTip: 'Start Your Data Query',
   startTip: 'Start Your Data Query',
   exprPlaceHolder: 'Please enter your data query, for example id > 0',
   exprPlaceHolder: 'Please enter your data query, for example id > 0',
 
 

+ 2 - 0
client/src/i18n/en/common.ts

@@ -40,6 +40,8 @@ const commonTrans = {
     segments: 'Segments',
     segments: 'Segments',
     partition: 'Partition',
     partition: 'Partition',
     partitions: 'Partitions',
     partitions: 'Partitions',
+    property: 'Property',
+    properties: 'Properties',
     results: 'results',
     results: 'results',
     of: 'of',
     of: 'of',
     nextLabel: 'next page',
     nextLabel: 'next page',

+ 4 - 0
client/src/i18n/en/dialog.ts

@@ -1,4 +1,5 @@
 const dialogTrans = {
 const dialogTrans = {
+  value: 'value',
   deleteTipAction: 'Type',
   deleteTipAction: 'Type',
   deleteTipPurpose: 'to confirm.',
   deleteTipPurpose: 'to confirm.',
   deleteTitle: `Drop {{type}}`,
   deleteTitle: `Drop {{type}}`,
@@ -15,12 +16,15 @@ const dialogTrans = {
 
 
   createTitle: `Create {{type}} on "{{name}}"`,
   createTitle: `Create {{type}} on "{{name}}"`,
   emptyTitle: `Empty data for {{type}}`,
   emptyTitle: `Empty data for {{type}}`,
+  editPropertyTitle: `Edit property {{type}}`,
+  resetPropertyTitle: `Reset property {{type}}`,
 
 
   // info
   // info
   duplicateCollectionInfo:
   duplicateCollectionInfo:
     '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?`
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 7 - 0
client/src/i18n/en/properties.ts

@@ -0,0 +1,7 @@
+const propertiesTrans = {
+  property: 'Property',
+  value: 'Value',
+  description: 'Description',
+};
+
+export default propertiesTrans;

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

@@ -8,6 +8,7 @@ const successTrans = {
   rename: `{{name}} has been renamed.`,
   rename: `{{name}} has been renamed.`,
   duplicate: `{{name}} has been duplicated.`,
   duplicate: `{{name}} has been duplicated.`,
   empty: `Emptying data for {{name}} has started.`,
   empty: `Emptying data for {{name}} has started.`,
+  reset: `{{name}} has been reset.`,
 };
 };
 
 
 export default successTrans;
 export default successTrans;

+ 2 - 0
client/src/i18n/en/warning.ts

@@ -3,6 +3,8 @@ const warningTrans = {
   requiredOnly: 'Required',
   requiredOnly: 'Required',
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
+  number: '{{name}} should be numbers',
+  bool: '{{name}} should be boolean value `true` or `false`',
   range: 'Range is {{min}} ~ {{max}}',
   range: 'Range is {{min}} ~ {{max}}',
   specValueOrRange:
   specValueOrRange:
     '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',
     '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',

+ 4 - 0
client/src/i18n/index.ts

@@ -34,6 +34,8 @@ import databaseTransEn from './en/database';
 import databaseTransCn from './cn/database';
 import databaseTransCn from './cn/database';
 import prometheusTransEn from './en/prometheus';
 import prometheusTransEn from './en/prometheus';
 import prometheusTransCn from './cn/prometheus';
 import prometheusTransCn from './cn/prometheus';
+import propertiesEn from './en/properties';
+import propertiesCn from './cn/properties';
 
 
 export const resources = {
 export const resources = {
   'zh-CN': {
   'zh-CN': {
@@ -53,6 +55,7 @@ export const resources = {
     user: userTransCn,
     user: userTransCn,
     database: databaseTransCn,
     database: databaseTransCn,
     prometheus: prometheusTransCn,
     prometheus: prometheusTransCn,
+    properties: propertiesCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -71,6 +74,7 @@ export const resources = {
     user: userTransEn,
     user: userTransEn,
     database: databaseTransEn,
     database: databaseTransEn,
     prometheus: prometheusTransEn,
     prometheus: prometheusTransEn,
+    properties: propertiesEn,
   },
   },
 };
 };
 
 

+ 18 - 6
client/src/pages/databases/Databases.tsx

@@ -11,6 +11,7 @@ import Partitions from './collections/partitions/Partitions';
 import Overview from './collections/overview/Overview';
 import Overview from './collections/overview/Overview';
 import Data from './collections/data/CollectionData';
 import Data from './collections/data/CollectionData';
 import Segments from './collections/segments/Segments';
 import Segments from './collections/segments/Segments';
+import Properties from './collections/properties/Properties';
 import Search from './collections/search/Search';
 import Search from './collections/search/Search';
 import { dataContext, authContext } from '@/context';
 import { dataContext, authContext } from '@/context';
 import Collections from './collections/Collections';
 import Collections from './collections/Collections';
@@ -19,7 +20,7 @@ import { ConsistencyLevelEnum } from '@/consts';
 import RefreshButton from './RefreshButton';
 import RefreshButton from './RefreshButton';
 import CopyButton from '@/components/advancedSearch/CopyButton';
 import CopyButton from '@/components/advancedSearch/CopyButton';
 import { SearchParams } from './types';
 import { SearchParams } from './types';
-import { CollectionObject } from '@server/types';
+import { CollectionObject, CollectionFullObject } from '@server/types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -267,6 +268,10 @@ const CollectionTabs = (props: {
   // i18n
   // i18n
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
 
 
+  const collection = collections.find(
+    i => i.collection_name === collectionName
+  ) as CollectionFullObject;
+
   // collection tabs
   // collection tabs
   const collectionTabs: ITab[] = [
   const collectionTabs: ITab[] = [
     {
     {
@@ -301,11 +306,18 @@ const CollectionTabs = (props: {
   ];
   ];
 
 
   if (!isManaged) {
   if (!isManaged) {
-    collectionTabs.push({
-      label: collectionTrans('segmentsTab'),
-      component: <Segments />,
-      path: `segments`,
-    });
+    collectionTabs.push(
+      {
+        label: collectionTrans('segmentsTab'),
+        component: <Segments />,
+        path: `segments`,
+      },
+      {
+        label: collectionTrans('propertiesTab'),
+        component: <Properties collection={collection} />,
+        path: `properties`,
+      }
+    );
   }
   }
 
 
   // get active collection tab
   // get active collection tab

+ 243 - 0
client/src/pages/databases/collections/properties/Properties.tsx

@@ -0,0 +1,243 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { useContext, useState } from 'react';
+import AttuGrid from '@/components/grid/Grid';
+import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import { useTranslation } from 'react-i18next';
+import { usePaginationHook } from '@/hooks';
+import StatusIcon, { LoadingType } from '@/components/status/StatusIcon';
+import EditPropertyDialog from '@/pages/dialogs/EditPropertyDialog';
+import ResetPropertyDialog from '@/pages/dialogs/ResetPropertyDialog';
+import { rootContext } from '@/context';
+import { getLabelDisplayedRows } from '@/pages/search/Utils';
+import { CollectionFullObject } from '@server/types';
+import { formatNumber } from '@/utils';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: `100%`,
+  },
+  icon: {
+    fontSize: '14px',
+    marginLeft: theme.spacing(0.5),
+  },
+  highlight: {
+    color: theme.palette.primary.main,
+    backgroundColor: 'transparent',
+  },
+}));
+
+export type Property = { key: string; value: any; desc: string; type: string };
+
+let defaults: Property[] = [
+  { key: 'collection.ttl.seconds', value: '', desc: '', type: 'number' },
+  {
+    key: 'collection.autocompaction.enabled',
+    value: '',
+    desc: '',
+    type: 'boolean',
+  },
+  { key: 'collection.insertRate.max.mb', value: '', desc: '', type: 'number' },
+  { key: 'collection.insertRate.min.mb', value: '', desc: '', type: 'number' },
+  { key: 'collection.upsertRate.max.mb', value: '', desc: '', type: 'number' },
+  { key: 'collection.upsertRate.min.mb', value: '', desc: '', type: 'number' },
+  { key: 'collection.deleteRate.max.mb', value: '', desc: '', type: 'number' },
+  { key: 'collection.deleteRate.min.mb', value: '', desc: '', type: 'number' },
+  {
+    key: 'collection.bulkLoadRate.max.mb',
+    value: '',
+    desc: '',
+    type: 'number',
+  },
+  {
+    key: 'collection.bulkLoadRate.min.mb',
+    value: '',
+    desc: '',
+    type: 'number',
+  },
+  { key: 'collection.queryRate.max.qps', value: '', desc: '', type: 'number' },
+  { key: 'collection.queryRate.min.qps', value: '', desc: '', type: 'number' },
+  { key: 'collection.searchRate.max.vps', value: '', desc: '', type: 'number' },
+  { key: 'collection.searchRate.min.vps', value: '', desc: '', type: 'number' },
+  {
+    key: 'collection.diskProtection.diskQuota.mb',
+    value: '',
+    desc: '',
+    type: 'number',
+  },
+  {
+    key: 'partition.diskProtection.diskQuota.mb',
+    value: '',
+    desc: '',
+    type: 'number',
+  },
+  { key: 'mmap.enabled', value: '', desc: '', type: 'boolean' },
+  { key: 'lazyload.enabled', value: '', desc: '', type: 'boolean' },
+];
+
+interface PropertiesProps {
+  collection: CollectionFullObject;
+}
+
+const Properties = (props: PropertiesProps) => {
+  const { collection } = props;
+
+  const classes = useStyles();
+  const { t } = useTranslation('properties');
+  const { t: successTrans } = useTranslation('success');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
+
+  const [selected, setSelected] = useState<Property[]>([]);
+  const { setDialog, openSnackBar } = useContext(rootContext);
+
+  // combine default properties with collection properties
+  let properties: Property[] = collection
+    ? defaults.map(i => {
+        let prop = collection.properties.find(p => p.key === i.key);
+        if (prop) {
+          return { ...i, ...prop };
+        } else {
+          return i;
+        }
+      })
+    : defaults;
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data,
+    order,
+    orderBy,
+    handleGridSort,
+  } = usePaginationHook(properties);
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      icon: 'edit',
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <EditPropertyDialog
+                collection={collection}
+                property={selected[0]}
+                cb={() => {
+                  openSnackBar(
+                    successTrans('update', { name: selected[0].key })
+                  );
+                }}
+              />
+            ),
+          },
+        });
+      },
+      label: btnTrans('edit'),
+      disabled: () => selected.length === 0,
+    },
+    {
+      icon: 'reset',
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <ResetPropertyDialog
+                collection={collection}
+                property={selected[0]}
+                cb={() => {
+                  openSnackBar(
+                    successTrans('reset', { name: selected[0].key })
+                  );
+                }}
+              />
+            ),
+          },
+        });
+      },
+      label: btnTrans('reset'),
+      disabled: () => selected.length === 0,
+    },
+  ];
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: 'key',
+      notSort: true,
+      align: 'left',
+      disablePadding: false,
+      label: t('property'),
+      needCopy: true,
+    },
+    {
+      id: 'value',
+      align: 'left',
+      disablePadding: false,
+      label: t('value'),
+      formatter: (obj: Property) => {
+        if (obj.value === '') {
+          return '-';
+        } else {
+          return obj.type === 'number' ? formatNumber(obj.value) : obj.value;
+        }
+      },
+    },
+  ];
+
+  const handleSelectChange = (value: Property[]) => {
+    // only select one row, filter out the rest
+    if (value.length > 1) {
+      value = [value[value.length - 1]];
+    }
+    setSelected(value);
+  };
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+    setSelected([]);
+  };
+
+  // collection is not found or collection full object is not ready
+  if (!collection || !collection.schema) {
+    return <StatusIcon type={LoadingType.CREATING} />;
+  }
+
+  return (
+    <section className={classes.wrapper}>
+      <AttuGrid
+        toolbarConfigs={toolbarConfigs}
+        colDefinitions={colDefinitions}
+        rows={data}
+        rowCount={total}
+        primaryKey="key"
+        selected={selected}
+        setSelected={handleSelectChange}
+        page={currentPage}
+        onPageChange={handlePageChange}
+        rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
+        isLoading={false}
+        order={order}
+        orderBy={orderBy}
+        handleSort={handleGridSort}
+        labelDisplayedRows={getLabelDisplayedRows(
+          gridTrans[data.length > 1 ? 'properties' : 'property']
+        )}
+      />
+    </section>
+  );
+};
+
+export default Properties;

+ 16 - 0
client/src/pages/databases/collections/properties/Types.ts

@@ -0,0 +1,16 @@
+import { ReactElement } from 'react';
+import { LOADING_STATE } from '@/consts';
+import { ManageRequestMethods } from '../../../../types/Common';
+
+// delete and create
+export interface PartitionManageParam {
+  collectionName: string;
+  partitionName: string;
+  type: ManageRequestMethods;
+}
+
+// load and release
+export interface PartitionParam {
+  collectionName: string;
+  partitionNames: string[];
+}

+ 117 - 0
client/src/pages/dialogs/EditPropertyDialog.tsx

@@ -0,0 +1,117 @@
+import { FC, useContext, useMemo, useState } from 'react';
+import { Typography, makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { rootContext, dataContext } from '@/context';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+import CustomInput from '@/components/customInput/CustomInput';
+import { formatForm } from '@/utils';
+import { IForm, useFormValidation } from '@/hooks';
+import { ITextfieldConfig } from '@/components/customInput/Types';
+import { CollectionObject } from '@server/types';
+import { Property } from '../databases/collections/properties/Properties';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  desc: {
+    margin: '8px 0 16px 0',
+  },
+}));
+
+export interface EditPropertyProps {
+  collection: CollectionObject;
+  property: Property;
+  cb?: (collection: CollectionObject) => void;
+}
+
+const EditPropertyDialog: FC<EditPropertyProps> = props => {
+  const { setProperty } = useContext(dataContext);
+  const { handleCloseDialog } = useContext(rootContext);
+
+  const { cb, collection, property } = props;
+  const [form, setForm] = useState<IForm>({
+    key: 'property',
+    value: property.value,
+  });
+
+  const classes = useStyles();
+
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: warningTrans } = useTranslation('warning');
+  const { t: btnTrans } = useTranslation('btn');
+
+  const handleInputChange = (value: string) => {
+    setForm({ ...form, key: 'property', value });
+  };
+
+  const handleConfirm = async () => {
+    let value: unknown = '';
+
+    if (form.value !== '') {
+      switch (property.type) {
+        case 'number':
+          value = Number(form.value);
+          break;
+        case 'boolean':
+          value = form.value === 'true';
+          break;
+        default:
+          value = form.value;
+      }
+    }
+
+    await setProperty(collection.collection_name, property.key, value);
+    handleCloseDialog();
+    cb && (await cb(collection));
+  };
+
+  const propertyInputConfig: ITextfieldConfig = {
+    label: dialogTrans('value'),
+    key: 'value',
+    onChange: handleInputChange,
+    variant: 'filled',
+    placeholder: '',
+    fullWidth: true,
+    validations: [
+      {
+        rule: property.type === 'number' ? 'number' : 'bool',
+        errorText:
+          property.type === 'number'
+            ? warningTrans('integer', { name: dialogTrans('value') })
+            : warningTrans('bool', { name: dialogTrans('value') }),
+      },
+    ],
+    defaultValue: form.value,
+  };
+
+  return (
+    <DialogTemplate
+      title={dialogTrans('editPropertyTitle')}
+      handleClose={handleCloseDialog}
+      children={
+        <>
+          <Typography variant="body1" component="p" className={classes.desc}>
+            <code>
+              <b>{property.key}</b>
+            </code>
+          </Typography>
+          <CustomInput
+            type="text"
+            textConfig={propertyInputConfig}
+            checkValid={checkIsValid}
+            validInfo={validation}
+          />
+        </>
+      }
+      confirmLabel={btnTrans('confirm')}
+      handleConfirm={handleConfirm}
+      confirmDisabled={disabled}
+    />
+  );
+};
+
+export default EditPropertyDialog;

+ 43 - 0
client/src/pages/dialogs/ResetPropertyDialog.tsx

@@ -0,0 +1,43 @@
+import { FC, useContext } from 'react';
+import { useTranslation } from 'react-i18next';
+import { rootContext, dataContext } from '@/context';
+import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import { CollectionObject } from '@server/types';
+import { Property } from '../databases/collections/properties/Properties';
+
+export interface EditPropertyProps {
+  collection: CollectionObject;
+  property: Property;
+  cb?: (collection: CollectionObject) => void;
+}
+
+const ResetPropertyDialog: FC<EditPropertyProps> = props => {
+  // context
+  const { setProperty } = useContext(dataContext);
+  const { handleCloseDialog } = useContext(rootContext);
+  // props
+  const { cb, collection, property } = props;
+
+  // UI handlers
+  const handleDelete = async () => {
+    await setProperty(collection.collection_name, property.key, '');
+    handleCloseDialog();
+    cb && (await cb(collection));
+  };
+
+  // i18n
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: dialogTrans } = useTranslation('dialog');
+
+  return (
+    <DeleteTemplate
+      label={btnTrans('reset')}
+      title={dialogTrans('resetPropertyTitle')}
+      text={dialogTrans('resetPropertyInfo')}
+      compare={property.key}
+      handleDelete={handleDelete}
+    />
+  );
+};
+
+export default ResetPropertyDialog;

+ 12 - 0
client/src/utils/Validation.ts

@@ -2,6 +2,8 @@ import { MetricType, METRIC_TYPES_VALUES } from '@/consts';
 import { CollectionObject } from '@server/types';
 import { CollectionObject } from '@server/types';
 
 
 export type ValidType =
 export type ValidType =
+  | 'bool'
+  | 'number'
   | 'email'
   | 'email'
   | 'require'
   | 'require'
   | 'confirm'
   | 'confirm'
@@ -211,11 +213,21 @@ export const checkDuplicate = (param: {
   return param.value !== param.compare;
   return param.value !== param.compare;
 };
 };
 
 
+export const checkBool = (value: string): boolean => {
+  return value === 'true' || value === 'false';
+}
+
+export const checkNumber = (value: string): boolean => {
+  return !isNaN(Number(value));
+}
+
 export const getCheckResult = (param: ICheckMapParam): boolean => {
 export const getCheckResult = (param: ICheckMapParam): boolean => {
   const { value, extraParam = {}, rule } = param;
   const { value, extraParam = {}, rule } = param;
   const numberValue = Number(value);
   const numberValue = Number(value);
 
 
   const checkMap = {
   const checkMap = {
+    bool: checkBool(value),
+    number: checkNumber(value),
     email: checkEmail(value),
     email: checkEmail(value),
     require: checkEmptyValid(value),
     require: checkEmptyValid(value),
     confirm: value === extraParam?.compareValue,
     confirm: value === extraParam?.compareValue,

+ 28 - 1
server/src/collections/collections.controller.ts

@@ -7,7 +7,6 @@ import {
   CreateCollectionDto,
   CreateCollectionDto,
   InsertDataDto,
   InsertDataDto,
   ImportSampleDto,
   ImportSampleDto,
-  VectorSearchDto,
   QueryDto,
   QueryDto,
   RenameCollectionDto,
   RenameCollectionDto,
   DuplicateCollectionDto,
   DuplicateCollectionDto,
@@ -61,6 +60,7 @@ export class CollectionController {
       dtoValidationMiddleware(RenameCollectionDto),
       dtoValidationMiddleware(RenameCollectionDto),
       this.renameCollection.bind(this)
       this.renameCollection.bind(this)
     );
     );
+
     // duplicate collection
     // duplicate collection
     this.router.post(
     this.router.post(
       '/:name/duplicate',
       '/:name/duplicate',
@@ -79,6 +79,12 @@ export class CollectionController {
     this.router.put('/:name/release', this.releaseCollection.bind(this));
     this.router.put('/:name/release', this.releaseCollection.bind(this));
     this.router.put('/:name/empty', this.empty.bind(this));
     this.router.put('/:name/empty', this.empty.bind(this));
 
 
+    // alter collection
+    this.router.put(
+      '/:name/properties',
+      this.setCollectionProperties.bind(this)
+    );
+
     // insert data
     // insert data
     this.router.post(
     this.router.post(
       '/:name/insert',
       '/:name/insert',
@@ -171,6 +177,27 @@ export class CollectionController {
     }
     }
   }
   }
 
 
+  async setCollectionProperties(
+    req: Request,
+    res: Response,
+    next: NextFunction
+  ) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.alterCollection(
+        req.clientId,
+        {
+          collection_name: name,
+          ...data,
+        }
+      );
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async duplicateCollection(req: Request, res: Response, next: NextFunction) {
   async duplicateCollection(req: Request, res: Response, next: NextFunction) {
     const name = req.params?.name;
     const name = req.params?.name;
     const data = req.body;
     const data = req.body;

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

@@ -25,6 +25,7 @@ import {
   CreateIndexReq,
   CreateIndexReq,
   DescribeIndexReq,
   DescribeIndexReq,
   DropIndexReq,
   DropIndexReq,
+  AlterCollectionReq,
   DataType,
   DataType,
   HybridSearchReq,
   HybridSearchReq,
   SearchSimpleReq,
   SearchSimpleReq,
@@ -166,6 +167,18 @@ export class CollectionsService {
     return newCollection[0];
     return newCollection[0];
   }
   }
 
 
+  async alterCollection(clientId: string, data: AlterCollectionReq) {
+    const { milvusClient } = clientCache.get(clientId);
+    const res = await milvusClient.alterCollection(data);
+    throwErrorFromSDK(res);
+
+    const newCollection = (await this.getAllCollections(clientId, [
+      data.collection_name,
+    ])) as CollectionFullObject[];
+
+    return newCollection[0];
+  }
+
   async dropCollection(clientId: string, data: DropCollectionReq) {
   async dropCollection(clientId: string, data: DropCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropCollection(data);
     const res = await milvusClient.dropCollection(data);
@@ -417,6 +430,7 @@ export class CollectionsService {
       replicas: replicas && replicas.replicas,
       replicas: replicas && replicas.replicas,
       loaded: status === LOADING_STATE.LOADED,
       loaded: status === LOADING_STATE.LOADED,
       status,
       status,
+      properties: collectionInfo.properties,
     };
     };
   }
   }
 
 

+ 2 - 0
server/src/types/collections.type.ts

@@ -55,6 +55,7 @@ export type CollectionFullObject = {
   replicas: ReplicaInfo[];
   replicas: ReplicaInfo[];
   status: LOADING_STATE;
   status: LOADING_STATE;
   loaded: boolean;
   loaded: boolean;
+  properties: KeyValuePair[];
 };
 };
 
 
 export type CollectionLazyObject = {
 export type CollectionLazyObject = {
@@ -71,6 +72,7 @@ export type CollectionLazyObject = {
   consistency_level: undefined;
   consistency_level: undefined;
   replicas: undefined;
   replicas: undefined;
   loaded: undefined;
   loaded: undefined;
+  properties: undefined;
 };
 };
 
 
 export type CollectionObject = CollectionFullObject | CollectionLazyObject;
 export type CollectionObject = CollectionFullObject | CollectionLazyObject;