Browse Source

Merge pull request #200 from zilliztech/fixQuery

Allow user to type query expression on query page
ryjiang 2 years ago
parent
commit
af3607e93c

+ 2 - 2
client/package.json

@@ -7,7 +7,7 @@
   "private": true,
   "dependencies": {
     "@date-io/dayjs": "1.x",
-    "@loadable/component": "^5.15.0",
+    "@json2csv/plainjs": "^7.0.1",
     "@material-ui/core": "4.11.4",
     "@material-ui/icons": "^4.11.3",
     "@material-ui/lab": "4.0.0-alpha.58",
@@ -26,7 +26,6 @@
     "react-router-dom": "^6.4.3",
     "react-syntax-highlighter": "^15.4.4",
     "socket.io-client": "^4.1.3",
-    "typescript": "^4.1.2",
     "vite": "^3.2.2",
     "vite-plugin-svgr": "^0.3.0",
     "web-vitals": "^1.0.1"
@@ -52,6 +51,7 @@
     "@vitest/coverage-c8": "^0.25.0",
     "jsdom": "^20.0.2",
     "prettier": "2.3.2",
+    "typescript": "^4.1.2",
     "vitest": "^0.24.5"
   },
   "homepage": "./",

+ 8 - 8
client/src/consts/Util.ts

