Переглянути джерело

Merge pull request #29 from Tumao727/feature/create-partition

Create partition
nameczz 4 роки тому
батько
коміт
36add9af9d

+ 14 - 2
client/src/components/customDialog/DialogTemplate.tsx

@@ -1,10 +1,21 @@
 import { FC } from 'react';
 import { useTranslation } from 'react-i18next';
-import { DialogContent, DialogActions } from '@material-ui/core';
+import {
+  DialogContent,
+  DialogActions,
+  makeStyles,
+  Theme,
+} from '@material-ui/core';
 import { DialogContainerProps } from './Types';
 import CustomDialogTitle from './CustomDialogTitle';
 import CustomButton from '../customButton/CustomButton';
 
+const useStyles = makeStyles((theme: Theme) => ({
+  actions: {
+    paddingTop: theme.spacing(2),
+  },
+}));
+
 const DialogTemplate: FC<DialogContainerProps> = ({
   title,
   cancelLabel,
@@ -17,12 +28,13 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
+  const classes = useStyles();
 
   return (
     <>
       <CustomDialogTitle onClose={handleCancel}>{title}</CustomDialogTitle>
       <DialogContent>{children}</DialogContent>
-      <DialogActions>
+      <DialogActions className={classes.actions}>
         <CustomButton onClick={handleCancel} color="default">
           {cancel}
         </CustomButton>

+ 0 - 7
client/src/components/layout/Layout.tsx

@@ -55,13 +55,6 @@ const Layout = (props: any) => {
       label: t('collection'),
       onClick: () => history.push('/collections'),
     },
-    // {
-    //   icon: icons.navConsole,
-    //   label: t('console'),
-    //   onClick: () => history.push('/console'),
-    //   iconActiveClass: classes.activeConsole,
-    //   iconNormalClass: classes.normalConsole,
-    // },
   ];
 
   return (

+ 13 - 7
client/src/hooks/Pagination.ts

@@ -1,14 +1,20 @@
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
 
-const PAGE_SIZE = 15;
-export const usePaginationHook = () => {
-  const [offset, setOffset] = useState(0);
+const PAGE_SIZE = 10;
+export const usePaginationHook = (list: any[]) => {
   const [currentPage, setCurrentPage] = useState(0);
-  const [total, setTotal] = useState(0);
+
+  const total = list.length;
+  const { data, offset } = useMemo(() => {
+    const offset = PAGE_SIZE * currentPage;
+    return {
+      offset,
+      data: list.slice(offset, offset + PAGE_SIZE),
+    };
+  }, [list, currentPage]);
 
   const handleCurrentPage = (page: number) => {
     setCurrentPage(page);
-    setOffset(PAGE_SIZE * page);
   };
 
   return {
@@ -17,6 +23,6 @@ export const usePaginationHook = () => {
     pageSize: PAGE_SIZE,
     handleCurrentPage,
     total,
-    setTotal,
+    data,
   };
 };

+ 17 - 2
client/src/http/Partition.ts

@@ -1,5 +1,5 @@
 import { StatusEnum } from '../components/status/Types';
-import { PartitionView } from '../pages/partitions/Types';
+import { PartitionManageParam, PartitionView } from '../pages/partitions/Types';
 import { formatNumber } from '../utils/Common';
 import BaseModel from './BaseModel';
 
@@ -14,12 +14,27 @@ export class PartitionHttp extends BaseModel implements PartitionView {
     Object.assign(this, props);
   }
 
+  static URL_BASE = `/partitions`;
+
   static getPartitions(collectionName: string): Promise<PartitionHttp[]> {
-    const path = '/partitions';
+    const path = this.URL_BASE;
 
     return super.findAll({ path, params: { collection_name: collectionName } });
   }
 
+  static createPartition(createParam: PartitionManageParam) {
+    const { collectionName, partitionName, type } = createParam;
+    const path = this.URL_BASE;
+    return super.create({
+      path,
+      data: {
+        collection_name: collectionName,
+        partition_name: partitionName,
+        type,
+      },
+    });
+  }
+
   get _id() {
     return this.id;
   }

+ 5 - 0
client/src/i18n/cn/partition.ts

@@ -2,11 +2,16 @@ const partitionTrans = {
   create: 'Create Partition',
   delete: 'Delete partition',
 
+  partition: 'Partition',
+
   id: 'ID',
   name: 'Name',
   status: 'Status',
   rowCount: 'Row Count',
   tooltip: 'data in one row',
+
+  createTitle: 'Create Partition',
+  nameWarning: '_default is reserved, cannot be used as name',
 };
 
 export default partitionTrans;

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

@@ -1,5 +1,6 @@
 const successTrans = {
   connect: 'Connet milvus success',
+  create: `{{name}} has been created`,
 };
 
 export default successTrans;

+ 5 - 0
client/src/i18n/en/partition.ts

@@ -2,11 +2,16 @@ const partitionTrans = {
   create: 'Create Partition',
   delete: 'Delete partition',
 
+  partition: 'Partition',
+
   id: 'ID',
   name: 'Name',
   status: 'Status',
   rowCount: 'Row Count',
   tooltip: 'data in one row',
+
+  createTitle: 'Create Partition',
+  nameWarning: '_default is reserved, cannot be used as name',
 };
 
 export default partitionTrans;

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

@@ -1,5 +1,6 @@
 const successTrans = {
   connect: 'Connet milvus success',
+  create: `{{name}} has been created`,
 };
 
 export default successTrans;

+ 2 - 26
client/src/pages/collections/Collection.tsx

@@ -5,11 +5,8 @@ import CustomTabList from '../../components/customTabList/CustomTabList';
 import { ITab } from '../../components/customTabList/Types';
 import Partitions from '../partitions/partitions';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
-import { useEffect, useMemo, useState } from 'react';
-import { PartitionView } from '../partitions/Types';
+import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
-import Status from '../../components/status/Status';
-import { PartitionHttp } from '../../http/Partition';
 
 enum TAB_EMUM {
   'partition',
@@ -23,8 +20,6 @@ const Collection = () => {
     }>();
 
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTION_DETAIL, { collectionName });
-  const [partitions, setPartitions] = useState<PartitionView[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
 
   const history = useHistory();
   const location = useLocation();
@@ -41,31 +36,12 @@ const Collection = () => {
   const handleTabChange = (activeIndex: number) => {
     const path = location.pathname;
     history.push(`${path}?activeIndex=${activeIndex}`);
-
-    // fetch data
-    if (activeIndex === TAB_EMUM.partition) {
-      fetchPartitions(collectionName);
-    }
   };
 
-  const fetchPartitions = async (collectionName: string) => {
-    const res = await PartitionHttp.getPartitions(collectionName);
-
-    const partitons: PartitionView[] = res.map(p =>
-      Object.assign(p, { _statusElement: <Status status={p._status} /> })
-    );
-    setLoading(false);
-    setPartitions(partitons);
-  };
-
-  useEffect(() => {
-    fetchPartitions(collectionName);
-  }, [collectionName]);
-
   const tabs: ITab[] = [
     {
       label: t('partitionTab'),
-      component: <Partitions data={partitions} loading={loading} />,
+      component: <Partitions collectionName={collectionName} />,
     },
     {
       label: t('structureTab'),

+ 33 - 30
client/src/pages/collections/Collections.tsx

@@ -42,16 +42,15 @@ const useStyles = makeStyles((theme: Theme) => ({
 const Collections = () => {
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
 
+  const [collections, setCollections] = useState<CollectionView[]>([]);
   const {
     pageSize,
     currentPage,
     handleCurrentPage,
-    // offset,
     total,
-    // setTotal
-  } = usePaginationHook();
-  const [collections, setCollections] = useState<CollectionView[]>([]);
-  // const [loading, setLoading] = useState<boolean>(false);
+    data: collectionList,
+  } = usePaginationHook(collections);
+  const [loading, setLoading] = useState<boolean>(true);
   const [selectedCollections, setSelectedCollections] = useState<
     CollectionView[]
   >([]);
@@ -63,35 +62,39 @@ const Collections = () => {
 
   const classes = useStyles();
 
-  const loading = false;
-
   const LoadIcon = icons.load;
   const ReleaseIcon = icons.release;
   const InfoIcon = icons.info;
 
   const fetchData = useCallback(async () => {
-    const res = await CollectionHttp.getCollections();
-    const statusRes = await CollectionHttp.getCollectionsIndexState();
-    setCollections(
-      res.map(v => {
-        const indexStatus = statusRes.find(item => item._name === v._name);
-        Object.assign(v, {
-          nameElement: (
-            <Link to={`/collections/${v._name}`} className={classes.link}>
-              {v._name}
-            </Link>
-          ),
-          statusElement: <Status status={v._status} />,
-          indexCreatingElement: (
-            <StatusIcon
-              type={indexStatus?._indexState || ChildrenStatusType.FINISH}
-            />
-          ),
-        });
+    try {
+      const res = await CollectionHttp.getCollections();
+      const statusRes = await CollectionHttp.getCollectionsIndexState();
+      setLoading(false);
+
+      setCollections(
+        res.map(v => {
+          const indexStatus = statusRes.find(item => item._name === v._name);
+          Object.assign(v, {
+            nameElement: (
+              <Link to={`/collections/${v._name}`} className={classes.link}>
+                {v._name}
+              </Link>
+            ),
+            statusElement: <Status status={v._status} />,
+            indexCreatingElement: (
+              <StatusIcon
+                type={indexStatus?._indexState || ChildrenStatusType.FINISH}
+              />
+            ),
+          });
 
-        return v;
-      })
-    );
+          return v;
+        })
+      );
+    } catch (err) {
+      setLoading(false);
+    }
   }, [classes.link]);
 
   useEffect(() => {
@@ -275,7 +278,7 @@ const Collections = () => {
         <MilvusGrid
           toolbarConfigs={toolbarConfigs}
           colDefinitions={colDefinitions}
-          rows={collections}
+          rows={collectionList}
           rowCount={total}
           primaryKey="id"
           openCheckBox={true}
@@ -285,7 +288,7 @@ const Collections = () => {
           page={currentPage}
           onChangePage={handlePageChange}
           rowsPerPage={pageSize}
-          // isLoading={loading}
+          isLoading={loading}
         />
       ) : (
         <>

+ 1 - 1
client/src/pages/connect/Connect.tsx

@@ -83,7 +83,7 @@ const Connect = () => {
     onChange: handleInputChange,
     variant: 'filled',
     className: classes.input,
-    placeholder: milvusTrans.addaress,
+    placeholder: milvusTrans.address,
     fullWidth: true,
     validations: [
       {

+ 0 - 9
client/src/pages/console/Console.tsx

@@ -1,9 +0,0 @@
-import { useNavigationHook } from '../../hooks/Navigation';
-import { ALL_ROUTER_TYPES } from '../../router/Types';
-
-const Console = () => {
-  useNavigationHook(ALL_ROUTER_TYPES.CONSOLE);
-  return <section>console</section>;
-};
-
-export default Console;

+ 83 - 0
client/src/pages/partitions/Create.tsx

@@ -0,0 +1,83 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '../../components/customDialog/DialogTemplate';
+import CustomInput from '../../components/customInput/CustomInput';
+import { ITextfieldConfig } from '../../components/customInput/Types';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm } from '../../utils/Form';
+import { PartitionCreateProps } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  input: {
+    margin: theme.spacing(3, 0, 0.5),
+  },
+}));
+
+const CreatePartition: FC<PartitionCreateProps> = ({
+  handleCreate,
+  handleClose,
+}) => {
+  const { t } = useTranslation('partition');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+
+  const [form, setForm] = useState<{ name: string }>({
+    name: '',
+  });
+  const checkedForm = useMemo(() => {
+    const { name } = form;
+    return formatForm({ name });
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const classes = useStyles();
+
+  const handleInputChange = (value: string) => {
+    setForm({ name: value });
+  };
+
+  const nameInputConfig: ITextfieldConfig = {
+    label: t('name'),
+    variant: 'filled',
+    key: 'name',
+    fullWidth: true,
+    onChange: handleInputChange,
+    className: classes.input,
+    validations: [
+      {
+        rule: 'require',
+        errorText: warningTrans('required', { name: t('name') }),
+      },
+      {
+        rule: 'partitionName',
+        errorText: t('nameWarning'),
+      },
+    ],
+  };
+
+  const handleCreatePartition = () => {
+    handleCreate(form.name);
+  };
+
+  return (
+    <DialogTemplate
+      title={t('createTitle')}
+      handleCancel={handleClose}
+      confirmLabel={btnTrans('create')}
+      handleConfirm={handleCreatePartition}
+      confirmDisabled={disabled}
+    >
+      <form>
+        <CustomInput
+          type="text"
+          textConfig={nameInputConfig}
+          checkValid={checkIsValid}
+          validInfo={validation}
+        />
+      </form>
+    </DialogTemplate>
+  );
+};
+
+export default CreatePartition;

+ 12 - 0
client/src/pages/partitions/Types.ts

@@ -1,5 +1,6 @@
 import { ReactElement } from 'react';
 import { StatusEnum } from '../../components/status/Types';
+import { ManageRequestMethods } from '../../types/Common';
 
 export interface PartitionView {
   _id: string;
@@ -8,3 +9,14 @@ export interface PartitionView {
   _statusElement?: ReactElement;
   _rowCount: string;
 }
+
+export interface PartitionManageParam {
+  collectionName: string;
+  partitionName: string;
+  type: ManageRequestMethods;
+}
+
+export interface PartitionCreateProps {
+  handleCreate: (name: string) => void;
+  handleClose: () => void;
+}

+ 70 - 15
client/src/pages/partitions/partitions.tsx

@@ -1,46 +1,86 @@
 import { makeStyles, Theme } from '@material-ui/core';
-import { FC, useState } from 'react';
-import { PartitionView } from './Types';
+import { FC, useContext, useEffect, useState } from 'react';
+import { PartitionManageParam, PartitionView } from './Types';
 import MilvusGrid from '../../components/grid';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { useTranslation } from 'react-i18next';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
 import CustomToolTip from '../../components/customToolTip/CustomToolTip';
+import { rootContext } from '../../context/Root';
+import CreatePartition from './Create';
+import { PartitionHttp } from '../../http/Partition';
+import Status from '../../components/status/Status';
+import { ManageRequestMethods } from '../../types/Common';
 
 const useStyles = makeStyles((theme: Theme) => ({
-  wrapper: {},
+  wrapper: {
+    height: '100%',
+  },
   icon: {
     fontSize: '20px',
     marginLeft: theme.spacing(0.5),
   },
 }));
 
-const Partitions: FC<{ data: PartitionView[]; loading: boolean }> = ({
-  data,
-  loading,
-}) => {
+const Partitions: FC<{
+  collectionName: string;
+}> = ({ collectionName }) => {
   const classes = useStyles();
   const { t } = useTranslation('partition');
+  const { t: successTrans } = useTranslation('success');
   const InfoIcon = icons.info;
 
+  const [selectedPartitions, setSelectedPartitions] = useState<PartitionView[]>(
+    []
+  );
+  const [partitions, setPartitions] = useState<PartitionView[]>([]);
   const {
     pageSize,
     currentPage,
     handleCurrentPage,
-    // offset,
     total,
-    // setTotal
-  } = usePaginationHook();
+    data: partitionList,
+  } = usePaginationHook(partitions);
+  const [loading, setLoading] = useState<boolean>(true);
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
 
-  const [selectedPartitions, setSelectedPartitions] = useState<PartitionView[]>(
-    []
-  );
+  useEffect(() => {
+    fetchPartitions(collectionName);
+  }, [collectionName]);
+
+  const fetchPartitions = async (collectionName: string) => {
+    try {
+      const res = await PartitionHttp.getPartitions(collectionName);
+
+      const partitons: PartitionView[] = res.map(p =>
+        Object.assign(p, { _statusElement: <Status status={p._status} /> })
+      );
+      setLoading(false);
+      setPartitions(partitons);
+    } catch (err) {
+      setLoading(false);
+    }
+  };
 
   const toolbarConfigs: ToolBarConfig[] = [
     {
       label: t('create'),
-      onClick: () => {},
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <CreatePartition
+                handleCreate={handleCreatePartition}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
       icon: 'add',
     },
     {
@@ -94,12 +134,27 @@ const Partitions: FC<{ data: PartitionView[]; loading: boolean }> = ({
     setSelectedPartitions([]);
   };
 
+  const handleCreatePartition = async (name: string) => {
+    const param: PartitionManageParam = {
+      partitionName: name,
+      collectionName,
+      type: ManageRequestMethods.CREATE,
+    };
+
+    await PartitionHttp.createPartition(param);
+
+    openSnackBar(successTrans('create', { name: t('partition') }));
+    handleCloseDialog();
+    // refresh partitions
+    fetchPartitions(collectionName);
+  };
+
   return (
     <section className={classes.wrapper}>
       <MilvusGrid
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
-        rows={data}
+        rows={partitionList}
         rowCount={total}
         primaryKey="id"
         openCheckBox={true}

+ 0 - 6
client/src/router/Config.ts

@@ -1,7 +1,6 @@
 import Collection from '../pages/collections/Collection';
 import Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
-// import Console from '../pages/console/Console';
 import Overview from '../pages/overview/Overview';
 import { RouterConfigType } from './Types';
 
@@ -26,11 +25,6 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     auth: true,
   },
-  // {
-  //   path: '/console',
-  //   component: Console,
-  //   auth: true,
-  // },
 ];
 
 export default RouterConfig;

+ 5 - 0
client/src/types/Common.ts

@@ -20,3 +20,8 @@ export interface IPaginationRes {
   offset: number;
   total_count: number;
 }
+
+export enum ManageRequestMethods {
+  DELETE = 'delete',
+  CREATE = 'create',
+}

+ 11 - 3
client/src/utils/Validation.ts

@@ -12,7 +12,8 @@ export type ValidType =
   | 'positiveNumber'
   | 'collectionName'
   | 'dimension'
-  | 'multiple';
+  | 'multiple'
+  | 'partitionName';
 export interface ICheckMapParam {
   value: string;
   extraParam?: IExtraParam;
@@ -84,12 +85,14 @@ export const checkClusterName = (value: string): boolean => {
 };
 
 export const checkIP = (value: string): boolean => {
-  const re = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+  const re =
+    /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
   return re.test(value);
 };
 
 export const checkCIDR = (value: string): boolean => {
-  const re = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2]\d|3[0-2])$/;
+  const re =
+    /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2]\d|3[0-2])$/;
   return re.test(value);
 };
 
@@ -116,6 +119,10 @@ export const checkCollectionName = (value: string): boolean => {
   return re.test(value);
 };
 
+export const checkPartitionName = (value: string): boolean => {
+  return value !== '_default';
+};
+
 export const checkMultiple = (param: {
   value: string;
   multipleNumber?: number;
@@ -168,6 +175,7 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
       value,
       multipleNumber: extraParam?.multipleNumber,
     }),
+    partitionName: checkPartitionName(value),
   };
 
   return checkMap[rule];