Browse Source

New data page (#382)

* part2

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

* finish query part3

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

* query part4

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

* finish query

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

* remove console

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

* update tip text

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

* add more tooltip

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

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 1 year ago
parent
commit
46513a2a94

+ 4 - 4
client/src/assets/icons/upload.svg

@@ -1,5 +1,5 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z" fill="white"/>
+<svg width="16" height="16" viewBox="0 0 16 16"  xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z"/>
 </svg>
 </svg>

+ 1 - 6
client/src/components/icons/Icons.tsx

@@ -118,12 +118,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
     </svg>
     </svg>
   ),
   ),
   upload: (props = {}) => (
   upload: (props = {}) => (
-    <SvgIcon
-      viewBox="0 0 16 16"
-      component={UploadIcon}
-      {...props}
-      fill="#000"
-    />
+    <SvgIcon viewBox="0 0 20 20" component={UploadIcon} {...props} fill="currentColor" />
   ),
   ),
   vectorSearch: (props = {}) => (
   vectorSearch: (props = {}) => (
     <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />
     <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />

+ 6 - 0
client/src/consts/Milvus.ts

@@ -267,6 +267,12 @@ export enum LOADING_STATE {
   LOADING,
   LOADING,
   UNLOADED,
   UNLOADED,
 }
 }
+export enum LOAD_STATE {
+  LoadStateNotExist = 'LoadStateNotExist',
+  LoadStateNotLoad = 'LoadStateNotLoad',
+  LoadStateLoading = 'LoadStateLoading',
+  LoadStateLoaded = 'LoadStateLoaded',
+}
 
 
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;

+ 224 - 0
client/src/hooks/Query.ts

@@ -0,0 +1,224 @@
+import { useState, useRef, useEffect } from 'react';
+import { DataTypeStringEnum, DYNAMIC_FIELD, LOAD_STATE } from '@/consts';
+import { Collection } from '@/http';
+
+export const useQuery = (params: {
+  onQueryStart: Function;
+  onQueryEnd?: Function;
+  onQueryFinally?: Function;
+  collectionName: string;
+}) => {
+  // state
+  const [collection, setCollection] = useState<any>({
+    fields: [],
+    consistencyLevel: '',
+    primaryKey: { value: '', type: DataTypeStringEnum.Int64 },
+    loaded: false,
+  });
+  const [currentPage, setCurrentPage] = useState<number>(0);
+  const [pageSize, setPageSize] = useState<number>(0);
+  const [total, setTotal] = useState<number>(0);
+  const [expr, setExpr] = useState<string>('');
+  const [queryResult, setQueryResult] = useState<any>({ data: [], latency: 0 });
+
+  // build local cache for pk ids
+  const pageCache = useRef(new Map());
+
+  // get next/previous expression
+  const getPageExpr = (page: number) => {
+    const cache = pageCache.current.get(
+      page > currentPage ? currentPage : page
+    );
+    const primaryKey = collection.primaryKey;
+
+    const formatPKValue = (pkId: string | number) =>
+      primaryKey.type === DataTypeStringEnum.VarChar ? `'${pkId}'` : pkId;
+
+    // If cache does not exist, return expression based on primaryKey type
+    let condition = '';
+    if (!cache) {
+      const defaultValue =
+        primaryKey.type === DataTypeStringEnum.VarChar ? "''" : '0';
+      condition = `${primaryKey.value} > ${defaultValue}`;
+    } else {
+      const { firstPKId, lastPKId } = cache;
+      const lastPKValue = formatPKValue(lastPKId);
+      const firstPKValue = formatPKValue(firstPKId);
+
+      condition =
+        page > currentPage
+          ? `(${primaryKey.value} > ${lastPKValue})`
+          : `((${primaryKey.value} <= ${lastPKValue}) && (${primaryKey.value} >= ${firstPKValue}))`;
+    }
+
+    return expr ? `${expr} && ${condition}` : condition;
+  };
+
+  // query function
+  const query = async (
+    page: number = currentPage,
+    consistency_level = collection.consistencyLevel
+  ) => {
+    if (!collection.primaryKey.value || !collection.loaded) {
+      //   console.info('[skip running query]: no key yet');
+      return;
+    }
+    const _expr = getPageExpr(page);
+    // console.log('query expr', _expr);
+    params.onQueryStart(_expr);
+
+    try {
+      // execute query
+      const res = await Collection.queryData(params.collectionName, {
+        expr: _expr,
+        output_fields: collection.fields.map((i: any) => i.name),
+        limit: pageSize || 10,
+        consistency_level,
+        // travel_timestamp: timeTravelInfo.timestamp,
+      });
+
+      // get last item of the data
+      const lastItem = res.data[res.data.length - 1];
+      // get first item of the data
+      const firstItem = res.data[0];
+      // get last pk id
+      const lastPKId: string | number =
+        lastItem && lastItem[collection.primaryKey.value];
+      // get first pk id
+      const firstPKId: string | number =
+        firstItem && firstItem[collection.primaryKey.value];
+
+      // store pk id in the cache with the page number
+      if (lastItem) {
+        pageCache.current.set(page, {
+          firstPKId,
+          lastPKId,
+        });
+      }
+
+      // console.log('query result page', page, pageCache.current);
+
+      // update query result
+      setQueryResult(res);
+
+      params.onQueryEnd?.(res);
+    } catch (e: any) {
+      reset();
+    } finally {
+      params.onQueryFinally?.();
+    }
+  };
+
+  // get collection info
+  const prepare = async (collectionName: string) => {
+    const collection = await Collection.getCollectionInfo(collectionName);
+    const schemaList = collection.fields;
+
+    const nameList = schemaList.map(v => ({
+      name: v.name,
+      type: v.fieldType,
+    }));
+
+    // if the dynamic field is enabled, we add $meta column in the grid
+    if (collection.enableDynamicField) {
+      nameList.push({
+        name: DYNAMIC_FIELD,
+        type: DataTypeStringEnum.JSON,
+      });
+    }
+    const primaryKey = schemaList.find(v => v.isPrimaryKey === true)!;
+    setCollection({
+      fields: nameList as any[],
+      consistencyLevel: collection.consistency_level,
+      primaryKey: { value: primaryKey['name'], type: primaryKey['fieldType'] },
+      loaded: collection.state === LOAD_STATE.LoadStateLoaded,
+    });
+  };
+
+  const count = async (consistency_level = collection.consistency_level) => {
+    if (!collection.primaryKey.value || !collection.loaded) {
+      return;
+    }
+    const count = 'count(*)';
+    const res = await Collection.queryData(params.collectionName, {
+      expr: expr,
+      output_fields: [count],
+      consistency_level,
+      // travel_timestamp: timeTravelInfo.timestamp,
+    });
+    setTotal(Number(res.data[0][count]));
+  };
+
+  // reset
+  const reset = () => {
+    setCurrentPage(0);
+    setQueryResult({ data: [] });
+    pageCache.current.clear();
+  };
+
+  // Get fields at first or collection name changed.
+  useEffect(() => {
+    params.collectionName && prepare(params.collectionName);
+  }, [params.collectionName]);
+
+  // query if expr is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // get count;
+    count();
+    // do the query
+    query();
+  }, [expr]);
+
+  // query if collection is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // get count;
+    count();
+    // do the query
+    query();
+  }, [collection]);
+
+  // query if page size is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // do the query
+    query();
+  }, [pageSize]);
+
+  return {
+    // collection info(primaryKey, consistency level, fields)
+    collection,
+    // total query count
+    total,
+    // page size
+    pageSize,
+    // update page size
+    setPageSize,
+    // update consistency level
+    setConsistencyLevel: (level: string) => {
+      setCollection({ ...collection, consistencyLevel: level });
+    },
+    // current page
+    currentPage,
+    // query current data page
+    setCurrentPage,
+    // expression to query
+    expr,
+    // expression updater
+    setExpr,
+    // query result
+    queryResult,
+    // query
+    query,
+    // reset
+    reset,
+    // count
+    count,
+    // get expression
+    getPageExpr,
+  };
+};