@@ -6,14 +6,6 @@ export const BYTE_UNITS: { [x: string]: number } = {
 };
 
 export const LOGICAL_OPERATORS = [
-  {
-    value: '<',
-    label: '<',
-  },
-  {
-    value: '<=',
-    label: '<=',
-  },
   {
     value: '>',
     label: '>',
@@ -30,6 +22,14 @@ export const LOGICAL_OPERATORS = [
     value: '!=',
     label: '!=',
   },
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
   {
     value: 'in',
     label: 'in',

+ 4 - 4
client/src/i18n/en/collection.ts

@@ -13,7 +13,8 @@ const collectionTrans = {
   alias: 'Alias',
   aliasTooltip: 'Please select one collection to create alias',
   download: 'Download',
-  downloadTooltip: 'Download all query results',
+  downloadTooltip: 'Export all query results to CSV file',
+  downloadDisabledTooltip: 'Please query data before exporting',
 
   collection: 'Collection',
   entites: 'entites',
@@ -84,15 +85,14 @@ const collectionTrans = {
   queryTab: 'Data Query',
   previewTab: 'Data Preview',
   startTip: 'Start your data query',
-  exprPlaceHolder: 'Please enter your query by using advanced filter ->',
+  exprPlaceHolder: 'Please enter your data query, for example id > 0',
 
   // alias dialog
   aliasCreatePlaceholder: 'Alias name',
 
   // rename dialog
   newColNamePlaceholder: 'New Collection Name',
-  newNameInfo:
-    'Only numbers, letters, and underscores are allowed.',
+  newNameInfo: 'Only numbers, letters, and underscores are allowed.',
 };
 
 export default collectionTrans;

+ 50 - 30
client/src/pages/query/Query.tsx

@@ -1,8 +1,8 @@
 import { FC, useEffect, useState, useRef, useMemo, useContext } from 'react';
+import { TextField } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
-
+import { Parser } from '@json2csv/plainjs';
 import { rootContext } from '../../context/Root';
-
 import EmptyCard from '../../components/cards/EmptyCard';
 import icons from '../../components/icons/Icons';
 import CustomButton from '../../components/customButton/CustomButton';
@@ -14,13 +14,11 @@ import { CollectionHttp } from '../../http/Collection';
 import { FieldHttp } from '../../http/Field';
 import { usePaginationHook } from '../../hooks/Pagination';
 // import { useTimeTravelHook } from '../../hooks/TimeTravel';
-
 import CopyButton from '../../components/advancedSearch/CopyButton';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import CustomToolBar from '../../components/grid/ToolBar';
 // import { CustomDatePicker } from '../../components/customDatePicker/CustomDatePicker';
 import { saveAs } from 'file-saver';
-import { generateCsvData } from '../../utils/Format';
 import { DataTypeStringEnum } from '../collections/Types';
 
 const Query: FC<{
@@ -32,7 +30,6 @@ const Query: FC<{
   const [queryResult, setQueryResult] = useState<any>();
   const [selectedData, setSelectedData] = useState<any[]>([]);
   const [primaryKey, setPrimaryKey] = useState<string>('');
-
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
   const VectorSearchIcon = icons.vectorSearch;
@@ -48,9 +45,6 @@ const Query: FC<{
 
   const classes = getQueryStyles();
 
-  // const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
-  //   useTimeTravelHook();
-
   // Format result list
   const queryResultMemo = useMemo(
     () =>
@@ -88,14 +82,6 @@ const Query: FC<{
     [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
   );
 
-  const csvDataMemo = useMemo(() => {
-    const headers: string[] = fields?.map(i => i.name);
-    if (headers?.length && queryResult?.length) {
-      return generateCsvData(headers, queryResult);
-    }
-    return '';
-  }, [fields, queryResult]);
-
   const {
     pageSize,
     handlePageSize,
@@ -142,14 +128,18 @@ const Query: FC<{
 
   const handleFilterSubmit = (expression: string) => {
     setExpression(expression);
-    setQueryResult(null);
+    handleQuery(expression);
   };
 
-  const handleQuery = async () => {
+  const handleQuery = async (expr: string = '') => {
     setTableLoading(true);
+    if (expr === '') {
+      handleFilterReset();
+      return;
+    }
     try {
       const res = await CollectionHttp.queryData(collectionName, {
-        expr: expression,
+        expr: expr,
         output_fields: fields.map(i => i.name),
         // travel_timestamp: timeTravelInfo.timestamp,
       });
@@ -207,15 +197,22 @@ const Query: FC<{
     {
       type: 'iconBtn',
       onClick: () => {
-        const csvData = new Blob([csvDataMemo.toString()], {
-          type: 'text/csv;charset=utf-8',
-        });
-        saveAs(csvData, 'query_result.csv');
+        try {
+          const opts = {};
+          const parser = new Parser(opts);
+          const csv = parser.parse(queryResult);
+          const csvData = new Blob([csv], {
+            type: 'text/csv;charset=utf-8',
+          });
+          saveAs(csvData, 'milvus_query_result.csv');
+        } catch (err) {
+          console.error(err);
+        }
       },
-      label: collectionTrans('delete'),
+      label: '',
       icon: 'download',
-      tooltip: collectionTrans('download'),
-      disabledTooltip: collectionTrans('downloadTooltip'),
+      tooltip: collectionTrans('downloadTooltip'),
+      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
       disabled: () => !queryResult?.length,
     },
   ];
@@ -225,8 +222,27 @@ const Query: FC<{
       <CustomToolBar toolbarConfigs={toolbarConfigs} />
       <div className={classes.toolbar}>
         <div className="left">
-          {/* <div className="expression"> */}
-          <div>{`${expression || collectionTrans('exprPlaceHolder')}`}</div>
+          <TextField
+            className="textarea"
+            InputProps={{
+              classes: {
+                root: 'textfield',
+                multiline: 'multiline',
+              },
+            }}
+            placeholder={collectionTrans('exprPlaceHolder')}
+            value={expression}
+            onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+              setExpression(e.target.value as string);
+            }}
+            onKeyDown={e => {
+              if (e.key === 'Enter') {
+                // Do code here
+                handleQuery(expression);
+                e.preventDefault();
+              }
+            }}
+          />
           <Filter
             ref={filterRef}
             title="Advanced Filter"
@@ -250,14 +266,18 @@ const Query: FC<{
           /> */}
         </div>
         <div className="right">
-          <CustomButton className="btn" onClick={handleFilterReset}>
+          <CustomButton
+            className="btn"
+            onClick={handleFilterReset}
+            disabled={!expression}
+          >
             <ResetIcon classes={{ root: 'icon' }} />
             {btnTrans('reset')}
           </CustomButton>
           <CustomButton
             variant="contained"
             disabled={!expression}
-            onClick={() => handleQuery()}
+            onClick={() => handleQuery(expression)}
           >
             {btnTrans('query')}
           </CustomButton>

+ 11 - 1
client/src/pages/query/Styles.ts

@@ -37,6 +37,16 @@ export const getQueryStyles = makeStyles((theme: Theme) => ({
         padding: theme.spacing(0, 1.5),
         backgroundColor: '#F9F9F9',
       },
+      '& .textarea': {
+        width: '100%',
+        '& .MuiInput-underline:before': {
+          borderWidth: 1,
+          borderColor: '#F9F9F9',
+        },
+        '& .MuiInput-underline:after': {
+          borderWidth: 1,
+        },
+      },
     },
 
     '& .right': {
@@ -61,6 +71,6 @@ export const getQueryStyles = makeStyles((theme: Theme) => ({
     width: '16px',
     height: '16px',
     position: 'relative',
-    top: '-3px'
+    top: '-3px',
   },
 }));

+ 0 - 29
client/src/utils/Format.ts

@@ -225,32 +225,3 @@ export const formatUtcToMilvus = (bigNumber: number) => {
   const milvusTimeStamp = BigInt(bigNumber) << BigInt(18);
   return milvusTimeStamp.toString();
 };
-
-/**
- * Convert headers and rows to csv string.
- * @param headers csv headers: string[]
- * @param rows csv data rows: {[key in headers]: any}[]
- * @returns csv string
- */
-export const generateCsvData = (headers: string[], rows: any[]) => {
-  const rowsData = rows.reduce((prev, item: any[]) => {
-    headers.forEach((colName: any, idx: number) => {
-      const val = item[colName];
-      if (typeof val === 'string') {
-        prev += val;
-      } else if (typeof val === 'object') {
-        // Use double quote to ensure csv display correctly
-        prev += `"${JSON.stringify(val)}"`;
-      } else {
-        prev += `${val}`;
-      }
-      if (idx === headers.length - 1) {
-        prev += '\n';
-      } else {
-        prev += ',';
-      }
-    });
-    return prev;
-  }, '');
-  return headers.join(',') + '\n' + rowsData;
-};

+ 25 - 10
client/yarn.lock

@@ -378,7 +378,7 @@
     "@babel/plugin-syntax-jsx" "^7.18.6"
     "@babel/types" "^7.19.0"
 
-"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7":
   version "7.16.0"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.0.tgz#e27b977f2e2088ba24748bf99b5e1dece64e4f0b"
   integrity sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==
@@ -568,14 +568,19 @@
     "@jridgewell/resolve-uri" "3.1.0"
     "@jridgewell/sourcemap-codec" "1.4.14"
 
-"@loadable/component@^5.15.0":
-  version "5.15.0"
-  resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.15.0.tgz#48b9524237be553f48b158f8c9152593f3f3fded"
-  integrity sha512-g63rQzypPOZi0BeGsK4ST2MYhsFR+i7bhL8k/McUoWDNMDuTTdUlQ2GACKxqh5sI/dNC/6nVoPrycMnSylnAgQ==
+"@json2csv/formatters@^7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@json2csv/formatters/-/formatters-7.0.1.tgz#c025f0795f9bbab480de77e2248ab593987296b9"
+  integrity sha512-eCmYKIIoFDXUB0Fotet2RmcoFTtNLXLmSV7j6aEQH/D2GiO749Uan3ts03PtAhXpE11QghxBjS0toXom8VQNBw==
+
+"@json2csv/plainjs@^7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@json2csv/plainjs/-/plainjs-7.0.1.tgz#361d849f04a2a5013c7880738f08b6bc193c24eb"
+  integrity sha512-UAdaZwahrUeYhMYYilJwDsRfE7wDRsmGMsszYH67j8FLD5gZitqG38RXpUgHEH0s6YjsY8iKYWeEQ19WILncFA==
   dependencies:
-    "@babel/runtime" "^7.7.7"
-    hoist-non-react-statics "^3.3.1"
-    react-is "^16.12.0"
+    "@json2csv/formatters" "^7.0.1"
+    "@streamparser/json" "^0.0.15"
+    lodash.get "^4.4.2"
 
 "@material-ui/core@4.11.4":
   version "4.11.4"
@@ -715,6 +720,11 @@
   resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz#8863915676f837d9dad7b76f50cb500c1e9422e9"
   integrity sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==
 
+"@streamparser/json@^0.0.15":
+  version "0.0.15"
+  resolved "https://registry.yarnpkg.com/@streamparser/json/-/json-0.0.15.tgz#405fbe94877ce0cbd3cf650b4d9186a0ec6acd0a"
+  integrity sha512-6oikjkMTYAHGqKmcC9leE4+kY4Ch4eiTImXUN/N4d2bNGBYs0LJ/tfxmpvF5eExSU7iiPlV9jYlADqvj3NWA3Q==
+
 "@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
   version "5.4.0"
   resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906"
@@ -2527,7 +2537,7 @@ highlight.js@^10.4.1, highlight.js@~10.7.0:
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
   integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
 
-hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -3011,6 +3021,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
+
 lodash@^4.17.15:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -3391,7 +3406,7 @@ react-i18next@^12.0.0:
     "@babel/runtime" "^7.14.5"
     html-parse-stringify "^3.0.1"
 
-react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==