Browse Source

Merge branch 'main' of github.com:zilliztech/attu

shanghaikid 2 years ago
parent
commit
19912b5d34

+ 2 - 3
client/package.json

@@ -13,8 +13,6 @@
     "@material-ui/lab": "4.0.0-alpha.58",
     "@material-ui/pickers": "^3.3.10",
     "@mui/x-data-grid": "^4.0.0",
-    "@vitejs/plugin-react": "^2.2.0",
-    "@vitejs/plugin-react-refresh": "^1.3.6",
     "axios": "^0.21.3",
     "dayjs": "^1.10.5",
     "file-saver": "^2.0.5",
@@ -26,7 +24,6 @@
     "react-i18next": "^12.0.0",
     "react-router-dom": "^5.2.0",
     "react-syntax-highlighter": "^15.4.4",
-    "set-value": "^4.1.0",
     "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "vite": "^3.2.2",
@@ -34,6 +31,8 @@
     "web-vitals": "^1.0.1"
   },
   "devDependencies": {
+    "@vitejs/plugin-react": "^2.2.0",
+    "@vitejs/plugin-react-refresh": "^1.3.6",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "12.1.2",
     "@testing-library/react-hooks": "^7.0.1",

+ 1 - 1
client/src/components/grid/Table.tsx

@@ -50,7 +50,7 @@ const useStyles = makeStyles(theme => ({
   },
   tableCell: {
     background: theme.palette.common.white,
-    paddingLeft: theme.spacing(2),
+    padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
   },
   hoverActionCell: {
     transition: '0.2s all',

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

@@ -65,6 +65,7 @@ const collectionTrans = {
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
   queryTab: 'Data Query',
+  previewTab: 'Data Preview',
   startTip: 'Start your data query',
   exprPlaceHolder: 'Please enter your query by using advanced filter ->',
 };

+ 5 - 0
client/src/pages/collections/Collection.tsx

@@ -9,6 +9,7 @@ import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
 import Schema from '../schema/Schema';
 import Query from '../query/Query';
+import Preview from '../preview/Preview';
 
 enum TAB_EMUM {
   'schema',
@@ -48,6 +49,10 @@ const Collection = () => {
       label: collectionTrans('partitionTab'),
       component: <Partitions collectionName={collectionName} />,
     },
+    {
+      label: collectionTrans('previewTab'),
+      component: <Preview collectionName={collectionName} />,
+    },
     {
       label: collectionTrans('queryTab'),
       component: <Query collectionName={collectionName} />,

+ 196 - 0
client/src/pages/preview/Preview.tsx

@@ -0,0 +1,196 @@
+import { FC, useEffect, useState, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import AttuGrid from '../../components/grid/Grid';
+import { getQueryStyles } from '../query/Styles';
+import { CollectionHttp } from '../../http/Collection';
+import { FieldHttp } from '../../http/Field';
+import { IndexHttp } from '../../http/Index';
+import { usePaginationHook } from '../../hooks/Pagination';
+import CopyButton from '../../components/advancedSearch/CopyButton';
+import { ToolBarConfig } from '../../components/grid/Types';
+import CustomToolBar from '../../components/grid/ToolBar';
+import { DataTypeStringEnum } from '../collections/Types';
+import { generateVector } from '../../utils/Common';
+
+import {
+  FLOAT_INDEX_CONFIG,
+  BINARY_INDEX_CONFIG,
+  DEFAULT_SEARCH_PARAM_VALUE_MAP,
+} from '../../consts/Milvus';
+
+const Preview: FC<{
+  collectionName: string;
+}> = ({ collectionName }) => {
+  const [fields, setFields] = useState<any[]>([]);
+  const [tableLoading, setTableLoading] = useState<any>();
+  const [queryResult, setQueryResult] = useState<any>();
+  const [primaryKey, setPrimaryKey] = useState<string>('');
+  const { t: commonTrans } = useTranslation();
+  const { t: collectionTrans } = useTranslation('collection');
+
+  const copyTrans = commonTrans('copy');
+
+  const classes = getQueryStyles();
+  // Format result list
+  const queryResultMemo = useMemo(
+    () =>
+      queryResult?.map((resultItem: { [key: string]: any }) => {
+        // Iterate resultItem keys, then format vector(array) items.
+        const tmp = Object.keys(resultItem).reduce(
+          (prev: { [key: string]: any }, item: string) => {
+            if (Array.isArray(resultItem[item])) {
+              const list2Str = JSON.stringify(resultItem[item]);
+              prev[item] = (
+                <div className={classes.vectorTableCell}>
+                  <div>{list2Str}</div>
+                  <CopyButton
+                    label={copyTrans.label}
+                    value={list2Str}
+                    className={classes.copyBtn}
+                  />
+                </div>
+              );
+            } else {
+              prev[item] = `${resultItem[item]}`;
+            }
+            return prev;
+          },
+          {}
+        );
+        return tmp;
+      }),
+    [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
+  );
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+  } = usePaginationHook(queryResultMemo || []);
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
+  const loadData = async (collectionName: string) => {
+    // get schema list
+    const schemaList = await FieldHttp.getFields(collectionName);
+    const nameList = schemaList.map(v => ({
+      name: v.name,
+      type: v.data_type,
+    }));
+
+    const id = schemaList.find(v => v._isPrimaryKey === true);
+    const primaryKey = id?._fieldName || '';
+    const delimiter = id?.data_type === 'Int64' ? '' : '"';
+
+    const vectorField = schemaList.find(
+      v => v.data_type === 'FloatVector' || v.data_type === 'BinaryVector'
+    );
+
+    const anns_field = vectorField?._fieldName!;
+    const dim = Number(vectorField?._dimension);
+    const vectors = [generateVector(dim)];
+    // get search params
+    const indexesInfo = await IndexHttp.getIndexInfo(collectionName);
+    const indexConfig =
+      BINARY_INDEX_CONFIG[indexesInfo[0]._indexType] ||
+      FLOAT_INDEX_CONFIG[indexesInfo[0]._indexType];
+    const metric_type = indexesInfo[0]._metricType;
+    const searchParamKey = indexConfig.search[0];
+    const searchParamValue = DEFAULT_SEARCH_PARAM_VALUE_MAP[searchParamKey];
+    const searchParam = { [searchParamKey]: searchParamValue };
+    const params = `${JSON.stringify(searchParam)}`;
+    setPrimaryKey(primaryKey);
+    // Temporarily hide bool field due to incorrect return from SDK.
+    const fieldWithoutBool = nameList.filter(
+      i => i.type !== DataTypeStringEnum.Bool
+    );
+
+    // set fields
+    setFields(fieldWithoutBool);
+
+    // set loading
+    setTableLoading(true);
+
+    try {
+      // first, search random data to get random id
+      const searchRes = await CollectionHttp.vectorSearchData(collectionName, {
+        search_params: {
+          topk: 100,
+          anns_field,
+          metric_type,
+          params,
+        },
+        expr: '',
+        vectors,
+        output_fields: [primaryKey],
+        vector_type: Number(vectorField?._fieldId),
+      });
+
+      // compose random id list expression
+      const expr = `${primaryKey} in [${searchRes.results
+        .map((d: any) => `${delimiter}${d.id}${delimiter}`)
+        .join(',')}]`;
+
+      // query by random id
+      const res = await CollectionHttp.queryData(collectionName, {
+        expr: expr,
+        output_fields: fieldWithoutBool.map(i => i.name),
+      });
+
+      const result = res.data;
+      setQueryResult(result);
+    } catch (err) {
+      setQueryResult([]);
+    } finally {
+      setTableLoading(false);
+    }
+  };
+
+  // Get fields at first or collection name changed.
+  useEffect(() => {
+    collectionName && loadData(collectionName);
+  }, [collectionName]);
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      type: 'iconBtn',
+      onClick: () => {
+        loadData(collectionName);
+      },
+      label: collectionTrans('refresh'),
+      icon: 'refresh',
+    },
+  ];
+
+  return (
+    <div className={classes.root}>
+      <CustomToolBar toolbarConfigs={toolbarConfigs} />
+
+      <AttuGrid
+        toolbarConfigs={[]}
+        colDefinitions={fields.map(i => ({
+          id: i.name,
+          align: 'left',
+          disablePadding: false,
+          label: i.name,
+        }))}
+        primaryKey={primaryKey}
+        openCheckBox={false}
+        isLoading={!!tableLoading}
+        rows={result}
+        rowCount={total}
+        page={currentPage}
+        onChangePage={handlePageChange}
+        rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
+      />
+    </div>
+  );
+};
+
+export default Preview;

+ 2 - 0
client/src/pages/query/Types.ts

@@ -3,4 +3,6 @@ export interface QueryParam {
   partitions_names?: string[];
   output_fields?: string[];
   travel_timestamp?: string;
+  limit?: number;
+  offset?: number;
 }

+ 4 - 0
client/src/utils/Common.ts

@@ -61,3 +61,7 @@ export const generateHashCode = (source: string) => {
 export const generateIdByHash = (salt?: string) => {
   return generateHashCode(`${new Date().getTime().toString()}-${salt}`);
 };
+
+export const generateVector = (dim: number) => {
+  return Array.from({ length: dim }).map(() => Math.random());
+};