+ 1 - 0
client/src/hooks/index.tsx

@@ -4,4 +4,5 @@ export * from './Navigation';
 export * from './Pagination';
 export * from './Pagination';
 export * from './Result';
 export * from './Result';
 export * from './SystemView';
 export * from './SystemView';
+export * from './Query';
 export * from './TimeTravel';
 export * from './TimeTravel';

+ 3 - 2
client/src/http/Collection.ts

@@ -4,7 +4,7 @@ import { VectorSearchParam } from '@/types/SearchTypes';
 import { QueryParam } from '@/pages/query/Types';
 import { QueryParam } from '@/pages/query/Types';
 import { formatNumber } from '@/utils/Common';
 import { formatNumber } from '@/utils/Common';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
-import { LOADING_STATE } from '@/consts';
+import { LOADING_STATE, LOAD_STATE } from '@/consts';
 import {
 import {
   IndexDescription,
   IndexDescription,
   ShowCollectionsType,
   ShowCollectionsType,
@@ -27,6 +27,7 @@ export class Collection extends BaseModel implements CollectionData {
   public index_descriptions!: IndexDescription[];
   public index_descriptions!: IndexDescription[];
   public schema!: CollectionSchema;
   public schema!: CollectionSchema;
   public replicas!: ReplicaInfo[];
   public replicas!: ReplicaInfo[];
+  public state!: LOAD_STATE;
 
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_STATISTICS_URL = '/collections/statistics';
   static COLLECTIONS_STATISTICS_URL = '/collections/statistics';
@@ -147,7 +148,7 @@ export class Collection extends BaseModel implements CollectionData {
     return formatNumber(Number(this.rowCount));
     return formatNumber(Number(this.rowCount));
   }
   }
 
 
-  // load status
+  // TODO: deprecated
   get status() {
   get status() {
     // If not load, insight server will return '-1'. Otherwise milvus will return percentage
     // If not load, insight server will return '-1'. Otherwise milvus will return percentage
     return this.loadedPercentage === '-1'
     return this.loadedPercentage === '-1'

+ 13 - 3
client/src/i18n/cn/button.ts

@@ -12,23 +12,33 @@ const btnTrans = {
   drop: 'drop',
   drop: 'drop',
   release: '释放',
   release: '释放',
   load: '加载',
   load: '加载',
-  insert: '导入数据',
+  insert: '插入数据',
+  importFile: '导入文件',
   refresh: '刷新',
   refresh: '刷新',
   next: '下一步',
   next: '下一步',
   previous: '上一步',
   previous: '上一步',
   done: '完成',
   done: '完成',
   vectorSearch: '向量搜索',
   vectorSearch: '向量搜索',
   query: '查询',
   query: '查询',
-  importSampleData: '入样本数据',
+  importSampleData: '入样本数据',
   loading: '加载中...',
   loading: '加载中...',
   importing: '导入中...',
   importing: '导入中...',
   example: '生成随机向量',
   example: '生成随机向量',
   rename: '重命名',
   rename: '重命名',
   duplicate: '复制',
   duplicate: '复制',
-  export: '导出',
+  export: '导出数据',
   empty: '清空数据',
   empty: '清空数据',
   flush: '落盘(Flush)',
   flush: '落盘(Flush)',
   compact: '压缩(Compact)',
   compact: '压缩(Compact)',
+  copyJson: '复制为JSON',
+
+  // tips
+  importFileTooltip: '导入JSON或者CSV文件',
+  importSampleDataTooltip: '导入样例数据',
+  exportTooltip: '将选择的查询结果导出到CSV文件',
+  copyJsonTooltip: '复制所选的数据为JSON格式',
+  emptyTooltip: '清空所有数据',
+  deleteTooltip: '删除所选的数据',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

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

@@ -16,10 +16,7 @@ const collectionTrans = {
   alias: '别名',
   alias: '别名',
   aliasTooltip: '请选择一个Collection创建别名',
   aliasTooltip: '请选择一个Collection创建别名',
   download: '下载',
   download: '下载',
-  downloadTooltip: '将所有查询结果导出到CSV文件',
-  downloadDisabledTooltip: '请在导出前查询数据',
   empty: '清空数据',
   empty: '清空数据',
-  emptyDataDisableTooltip: '请选择一个已加载的Collection进行清空数据操作',
 
 
   collection: 'Collection',
   collection: 'Collection',
   entities: 'Entities',
   entities: 'Entities',
@@ -97,11 +94,10 @@ const collectionTrans = {
   // collection tabs
   // collection tabs
   partitionTab: '分区',
   partitionTab: '分区',
   schemaTab: 'Schema',
   schemaTab: 'Schema',
-  queryTab: '数据查询',
+  queryTab: '数据',
   previewTab: '数据预览',
   previewTab: '数据预览',
   segmentsTab: '数据段(Segments)',
   segmentsTab: '数据段(Segments)',
   startTip: '开始你的数据查询',
   startTip: '开始你的数据查询',
-  dataQuerylimits: '请注意,你的数据查询的结果数量最大为16384。',
   exprPlaceHolder: '请输入你的数据查询,例如 id > 0',
   exprPlaceHolder: '请输入你的数据查询,例如 id > 0',
 
 
   // alias dialog
   // alias dialog

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

@@ -15,6 +15,7 @@ const searchTrans = {
   timeTravel: '时间旅行',
   timeTravel: '时间旅行',
   timeTravelPrefix: '之前的数据',
   timeTravelPrefix: '之前的数据',
   dynamicFields: '动态字段',
   dynamicFields: '动态字段',
+  collectionNotLoaded: 'Collection 没有加载',
 };
 };
 
 
 export default searchTrans;
 export default searchTrans;

+ 12 - 2
client/src/i18n/en/button.ts

@@ -12,14 +12,15 @@ const btnTrans = {
   drop: 'Drop',
   drop: 'Drop',
   release: 'Release',
   release: 'Release',
   load: 'Load',
   load: 'Load',
-  insert: 'Import Data',
+  insert: 'Add Data',
+  importFile: 'Import File',
   refresh: 'Refresh',
   refresh: 'Refresh',
   next: 'Next',
   next: 'Next',
   previous: 'Previous',
   previous: 'Previous',
   done: 'Done',
   done: 'Done',
   vectorSearch: 'Vector Search',
   vectorSearch: 'Vector Search',
   query: 'Query',
   query: 'Query',
-  importSampleData: 'Import Sample data',
+  importSampleData: 'Add Sample Data',
   loading: 'Loading...',
   loading: 'Loading...',
   importing: 'Importing...',
   importing: 'Importing...',
   example: 'Generate Random Vector',
   example: 'Generate Random Vector',
@@ -29,6 +30,15 @@ const btnTrans = {
   empty: 'Empty',
   empty: 'Empty',
   flush: 'Flush',
   flush: 'Flush',
   compact: 'Compact',
   compact: 'Compact',
+  copyJson: 'Copy as JSON',
+
+  // tips
+  importFileTooltip: 'Import JSON or CSV file',
+  importSampleDataTooltip: 'Import sample data into the current collection',
+  exportTooltip: 'Export selected data to csv',
+  copyJsonTooltip: 'Copy selected data as JSON format',
+  emptyTooltip: 'Empty all data in the collection',
+  deleteTooltip: 'Delete selected data',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

+ 2 - 5
client/src/i18n/en/collection.ts

@@ -16,8 +16,7 @@ const collectionTrans = {
   alias: 'Alias',
   alias: 'Alias',
   aliasTooltip: 'Please select one collection to create alias',
   aliasTooltip: 'Please select one collection to create alias',
   download: 'Download',
   download: 'Download',
-  downloadTooltip: 'Export all query results to CSV file',
-  downloadDisabledTooltip: 'Please query data before exporting',
+  downloadDisabledTooltip: 'Please select data before exporting',
   empty: 'empty data',
   empty: 'empty data',
   emptyDataDisableTooltip: 'Please select one loaded collection to empty data',
   emptyDataDisableTooltip: 'Please select one loaded collection to empty data',
 
 
@@ -99,12 +98,10 @@ const collectionTrans = {
   // collection tabs
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
   schemaTab: 'Schema',
-  queryTab: 'Data Query',
+  queryTab: 'Data',
   previewTab: 'Data Preview',
   previewTab: 'Data Preview',
   segmentsTab: 'Segments',
   segmentsTab: 'Segments',
   startTip: 'Start Your Data Query',
   startTip: 'Start Your Data Query',
-  dataQuerylimits:
-    ' Please note that the maximum number of results for your data query is 16384.',
   exprPlaceHolder: 'Please enter your data query, for example id > 0',
   exprPlaceHolder: 'Please enter your data query, for example id > 0',
 
 
   // alias dialog
   // alias dialog

+ 1 - 1
client/src/i18n/en/insert.ts

@@ -30,7 +30,7 @@ const insertTrans = {
   statusSuccess: 'Import Data Successfully!',
   statusSuccess: 'Import Data Successfully!',
   statusError: 'Import Data Failed!',
   statusError: 'Import Data Failed!',
 
 
-  importSampleData: 'Import sample data into {{collection}}',
+  importSampleData: 'Add sample data into {{collection}}',
   sampleDataSize: 'Choose sample data size',
   sampleDataSize: 'Choose sample data size',
   importSampleDataDesc: `This function imports randomly generated data matching the collection schema. Useful for testing and development. Click the download button to get the data.`,
   importSampleDataDesc: `This function imports randomly generated data matching the collection schema. Useful for testing and development. Click the download button to get the data.`,
   downloadSampleDataCSV: `Download Sample CSV Data`,
   downloadSampleDataCSV: `Download Sample CSV Data`,

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

@@ -15,6 +15,7 @@ const searchTrans = {
   timeTravel: 'Time Travel',
   timeTravel: 'Time Travel',
   timeTravelPrefix: 'Data before',
   timeTravelPrefix: 'Data before',
   dynamicFields: 'Dynamic Fields',
   dynamicFields: 'Dynamic Fields',
+  collectionNotLoaded: 'Collection not loaded',
 };
 };
 
 
 export default searchTrans;
 export default searchTrans;

+ 6 - 11
client/src/pages/collections/Collection.tsx

@@ -10,7 +10,6 @@ import { ITab } from '@/components/customTabList/Types';
 import Partitions from '../partitions/Partitions';
 import Partitions from '../partitions/Partitions';
 import Schema from '../schema/Schema';
 import Schema from '../schema/Schema';
 import Query from '../query/Query';
 import Query from '../query/Query';
-import Preview from '../preview/Preview';
 import Segments from '../segments/Segments';
 import Segments from '../segments/Segments';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
@@ -46,6 +45,11 @@ const Collection = () => {
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
 
 
   const tabs: ITab[] = [
   const tabs: ITab[] = [
+    {
+      label: collectionTrans('queryTab'),
+      component: <Query />,
+      path: `query`,
+    },
     {
     {
       label: collectionTrans('schemaTab'),
       label: collectionTrans('schemaTab'),
       component: <Schema />,
       component: <Schema />,
@@ -56,16 +60,7 @@ const Collection = () => {
       component: <Partitions />,
       component: <Partitions />,
       path: `partitions`,
       path: `partitions`,
     },
     },
-    {
-      label: collectionTrans('previewTab'),
-      component: <Preview />,
-      path: `preview`,
-    },
-    {
-      label: collectionTrans('queryTab'),
-      component: <Query />,
-      path: `query`,
-    },
+
     {
     {
       label: collectionTrans('segmentsTab'),
       label: collectionTrans('segmentsTab'),
       component: <Segments />,
       component: <Segments />,

+ 5 - 65
client/src/pages/collections/Collections.tsx

@@ -9,12 +9,11 @@ import {
   dataContext,
   dataContext,
   webSocketContext,
   webSocketContext,
 } from '@/context';
 } from '@/context';
-import { Collection, MilvusService, DataService, MilvusIndex } from '@/http';
+import { Collection, MilvusService, MilvusIndex } from '@/http';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import AttuGrid from '@/components/grid/Grid';
 import AttuGrid from '@/components/grid/Grid';
 import CustomToolBar from '@/components/grid/ToolBar';
 import CustomToolBar from '@/components/grid/ToolBar';
-import { InsertDataParam } from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
 import EmptyCard from '@/components/cards/EmptyCard';
 import EmptyCard from '@/components/cards/EmptyCard';
@@ -26,7 +25,6 @@ import ReleaseCollectionDialog from '../dialogs/ReleaseCollectionDialog';
 import DropCollectionDialog from '../dialogs/DropCollectionDialog';
 import DropCollectionDialog from '../dialogs/DropCollectionDialog';
 import RenameCollectionDialog from '../dialogs/RenameCollectionDialog';
 import RenameCollectionDialog from '../dialogs/RenameCollectionDialog';
 import DuplicateCollectionDialog from '../dialogs/DuplicateCollectionDailog';
 import DuplicateCollectionDialog from '../dialogs/DuplicateCollectionDailog';
-import EmptyDataDialog from '../dialogs/EmptyDataDialog';
 import InsertDialog from '../dialogs/insert/Dialog';
 import InsertDialog from '../dialogs/insert/Dialog';
 import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 import { LOADING_STATE } from '@/consts';
 import { LOADING_STATE } from '@/consts';
@@ -151,7 +149,7 @@ const Collections = () => {
       Object.assign(v, {
       Object.assign(v, {
         nameElement: (
         nameElement: (
           <Link
           <Link
-            to={`/collections/${v.collectionName}/schema`}
+            to={`/collections/${v.collectionName}/data`}
             className={classes.link}
             className={classes.link}
             title={v.collectionName}
             title={v.collectionName}
           >
           >
@@ -303,7 +301,7 @@ const Collections = () => {
       type: 'button',
       type: 'button',
       btnVariant: 'text',
       btnVariant: 'text',
       btnColor: 'secondary',
       btnColor: 'secondary',
-      label: btnTrans('insert'),
+      label: btnTrans('importFile'),
       onClick: () => {
       onClick: () => {
         setDialog({
         setDialog({
           open: true,
           open: true,
@@ -319,30 +317,7 @@ const Collections = () => {
                 }
                 }
                 // user can't select partition on collection page, so default value is ''
                 // user can't select partition on collection page, so default value is ''
                 defaultSelectedPartition={''}
                 defaultSelectedPartition={''}
-                handleInsert={async (
-                  collectionName: string,
-                  partitionName: string,
-                  fieldData: any[]
-                ): Promise<{ result: boolean; msg: string }> => {
-                  const param: InsertDataParam = {
-                    partition_name: partitionName,
-                    fields_data: fieldData,
-                  };
-                  try {
-                    await DataService.insertData(collectionName, param);
-                    await DataService.flush(collectionName);
-                    // update collections
-                    fetchData();
-                    return { result: true, msg: '' };
-                  } catch (err: any) {
-                    const {
-                      response: {
-                        data: { message },
-                      },
-                    } = err;
-                    return { result: false, msg: message || '' };
-                  }
-                }}
+                onInsert={() => {}}
               />
               />
             ),
             ),
           },
           },
@@ -420,42 +395,7 @@ const Collections = () => {
       disabledTooltip: collectionTrans('duplicateTooltip'),
       disabledTooltip: collectionTrans('duplicateTooltip'),
       disabled: data => data.length !== 1,
       disabled: data => data.length !== 1,
     },
     },
-    {
-      icon: 'deleteOutline',
-      type: 'button',
-      btnVariant: 'text',
-      onClick: () => {
-        setDialog({
-          open: true,
-          type: 'custom',
-          params: {
-            component: (
-              <EmptyDataDialog
-                cb={async () => {
-                  openSnackBar(
-                    successTrans('empty', {
-                      name: collectionTrans('collection'),
-                    })
-                  );
-                  setSelectedCollections([]);
-                  await fetchData();
-                }}
-                collectionName={selectedCollections[0].collectionName}
-              />
-            ),
-          },
-        });
-      },
-      label: btnTrans('empty'),
-      disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
-      disabled: (data: any) => {
-        if (data.length === 0 || data.length > 1) {
-          return true;
-        } else {
-          return Number(data[0].loadedPercentage) !== 100;
-        }
-      },
-    },
+
     {
     {
       icon: 'delete',
       icon: 'delete',
       type: 'button',
       type: 'button',

+ 1 - 1
client/src/pages/dialogs/EmptyDataDialog.tsx

@@ -15,7 +15,7 @@ const EmptyDataDialog: FC<EmptyDataProps> = props => {
 
 
   const handleDelete = async () => {
   const handleDelete = async () => {
     // duplicate
     // duplicate
-    DataService.emptyData(collectionName);
+    await DataService.emptyData(collectionName);
     // close dialog
     // close dialog
     handleCloseDialog();
     handleCloseDialog();
     cb && cb();
     cb && cb();

+ 4 - 1
client/src/pages/dialogs/ImportSampleDialog.tsx

@@ -76,7 +76,7 @@ const sizeOptions = [
   },
   },
 ];
 ];
 
 
-const ImportSampleDialog: FC<{ collection: string }> = props => {
+const ImportSampleDialog: FC<{ collection: string; cb?: Function }> = props => {
   const classes = getStyles();
   const classes = getStyles();
   const { collection } = props;
   const { collection } = props;
   const [size, setSize] = useState<string>(sizeOptions[0].value);
   const [size, setSize] = useState<string>(sizeOptions[0].value);
@@ -118,6 +118,9 @@ const ImportSampleDialog: FC<{ collection: string }> = props => {
         return { result: res.sampleFile, msg: '' };
         return { result: res.sampleFile, msg: '' };
       }
       }
       await DataService.flush(collectionName);
       await DataService.flush(collectionName);
+      if (props.cb) {
+        props.cb();
+      }
       return { result: true, msg: '' };
       return { result: true, msg: '' };
     } catch (err: any) {
     } catch (err: any) {
       const {
       const {

+ 21 - 10
client/src/pages/dialogs/insert/Dialog.tsx

@@ -25,6 +25,8 @@ import {
   InsertStatusEnum,
   InsertStatusEnum,
   InsertStepperEnum,
   InsertStepperEnum,
 } from './Types';
 } from './Types';
+import { InsertDataParam } from '@/pages/collections/Types';
+import { DataService } from '@/http';
 
 
 const getStyles = makeStyles((theme: Theme) => ({
 const getStyles = makeStyles((theme: Theme) => ({
   icon: {
   icon: {
@@ -44,7 +46,7 @@ const InsertContainer: FC<InsertContentProps> = ({
 
 
   partitions,
   partitions,
   schema,
   schema,
-  handleInsert,
+  onInsert,
 }) => {
 }) => {
   const classes = getStyles();
   const classes = getStyles();
 
 
@@ -330,17 +332,26 @@ const InsertContainer: FC<InsertContentProps> = ({
             isContainFieldNames ? csvData.slice(1) : csvData
             isContainFieldNames ? csvData.slice(1) : csvData
           );
           );
 
 
-    const { result, msg } = await handleInsert(
-      collectionValue,
-      partitionValue,
-      data
-    );
+    const param: InsertDataParam = {
+      partition_name: partitionValue,
+      fields_data: data,
+    };
 
 
-    if (!result) {
-      setInsertFailMsg(msg);
+    try {
+      await DataService.insertData(collectionValue, param);
+      await DataService.flush(collectionValue);
+      // update collections
+      onInsert();
+      setInsertStatus(InsertStatusEnum.success);
+    } catch (err: any) {
+      const {
+        response: {
+          data: { message },
+        },
+      } = err;
+      setInsertFailMsg(message);
+      setInsertStatus(InsertStatusEnum.error);
     }
     }
-    const status = result ? InsertStatusEnum.success : InsertStatusEnum.error;
-    setInsertStatus(status);
   };
   };
 
 
   const handleCollectionChange = (name: string) => {
   const handleCollectionChange = (name: string) => {

+ 1 - 5
client/src/pages/dialogs/insert/Types.ts

@@ -19,11 +19,7 @@ export interface InsertContentProps {
   // if default value is not '', partitions not selectable
   // if default value is not '', partitions not selectable
   defaultSelectedPartition: string;
   defaultSelectedPartition: string;
 
 
-  handleInsert: (
-    collectionName: string,
-    partitionName: string,
-    fieldData: any[]
-  ) => Promise<{ result: boolean; msg: string }>;
+  onInsert: Function;
 }
 }
 
 
 export enum InsertStepperEnum {
 export enum InsertStepperEnum {

+ 1 - 1
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -154,7 +154,7 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <div>
         <div>
           <Status status={status} percentage={loadedPercentage} />
           <Status status={status} percentage={loadedPercentage} />
         </div>
         </div>
-        <Link className="link" to={`/collections/${collectionName}/schema`}>
+        <Link className="link" to={`/collections/${collectionName}/data`}>
           {collectionName}
           {collectionName}
           <RightArrowIcon classes={{ root: classes.icon }} />
           <RightArrowIcon classes={{ root: classes.icon }} />
         </Link>
         </Link>

+ 237 - 148
client/src/pages/query/Query.tsx

@@ -1,10 +1,10 @@
-import { useEffect, useState, useRef, useContext } from 'react';
+import { useState, useEffect, useRef, useContext } from 'react';
 import { TextField } from '@material-ui/core';
 import { TextField } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useParams } from 'react-router-dom';
 import { useParams } from 'react-router-dom';
 import { rootContext } from '@/context';
 import { rootContext } from '@/context';
-import { Collection, DataService } from '@/http';
-import { usePaginationHook, useSearchResult } from '@/hooks';
+import { DataService } from '@/http';
+import { useQuery, useSearchResult } from '@/hooks';
 import { saveCsvAs } from '@/utils';
 import { saveCsvAs } from '@/utils';
 import EmptyCard from '@/components/cards/EmptyCard';
 import EmptyCard from '@/components/cards/EmptyCard';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
@@ -14,152 +14,233 @@ import { ToolBarConfig } from '@/components/grid/Types';
 import Filter from '@/components/advancedSearch';
 import Filter from '@/components/advancedSearch';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import CustomToolBar from '@/components/grid/ToolBar';
 import CustomToolBar from '@/components/grid/ToolBar';
+import InsertDialog from '../dialogs/insert/Dialog';
 import { getLabelDisplayedRows } from '../search/Utils';
 import { getLabelDisplayedRows } from '../search/Utils';
 import { getQueryStyles } from './Styles';
 import { getQueryStyles } from './Styles';
 import {
 import {
   DYNAMIC_FIELD,
   DYNAMIC_FIELD,
   DataTypeStringEnum,
   DataTypeStringEnum,
   CONSISTENCY_LEVEL_OPTIONS,
   CONSISTENCY_LEVEL_OPTIONS,
+  ConsistencyLevelEnum,
 } from '@/consts';
 } from '@/consts';
 import CustomSelector from '@/components/customSelector/CustomSelector';
 import CustomSelector from '@/components/customSelector/CustomSelector';
+import EmptyDataDialog from '../dialogs/EmptyDataDialog';
+import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 
 
 const Query = () => {
 const Query = () => {
-  const { collectionName } = useParams<{ collectionName: string }>();
-  const [fields, setFields] = useState<any[]>([]);
-  const [expression, setExpression] = useState('');
-  const [tableLoading, setTableLoading] = useState<any>();
-  const [queryResult, setQueryResult] = useState<any>();
+  // get collection name from url
+  const { collectionName = '' } = useParams<{ collectionName: string }>();
+  // UI state
+  const [tableLoading, setTableLoading] = useState<boolean>();
   const [selectedData, setSelectedData] = useState<any[]>([]);
   const [selectedData, setSelectedData] = useState<any[]>([]);
-  const [primaryKey, setPrimaryKey] = useState<{ value: string; type: string }>(
-    { value: '', type: DataTypeStringEnum.Int64 }
-  );
-  const [consistency_level, setConsistency_level] = useState<string>('');
-
-  // latency
-  const [latency, setLatency] = useState<number>(0);
-
+  const [expression, setExpression] = useState<string>('');
+  // UI functions
   const { setDialog, handleCloseDialog, openSnackBar } =
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
     useContext(rootContext);
+  // icons
   const VectorSearchIcon = icons.vectorSearch;
   const VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
   const ResetIcon = icons.refresh;
-
+  // translations
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
   const { t: searchTrans } = useTranslation('search');
   const { t: searchTrans } = useTranslation('search');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
-
+  // classes
   const classes = getQueryStyles();
   const classes = getQueryStyles();
 
 
-  // Format result list
-  const queryResultMemo = useSearchResult(queryResult);
-
-  const {
-    pageSize,
-    handlePageSize,
-    currentPage,
-    handleCurrentPage,
-    total,
-    data: result,
-    order,
-    orderBy,
-    handleGridSort,
-  } = usePaginationHook(queryResultMemo || []);
-
-  const handlePageChange = (e: any, page: number) => {
-    handleCurrentPage(page);
-  };
-
-  const getFields = async (collectionName: string) => {
-    const collection = await Collection.getCollectionInfo(collectionName);
-    const schemaList = collection.fields;
-
-    const nameList = schemaList.map(v => ({
-      name: v.name,
-      type: v.fieldType,
-    }));
-
-    // if the dynamic field is enabled, we add $meta column in the grid
-    if (collection.enableDynamicField) {
-      nameList.push({
-        name: DYNAMIC_FIELD,
-        type: DataTypeStringEnum.JSON,
-      });
-    }
-
-    const primaryKey = schemaList.find(v => v.isPrimaryKey === true)!;
-    setPrimaryKey({ value: primaryKey['name'], type: primaryKey['fieldType'] });
-    setConsistency_level(collection.consistency_level);
-
-    setFields(nameList);
-  };
-
-  // Get fields at first or collection name changed.
-  useEffect(() => {
-    collectionName && getFields(collectionName);
-  }, [collectionName]);
-
+  // UI ref
   const filterRef = useRef();
   const filterRef = useRef();
+  const inputRef = useRef<HTMLInputElement>();
 
 
-  const handleFilterReset = () => {
+  // UI event handlers
+  const handleFilterReset = async () => {
+    // reset advanced filter
     const currentFilter: any = filterRef.current;
     const currentFilter: any = filterRef.current;
     currentFilter?.getReset();
     currentFilter?.getReset();
+    // update UI expression
     setExpression('');
     setExpression('');
-    setTableLoading(null);
-    setQueryResult(null);
-    handleCurrentPage(0);
+    // reset query
+    reset();
+    // ensure not loading
+    setTableLoading(false);
   };
   };
-
-  const handleFilterSubmit = (expression: string) => {
+  const handleFilterSubmit = async (expression: string) => {
+    // update UI expression
     setExpression(expression);
     setExpression(expression);
-    handleQuery(expression);
+    // update expression
+    setExpr(expression);
   };
   };
-
-  const handleQuery = async (expr: string = '') => {
-    setTableLoading(true);
-    if (expr === '') {
-      handleFilterReset();
-      return;
-    }
-    try {
-      const res = await Collection.queryData(collectionName!, {
-        expr: expr,
-        output_fields: fields.map(i => i.name),
-        offset: 0,
-        limit: 16384,
-        consistency_level: consistency_level,
-        // travel_timestamp: timeTravelInfo.timestamp,
-      });
-      const result = res.data;
-      setQueryResult(result);
-      setLatency(res.latency);
-    } catch (err) {
-      setQueryResult([]);
-    } finally {
-      setTableLoading(false);
-    }
+  const handlePageChange = async (e: any, page: number) => {
+    // do the query
+    await query(page);
+    // update page number
+    setCurrentPage(page);
   };
   };
-
-  const handleSelectChange = (value: any) => {
+  const onSelectChange = (value: any) => {
     setSelectedData(value);
     setSelectedData(value);
   };
   };
-
+  const onDelete = async () => {
+    // reset query
+    reset();
+    count(ConsistencyLevelEnum.Strong);
+    await query(0, ConsistencyLevelEnum.Strong);
+  };
   const handleDelete = async () => {
   const handleDelete = async () => {
-    await DataService.deleteEntities(collectionName!, {
-      expr: `${primaryKey.value} in [${selectedData
+    // call delete api
+    await DataService.deleteEntities(collectionName, {
+      expr: `${collection.primaryKey.value} in [${selectedData
         .map(v =>
         .map(v =>
-          primaryKey.type === DataTypeStringEnum.VarChar
-            ? `"${v[primaryKey.value]}"`
-            : v[primaryKey.value]
+          collection.primaryKey.type === DataTypeStringEnum.VarChar
+            ? `"${v[collection.primaryKey.value]}"`
+            : v[collection.primaryKey.value]
         )
         )
         .join(',')}]`,
         .join(',')}]`,
     });
     });
     handleCloseDialog();
     handleCloseDialog();
     openSnackBar(successTrans('delete', { name: collectionTrans('entities') }));
     openSnackBar(successTrans('delete', { name: collectionTrans('entities') }));
-    handleQuery(expression);
+    setSelectedData([]);
+    await onDelete();
   };
   };
 
 
+  // Query hook
+  const {
+    collection,
+    currentPage,
+    total,
+    pageSize,
+    expr,
+    queryResult,
+    setPageSize,
+    setConsistencyLevel,
+    setCurrentPage,
+    setExpr,
+    query,
+    reset,
+    count,
+  } = useQuery({
+    collectionName,
+    onQueryStart: (expr: string = '') => {
+      setTableLoading(true);
+      if (expr === '') {
+        handleFilterReset();
+        return;
+      }
+    },
+    onQueryFinally: () => {
+      setTableLoading(false);
+    },
+  });
+
+  // Format result list
+  const queryResultMemo = useSearchResult(queryResult.data);
+
+  // Toolbar settings
   const toolbarConfigs: ToolBarConfig[] = [
   const toolbarConfigs: ToolBarConfig[] = [
+    {
+      icon: 'uploadFile',
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: btnTrans('importFile'),
+      tooltip: btnTrans('importFileTooltip'),
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <InsertDialog
+                defaultSelectedCollection={collectionName}
+                // user can't select partition on collection page, so default value is ''
+                defaultSelectedPartition={''}
+                onInsert={() => {}}
+              />
+            ),
+          },
+        });
+      },
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <ImportSampleDialog collection={collectionName} cb={onDelete} />
+            ),
+          },
+        });
+      },
+      tooltip: btnTrans('importSampleDataTooltip'),
+      label: btnTrans('importSampleData'),
+      icon: 'add',
+      // tooltip: collectionTrans('deleteTooltip'),
+    },
+    {
+      icon: 'deleteOutline',
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <EmptyDataDialog
+                cb={async () => {
+                  openSnackBar(
+                    successTrans('empty', {
+                      name: collectionTrans('collection'),
+                    })
+                  );
+                  await onDelete();
+                }}
+                collectionName={collectionName}
+              />
+            ),
+          },
+        });
+      },
+      disabled: () => total == 0,
+      label: btnTrans('empty'),
+      tooltip: btnTrans('emptyTooltip'),
+      disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        saveCsvAs(selectedData, `${collectionName}.query.csv`);
+      },
+      label: btnTrans('export'),
+      icon: 'download',
+      tooltip: btnTrans('exportTooltip'),
+      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
+      disabled: () => !selectedData?.length,
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: async () => {
+        const json = JSON.stringify(selectedData);
+        try {
+          await navigator.clipboard.writeText(json);
+          alert(`${selectedData.length} rows copied to clipboard`);
+        } catch (err) {
+          console.error('Failed to copy text: ', err);
+        }
+      },
+      label: btnTrans('copyJson'),
+      icon: 'copy',
+      tooltip: btnTrans('copyJsonTooltip'),
+      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
+      disabled: () => !selectedData?.length,
+    },
+
     {
     {
       type: 'button',
       type: 'button',
       btnVariant: 'text',
       btnVariant: 'text',
@@ -183,27 +264,21 @@ const Query = () => {
       },
       },
       label: btnTrans('delete'),
       label: btnTrans('delete'),
       icon: 'delete',
       icon: 'delete',
-      // tooltip: collectionTrans('deleteTooltip'),
+      tooltip: btnTrans('deleteTooltip'),
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: () => selectedData.length === 0,
       disabled: () => selectedData.length === 0,
     },
     },
-    {
-      type: 'button',
-      btnVariant: 'text',
-      onClick: () => {
-        saveCsvAs(queryResult, 'milvus_query_result.csv');
-      },
-      label: btnTrans('export'),
-      icon: 'download',
-      tooltip: collectionTrans('downloadTooltip'),
-      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
-      disabled: () => !queryResult?.length,
-    },
   ];
   ];
 
 
+  useEffect(() => {
+    if (inputRef.current) {
+      inputRef.current.focus();
+    }
+  }, []);
+
   return (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
-      <CustomToolBar toolbarConfigs={toolbarConfigs} />
+      <CustomToolBar toolbarConfigs={toolbarConfigs} hideOnDisable={true} />
       <div className={classes.toolbar}>
       <div className={classes.toolbar}>
         <div className="left">
         <div className="left">
           <TextField
           <TextField
@@ -214,28 +289,37 @@ const Query = () => {
                 multiline: 'multiline',
                 multiline: 'multiline',
               },
               },
             }}
             }}
-            placeholder={collectionTrans('exprPlaceHolder')}
             value={expression}
             value={expression}
             onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
             onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
               setExpression(e.target.value as string);
               setExpression(e.target.value as string);
             }}
             }}
+            disabled={!collection.loaded}
+            InputLabelProps={{ shrink: true }}
+            label={collectionTrans('exprPlaceHolder')}
             onKeyDown={e => {
             onKeyDown={e => {
               if (e.key === 'Enter') {
               if (e.key === 'Enter') {
-                // Do code here
-                handleQuery(expression);
+                // reset page
+                setCurrentPage(0);
+                if (expr !== expression) {
+                  setExpr(expression);
+                } else {
+                  // ensure query
+                  query();
+                }
                 e.preventDefault();
                 e.preventDefault();
               }
               }
             }}
             }}
+            inputRef={inputRef}
           />
           />
           <Filter
           <Filter
             ref={filterRef}
             ref={filterRef}
             title="Advanced Filter"
             title="Advanced Filter"
-            fields={fields.filter(
-              i =>
+            fields={collection.fields.filter(
+              (i: any) =>
                 i.type !== DataTypeStringEnum.FloatVector &&
                 i.type !== DataTypeStringEnum.FloatVector &&
                 i.type !== DataTypeStringEnum.BinaryVector
                 i.type !== DataTypeStringEnum.BinaryVector
             )}
             )}
-            filterDisabled={false}
+            filterDisabled={!collection.loaded}
             onSubmit={handleFilterSubmit}
             onSubmit={handleFilterSubmit}
             showTitle={false}
             showTitle={false}
             showTooltip={false}
             showTooltip={false}
@@ -243,13 +327,14 @@ const Query = () => {
           {/* </div> */}
           {/* </div> */}
           <CustomSelector
           <CustomSelector
             options={CONSISTENCY_LEVEL_OPTIONS}
             options={CONSISTENCY_LEVEL_OPTIONS}
-            value={consistency_level}
-            label={collectionTrans('consistencyLevel')}
+            value={collection.consistencyLevel}
+            label={collectionTrans('consistency')}
             wrapperClass={classes.selector}
             wrapperClass={classes.selector}
+            disabled={!collection.loaded}
             variant="filled"
             variant="filled"
             onChange={(e: { target: { value: unknown } }) => {
             onChange={(e: { target: { value: unknown } }) => {
               const consistency = e.target.value as string;
               const consistency = e.target.value as string;
-              setConsistency_level(consistency);
+              setConsistencyLevel(consistency);
             }}
             }}
           />
           />
         </div>
         </div>
@@ -257,24 +342,32 @@ const Query = () => {
           <CustomButton
           <CustomButton
             className="btn"
             className="btn"
             onClick={handleFilterReset}
             onClick={handleFilterReset}
-            disabled={!expression}
+            disabled={!collection.loaded}
           >
           >
             <ResetIcon classes={{ root: 'icon' }} />
             <ResetIcon classes={{ root: 'icon' }} />
             {btnTrans('reset')}
             {btnTrans('reset')}
           </CustomButton>
           </CustomButton>
           <CustomButton
           <CustomButton
             variant="contained"
             variant="contained"
-            disabled={!expression}
-            onClick={() => handleQuery(expression)}
+            onClick={() => {
+              setCurrentPage(0);
+              if (expr !== expression) {
+                setExpr(expression);
+              } else {
+                // ensure query
+                query();
+              }
+            }}
+            disabled={!collection.loaded}
           >
           >
             {btnTrans('query')}
             {btnTrans('query')}
           </CustomButton>
           </CustomButton>
         </div>
         </div>
       </div>
       </div>
-      {tableLoading || queryResult?.length ? (
+      {tableLoading || queryResultMemo?.length ? (
         <AttuGrid
         <AttuGrid
           toolbarConfigs={[]}
           toolbarConfigs={[]}
-          colDefinitions={fields.map(i => ({
+          colDefinitions={collection.fields.map((i: any) => ({
             id: i.name,
             id: i.name,
             align: 'left',
             align: 'left',
             disablePadding: false,
             disablePadding: false,
@@ -282,32 +375,28 @@ const Query = () => {
             label:
             label:
               i.name === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i.name,
               i.name === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i.name,
           }))}
           }))}
-          primaryKey={primaryKey.value}
+          primaryKey={collection.primaryKey.value}
           openCheckBox={true}
           openCheckBox={true}
           isLoading={!!tableLoading}
           isLoading={!!tableLoading}
-          rows={result}
+          rows={queryResultMemo}
           rowCount={total}
           rowCount={total}
           selected={selectedData}
           selected={selectedData}
-          setSelected={handleSelectChange}
+          setSelected={onSelectChange}
           page={currentPage}
           page={currentPage}
           onPageChange={handlePageChange}
           onPageChange={handlePageChange}
+          setRowsPerPage={setPageSize}
           rowsPerPage={pageSize}
           rowsPerPage={pageSize}
-          setRowsPerPage={handlePageSize}
-          orderBy={orderBy}
-          order={order}
-          handleSort={handleGridSort}
-          labelDisplayedRows={getLabelDisplayedRows(`(${latency} ms)`)}
+          labelDisplayedRows={getLabelDisplayedRows(
+            `(${queryResult.latency || ''} ms)`
+          )}
         />
         />
       ) : (
       ) : (
         <EmptyCard
         <EmptyCard
           wrapperClass={`page-empty-card ${classes.emptyCard}`}
           wrapperClass={`page-empty-card ${classes.emptyCard}`}
           icon={<VectorSearchIcon />}
           icon={<VectorSearchIcon />}
-          text={
-            queryResult?.length === 0
-              ? searchTrans('empty')
-              : collectionTrans('startTip')
-          }
-          subText={collectionTrans('dataQuerylimits')}
+          text={searchTrans(
+            `${collection.loaded ? 'empty' : 'collectionNotLoaded'}`
+          )}
         />
         />
       )}
       )}
     </div>
     </div>

+ 12 - 5
server/src/collections/collections.controller.ts

@@ -203,14 +203,21 @@ export class CollectionController {
 
 
   async getCollectionInfo(req: Request, res: Response, next: NextFunction) {
   async getCollectionInfo(req: Request, res: Response, next: NextFunction) {
     const name = req.params?.name;
     const name = req.params?.name;
+    const params = {
+      collection_name: name,
+    };
     try {
     try {
       const result = await this.collectionsService.describeCollection(
       const result = await this.collectionsService.describeCollection(
         req.clientId,
         req.clientId,
-        {
-          collection_name: name,
-        }
+        params
       );
       );
-      res.send(result);
+
+      const loadState = await this.collectionsService.getLoadState(
+        req.clientId,
+        params
+      );
+
+      res.send({ ...result, state: loadState.state });
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
     }
     }
@@ -346,7 +353,7 @@ export class CollectionController {
     const resultPage: any = req.query?.page;
     const resultPage: any = req.query?.page;
 
 
     try {
     try {
-      const limit = isNaN(resultLimit) ? 100 : parseInt(resultLimit, 10);
+      const limit = parseInt(resultLimit, 10);
       const page = isNaN(resultPage) ? 0 : parseInt(resultPage, 10);
       const page = isNaN(resultPage) ? 0 : parseInt(resultPage, 10);
       // TODO: add page and limit to node SDK
       // TODO: add page and limit to node SDK
       // Here may raise "Error: 8 RESOURCE_EXHAUSTED: Received message larger than max"
       // Here may raise "Error: 8 RESOURCE_EXHAUSTED: Received message larger than max"

+ 31 - 23
server/src/collections/collections.service.ts

@@ -21,6 +21,7 @@ import {
   HasCollectionReq,
   HasCollectionReq,
   CountReq,
   CountReq,
   FieldSchema,
   FieldSchema,
+  GetLoadStateReq,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
 import { Parser } from '@json2csv/plainjs';
 import { Parser } from '@json2csv/plainjs';
 import {
 import {
@@ -43,49 +44,49 @@ export class CollectionsService {
   }
   }
 
 
   async getCollections(clientId: string, data?: ShowCollectionsReq) {
   async getCollections(clientId: string, data?: ShowCollectionsReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.showCollections(data);
     const res = await milvusClient.showCollections(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async createCollection(clientId: string, data: CreateCollectionReq) {
   async createCollection(clientId: string, data: CreateCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createCollection(data);
     const res = await milvusClient.createCollection(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async describeCollection(clientId: string, data: DescribeCollectionReq) {
   async describeCollection(clientId: string, data: DescribeCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.describeCollection(data);
     const res = await milvusClient.describeCollection(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async renameCollection(clientId: string, data: RenameCollectionReq) {
   async renameCollection(clientId: string, data: RenameCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.renameCollection(data);
     const res = await milvusClient.renameCollection(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   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);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async loadCollection(clientId: string, data: LoadCollectionReq) {
   async loadCollection(clientId: string, data: LoadCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollection(data);
     const res = await milvusClient.loadCollection(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.releaseCollection(data);
     const res = await milvusClient.releaseCollection(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
@@ -95,14 +96,21 @@ export class CollectionsService {
     clientId: string,
     clientId: string,
     data: GetCollectionStatisticsReq
     data: GetCollectionStatisticsReq
   ) {
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCollectionStatistics(data);
     const res = await milvusClient.getCollectionStatistics(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
+  async getLoadState(clientId: string, data: GetLoadStateReq) {
+    const { milvusClient } = clientCache.get(clientId);
+    const res = await milvusClient.getLoadState(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
   async count(clientId: string, data: CountReq) {
   async count(clientId: string, data: CountReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     let count = 0;
     let count = 0;
     try {
     try {
       const countRes = await milvusClient.count(data);
       const countRes = await milvusClient.count(data);
@@ -118,21 +126,21 @@ export class CollectionsService {
   }
   }
 
 
   async insert(clientId: string, data: InsertReq) {
   async insert(clientId: string, data: InsertReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.insert(data);
     const res = await milvusClient.insert(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     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);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async vectorSearch(clientId: string, data: SearchReq) {
   async vectorSearch(clientId: string, data: SearchReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const now = Date.now();
     const now = Date.now();
     const res = await milvusClient.search(data);
     const res = await milvusClient.search(data);
     const after = Date.now();
     const after = Date.now();
@@ -143,28 +151,28 @@ export class CollectionsService {
   }
   }
 
 
   async createAlias(clientId: string, data: CreateAliasReq) {
   async createAlias(clientId: string, data: CreateAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createAlias(data);
     const res = await milvusClient.createAlias(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async alterAlias(clientId: string, data: AlterAliasReq) {
   async alterAlias(clientId: string, data: AlterAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.alterAlias(data);
     const res = await milvusClient.alterAlias(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async dropAlias(clientId: string, data: DropAliasReq) {
   async dropAlias(clientId: string, data: DropAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropAlias(data);
     const res = await milvusClient.dropAlias(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async getReplicas(clientId: string, data: GetReplicasDto) {
   async getReplicas(clientId: string, data: GetReplicasDto) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getReplicas(data);
     const res = await milvusClient.getReplicas(data);
     return res;
     return res;
   }
   }
@@ -175,7 +183,7 @@ export class CollectionsService {
       collection_name: string;
       collection_name: string;
     } & QueryDto
     } & QueryDto
   ) {
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const now = Date.now();
     const now = Date.now();
     const res = await milvusClient.query(data);
     const res = await milvusClient.query(data);
 
 
@@ -370,14 +378,14 @@ export class CollectionsService {
   }
   }
 
 
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCompactionState(data);
     const res = await milvusClient.getCompactionState(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getQuerySegmentInfo(data);
     const res = await milvusClient.getQuerySegmentInfo(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
@@ -387,21 +395,21 @@ export class CollectionsService {
     clientId: string,
     clientId: string,
     data: GePersistentSegmentInfoReq
     data: GePersistentSegmentInfoReq
   ) {
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getPersistentSegmentInfo(data);
     const res = await milvusClient.getPersistentSegmentInfo(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async compact(clientId: string, data: CompactReq) {
   async compact(clientId: string, data: CompactReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.compact(data);
     const res = await milvusClient.compact(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async hasCollection(clientId: string, data: HasCollectionReq) {
   async hasCollection(clientId: string, data: HasCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.hasCollection(data);
     const res = await milvusClient.hasCollection(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
@@ -430,7 +438,7 @@ export class CollectionsService {
   }
   }
 
 
   async emptyCollection(clientId: string, data: HasCollectionReq) {
   async emptyCollection(clientId: string, data: HasCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const pkField = await milvusClient.getPkFieldName(data);
     const pkField = await milvusClient.getPkFieldName(data);
     const pkType = await milvusClient.getPkFieldType(data);
     const pkType = await milvusClient.getPkFieldType(data);