Query.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import { useState, useEffect, useRef, useContext } from 'react';
  2. import { TextField } from '@material-ui/core';
  3. import { useTranslation } from 'react-i18next';
  4. import { useParams } from 'react-router-dom';
  5. import { rootContext } from '@/context';
  6. import { DataService } from '@/http';
  7. import { useQuery, useSearchResult } from '@/hooks';
  8. import { saveCsvAs } from '@/utils';
  9. import EmptyCard from '@/components/cards/EmptyCard';
  10. import icons from '@/components/icons/Icons';
  11. import CustomButton from '@/components/customButton/CustomButton';
  12. import AttuGrid from '@/components/grid/Grid';
  13. import { ToolBarConfig } from '@/components/grid/Types';
  14. import Filter from '@/components/advancedSearch';
  15. import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
  16. import CustomToolBar from '@/components/grid/ToolBar';
  17. import InsertDialog from '../dialogs/insert/Dialog';
  18. import { getLabelDisplayedRows } from '../search/Utils';
  19. import { getQueryStyles } from './Styles';
  20. import {
  21. DYNAMIC_FIELD,
  22. DataTypeStringEnum,
  23. CONSISTENCY_LEVEL_OPTIONS,
  24. ConsistencyLevelEnum,
  25. } from '@/consts';
  26. import CustomSelector from '@/components/customSelector/CustomSelector';
  27. import EmptyDataDialog from '../dialogs/EmptyDataDialog';
  28. import ImportSampleDialog from '../dialogs/ImportSampleDialog';
  29. const Query = () => {
  30. // get collection name from url
  31. const { collectionName = '' } = useParams<{ collectionName: string }>();
  32. // UI state
  33. const [tableLoading, setTableLoading] = useState<boolean>();
  34. const [selectedData, setSelectedData] = useState<any[]>([]);
  35. const [expression, setExpression] = useState<string>('');
  36. // UI functions
  37. const { setDialog, handleCloseDialog, openSnackBar } =
  38. useContext(rootContext);
  39. // icons
  40. const VectorSearchIcon = icons.vectorSearch;
  41. const ResetIcon = icons.refresh;
  42. // translations
  43. const { t: dialogTrans } = useTranslation('dialog');
  44. const { t: successTrans } = useTranslation('success');
  45. const { t: searchTrans } = useTranslation('search');
  46. const { t: collectionTrans } = useTranslation('collection');
  47. const { t: btnTrans } = useTranslation('btn');
  48. // classes
  49. const classes = getQueryStyles();
  50. // UI ref
  51. const filterRef = useRef();
  52. const inputRef = useRef<HTMLInputElement>();
  53. // UI event handlers
  54. const handleFilterReset = async () => {
  55. // reset advanced filter
  56. const currentFilter: any = filterRef.current;
  57. currentFilter?.getReset();
  58. // update UI expression
  59. setExpression('');
  60. // reset query
  61. reset();
  62. // ensure not loading
  63. setTableLoading(false);
  64. };
  65. const handleFilterSubmit = async (expression: string) => {
  66. // update UI expression
  67. setExpression(expression);
  68. // update expression
  69. setExpr(expression);
  70. };
  71. const handlePageChange = async (e: any, page: number) => {
  72. // do the query
  73. await query(page);
  74. // update page number
  75. setCurrentPage(page);
  76. };
  77. const onSelectChange = (value: any) => {
  78. setSelectedData(value);
  79. };
  80. const onDelete = async () => {
  81. // reset query
  82. reset();
  83. count(ConsistencyLevelEnum.Strong);
  84. await query(0, ConsistencyLevelEnum.Strong);
  85. };
  86. const handleDelete = async () => {
  87. // call delete api
  88. await DataService.deleteEntities(collectionName, {
  89. expr: `${collection.primaryKey.value} in [${selectedData
  90. .map(v =>
  91. collection.primaryKey.type === DataTypeStringEnum.VarChar
  92. ? `"${v[collection.primaryKey.value]}"`
  93. : v[collection.primaryKey.value]
  94. )
  95. .join(',')}]`,
  96. });
  97. handleCloseDialog();
  98. openSnackBar(successTrans('delete', { name: collectionTrans('entities') }));
  99. setSelectedData([]);
  100. await onDelete();
  101. };
  102. // Query hook
  103. const {
  104. collection,
  105. currentPage,
  106. total,
  107. pageSize,
  108. expr,
  109. queryResult,
  110. setPageSize,
  111. setConsistencyLevel,
  112. setCurrentPage,
  113. setExpr,
  114. query,
  115. reset,
  116. count,
  117. } = useQuery({
  118. collectionName,
  119. onQueryStart: (expr: string = '') => {
  120. setTableLoading(true);
  121. if (expr === '') {
  122. handleFilterReset();
  123. return;
  124. }
  125. },
  126. onQueryFinally: () => {
  127. setTableLoading(false);
  128. },
  129. });
  130. // Format result list
  131. const queryResultMemo = useSearchResult(queryResult.data);
  132. // Toolbar settings
  133. const toolbarConfigs: ToolBarConfig[] = [
  134. {
  135. icon: 'uploadFile',
  136. type: 'button',
  137. btnVariant: 'text',
  138. btnColor: 'secondary',
  139. label: btnTrans('importFile'),
  140. tooltip: btnTrans('importFileTooltip'),
  141. onClick: () => {
  142. setDialog({
  143. open: true,
  144. type: 'custom',
  145. params: {
  146. component: (
  147. <InsertDialog
  148. defaultSelectedCollection={collectionName}
  149. // user can't select partition on collection page, so default value is ''
  150. defaultSelectedPartition={''}
  151. onInsert={() => {}}
  152. />
  153. ),
  154. },
  155. });
  156. },
  157. },
  158. {
  159. type: 'button',
  160. btnVariant: 'text',
  161. onClick: () => {
  162. setDialog({
  163. open: true,
  164. type: 'custom',
  165. params: {
  166. component: (
  167. <ImportSampleDialog collection={collectionName} cb={onDelete} />
  168. ),
  169. },
  170. });
  171. },
  172. tooltip: btnTrans('importSampleDataTooltip'),
  173. label: btnTrans('importSampleData'),
  174. icon: 'add',
  175. // tooltip: collectionTrans('deleteTooltip'),
  176. },
  177. {
  178. icon: 'deleteOutline',
  179. type: 'button',
  180. btnVariant: 'text',
  181. onClick: () => {
  182. setDialog({
  183. open: true,
  184. type: 'custom',
  185. params: {
  186. component: (
  187. <EmptyDataDialog
  188. cb={async () => {
  189. openSnackBar(
  190. successTrans('empty', {
  191. name: collectionTrans('collection'),
  192. })
  193. );
  194. await onDelete();
  195. }}
  196. collectionName={collectionName}
  197. />
  198. ),
  199. },
  200. });
  201. },
  202. disabled: () => total == 0,
  203. label: btnTrans('empty'),
  204. tooltip: btnTrans('emptyTooltip'),
  205. disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
  206. },
  207. {
  208. type: 'button',
  209. btnVariant: 'text',
  210. onClick: () => {
  211. saveCsvAs(selectedData, `${collectionName}.query.csv`);
  212. },
  213. label: btnTrans('export'),
  214. icon: 'download',
  215. tooltip: btnTrans('exportTooltip'),
  216. disabledTooltip: collectionTrans('downloadDisabledTooltip'),
  217. disabled: () => !selectedData?.length,
  218. },
  219. {
  220. type: 'button',
  221. btnVariant: 'text',
  222. onClick: async () => {
  223. const json = JSON.stringify(selectedData);
  224. try {
  225. await navigator.clipboard.writeText(json);
  226. alert(`${selectedData.length} rows copied to clipboard`);
  227. } catch (err) {
  228. console.error('Failed to copy text: ', err);
  229. }
  230. },
  231. label: btnTrans('copyJson'),
  232. icon: 'copy',
  233. tooltip: btnTrans('copyJsonTooltip'),
  234. disabledTooltip: collectionTrans('downloadDisabledTooltip'),
  235. disabled: () => !selectedData?.length,
  236. },
  237. {
  238. type: 'button',
  239. btnVariant: 'text',
  240. onClick: () => {
  241. setDialog({
  242. open: true,
  243. type: 'custom',
  244. params: {
  245. component: (
  246. <DeleteTemplate
  247. label={btnTrans('drop')}
  248. title={dialogTrans('deleteTitle', {
  249. type: collectionTrans('entities'),
  250. })}
  251. text={collectionTrans('deleteDataWarning')}
  252. handleDelete={handleDelete}
  253. />
  254. ),
  255. },
  256. });
  257. },
  258. label: btnTrans('delete'),
  259. icon: 'delete',
  260. tooltip: btnTrans('deleteTooltip'),
  261. disabledTooltip: collectionTrans('deleteTooltip'),
  262. disabled: () => selectedData.length === 0,
  263. },
  264. ];
  265. useEffect(() => {
  266. if (inputRef.current) {
  267. inputRef.current.focus();
  268. }
  269. }, []);
  270. return (
  271. <div className={classes.root}>
  272. <CustomToolBar toolbarConfigs={toolbarConfigs} hideOnDisable={true} />
  273. <div className={classes.toolbar}>
  274. <div className="left">
  275. <TextField
  276. className="textarea"
  277. InputProps={{
  278. classes: {
  279. root: 'textfield',
  280. multiline: 'multiline',
  281. },
  282. }}
  283. value={expression}
  284. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  285. setExpression(e.target.value as string);
  286. }}
  287. disabled={!collection.loaded}
  288. InputLabelProps={{ shrink: true }}
  289. label={collectionTrans('exprPlaceHolder')}
  290. onKeyDown={e => {
  291. if (e.key === 'Enter') {
  292. // reset page
  293. setCurrentPage(0);
  294. if (expr !== expression) {
  295. setExpr(expression);
  296. } else {
  297. // ensure query
  298. query();
  299. }
  300. e.preventDefault();
  301. }
  302. }}
  303. inputRef={inputRef}
  304. />
  305. <Filter
  306. ref={filterRef}
  307. title="Advanced Filter"
  308. fields={collection.fields.filter(
  309. (i: any) =>
  310. i.type !== DataTypeStringEnum.FloatVector &&
  311. i.type !== DataTypeStringEnum.BinaryVector
  312. )}
  313. filterDisabled={!collection.loaded}
  314. onSubmit={handleFilterSubmit}
  315. showTitle={false}
  316. showTooltip={false}
  317. />
  318. {/* </div> */}
  319. <CustomSelector
  320. options={CONSISTENCY_LEVEL_OPTIONS}
  321. value={collection.consistencyLevel}
  322. label={collectionTrans('consistency')}
  323. wrapperClass={classes.selector}
  324. disabled={!collection.loaded}
  325. variant="filled"
  326. onChange={(e: { target: { value: unknown } }) => {
  327. const consistency = e.target.value as string;
  328. setConsistencyLevel(consistency);
  329. }}
  330. />
  331. </div>
  332. <div className="right">
  333. <CustomButton
  334. className="btn"
  335. onClick={handleFilterReset}
  336. disabled={!collection.loaded}
  337. >
  338. <ResetIcon classes={{ root: 'icon' }} />
  339. {btnTrans('reset')}
  340. </CustomButton>
  341. <CustomButton
  342. variant="contained"
  343. onClick={() => {
  344. setCurrentPage(0);
  345. if (expr !== expression) {
  346. setExpr(expression);
  347. } else {
  348. // ensure query
  349. query();
  350. }
  351. }}
  352. disabled={!collection.loaded}
  353. >
  354. {btnTrans('query')}
  355. </CustomButton>
  356. </div>
  357. </div>
  358. {tableLoading || queryResultMemo?.length ? (
  359. <AttuGrid
  360. toolbarConfigs={[]}
  361. colDefinitions={collection.fields.map((i: any) => ({
  362. id: i.name,
  363. align: 'left',
  364. disablePadding: false,
  365. needCopy: true,
  366. label:
  367. i.name === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i.name,
  368. }))}
  369. primaryKey={collection.primaryKey.value}
  370. openCheckBox={true}
  371. isLoading={!!tableLoading}
  372. rows={queryResultMemo}
  373. rowCount={total}
  374. selected={selectedData}
  375. setSelected={onSelectChange}
  376. page={currentPage}
  377. onPageChange={handlePageChange}
  378. setRowsPerPage={setPageSize}
  379. rowsPerPage={pageSize}
  380. labelDisplayedRows={getLabelDisplayedRows(
  381. `(${queryResult.latency || ''} ms)`
  382. )}
  383. />
  384. ) : (
  385. <EmptyCard
  386. wrapperClass={`page-empty-card ${classes.emptyCard}`}
  387. icon={<VectorSearchIcon />}
  388. text={searchTrans(
  389. `${collection.loaded ? 'empty' : 'collectionNotLoaded'}`
  390. )}
  391. />
  392. )}
  393. </div>
  394. );
  395. };
  396. export default Query;