Query.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import { useState, useEffect, useRef, useContext } from 'react';
  2. import { TextField, Typography } 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 } from '@/hooks';
  8. import { saveCsvAs } from '@/utils';
  9. import icons from '@/components/icons/Icons';
  10. import CustomButton from '@/components/customButton/CustomButton';
  11. import AttuGrid from '@/components/grid/Grid';
  12. import { ToolBarConfig } from '@/components/grid/Types';
  13. import Filter from '@/components/advancedSearch';
  14. import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
  15. import CustomToolBar from '@/components/grid/ToolBar';
  16. import InsertDialog from '../dialogs/insert/Dialog';
  17. import { getLabelDisplayedRows } from '../search/Utils';
  18. import { getQueryStyles } from './Styles';
  19. import {
  20. DYNAMIC_FIELD,
  21. DataTypeStringEnum,
  22. CONSISTENCY_LEVEL_OPTIONS,
  23. ConsistencyLevelEnum,
  24. } from '@/consts';
  25. import CustomSelector from '@/components/customSelector/CustomSelector';
  26. import EmptyDataDialog from '../dialogs/EmptyDataDialog';
  27. import ImportSampleDialog from '../dialogs/ImportSampleDialog';
  28. import { detectItemType } from '@/utils';
  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 ResetIcon = icons.refresh;
  41. // translations
  42. const { t: dialogTrans } = useTranslation('dialog');
  43. const { t: successTrans } = useTranslation('success');
  44. const { t: searchTrans } = useTranslation('search');
  45. const { t: collectionTrans } = useTranslation('collection');
  46. const { t: btnTrans } = useTranslation('btn');
  47. // classes
  48. const classes = getQueryStyles();
  49. // UI ref
  50. const filterRef = useRef();
  51. const inputRef = useRef<HTMLInputElement>();
  52. // UI event handlers
  53. const handleFilterReset = async () => {
  54. // reset advanced filter
  55. const currentFilter: any = filterRef.current;
  56. currentFilter?.getReset();
  57. // update UI expression
  58. setExpression('');
  59. // reset query
  60. reset();
  61. // ensure not loading
  62. setTableLoading(false);
  63. };
  64. const handleFilterSubmit = async (expression: string) => {
  65. // update UI expression
  66. setExpression(expression);
  67. // update expression
  68. setExpr(expression);
  69. };
  70. const handlePageChange = async (e: any, page: number) => {
  71. // do the query
  72. await query(page);
  73. // update page number
  74. setCurrentPage(page);
  75. };
  76. const onSelectChange = (value: any) => {
  77. setSelectedData(value);
  78. };
  79. const onDelete = async () => {
  80. // reset query
  81. reset();
  82. count(ConsistencyLevelEnum.Strong);
  83. await query(0, ConsistencyLevelEnum.Strong);
  84. };
  85. const handleDelete = async () => {
  86. // call delete api
  87. await DataService.deleteEntities(collectionName, {
  88. expr: `${collection!.schema.primaryField.name} in [${selectedData
  89. .map(v =>
  90. collection!.schema.primaryField.data_type ===
  91. DataTypeStringEnum.VarChar
  92. ? `"${v[collection!.schema.primaryField.name]}"`
  93. : v[collection!.schema.primaryField.name]
  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. fields,
  106. currentPage,
  107. total,
  108. pageSize,
  109. expr,
  110. queryResult,
  111. setPageSize,
  112. consistencyLevel,
  113. setConsistencyLevel,
  114. setCurrentPage,
  115. setExpr,
  116. query,
  117. reset,
  118. count,
  119. } = useQuery({
  120. collectionName,
  121. onQueryStart: (expr: string = '') => {
  122. setTableLoading(true);
  123. if (expr === '') {
  124. handleFilterReset();
  125. return;
  126. }
  127. },
  128. onQueryFinally: () => {
  129. setTableLoading(false);
  130. },
  131. });
  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. collections={[collection!]}
  152. onInsert={() => {}}
  153. />
  154. ),
  155. },
  156. });
  157. },
  158. },
  159. {
  160. type: 'button',
  161. btnVariant: 'text',
  162. onClick: () => {
  163. setDialog({
  164. open: true,
  165. type: 'custom',
  166. params: {
  167. component: (
  168. <ImportSampleDialog collection={collectionName} cb={onDelete} />
  169. ),
  170. },
  171. });
  172. },
  173. tooltip: btnTrans('importSampleDataTooltip'),
  174. label: btnTrans('importSampleData'),
  175. icon: 'add',
  176. // tooltip: collectionTrans('deleteTooltip'),
  177. },
  178. {
  179. icon: 'deleteOutline',
  180. type: 'button',
  181. btnVariant: 'text',
  182. onClick: () => {
  183. setDialog({
  184. open: true,
  185. type: 'custom',
  186. params: {
  187. component: (
  188. <EmptyDataDialog
  189. cb={async () => {
  190. openSnackBar(
  191. successTrans('empty', {
  192. name: collectionTrans('collection'),
  193. })
  194. );
  195. await onDelete();
  196. }}
  197. collectionName={collectionName}
  198. />
  199. ),
  200. },
  201. });
  202. },
  203. disabled: () => total == 0,
  204. label: btnTrans('empty'),
  205. tooltip: btnTrans('emptyTooltip'),
  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: btnTrans('downloadDisabledTooltip'),
  217. disabled: () => !selectedData?.length,
  218. },
  219. {
  220. type: 'button',
  221. btnVariant: 'text',
  222. onClick: async () => {
  223. let 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: btnTrans('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('deleteDisabledTooltip'),
  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. {collection && (
  273. <>
  274. <CustomToolBar toolbarConfigs={toolbarConfigs} hideOnDisable={true} />
  275. <div className={classes.toolbar}>
  276. <div className="left">
  277. <TextField
  278. className="textarea"
  279. InputProps={{
  280. classes: {
  281. root: 'textfield',
  282. multiline: 'multiline',
  283. },
  284. }}
  285. value={expression}
  286. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  287. setExpression(e.target.value as string);
  288. }}
  289. disabled={!collection.loaded}
  290. InputLabelProps={{ shrink: true }}
  291. label={collectionTrans('exprPlaceHolder')}
  292. onKeyDown={e => {
  293. if (e.key === 'Enter') {
  294. // reset page
  295. setCurrentPage(0);
  296. if (expr !== expression) {
  297. setExpr(expression);
  298. } else {
  299. // ensure query
  300. query();
  301. }
  302. e.preventDefault();
  303. }
  304. }}
  305. inputRef={inputRef}
  306. />
  307. <Filter
  308. ref={filterRef}
  309. title="Advanced Filter"
  310. fields={collection.schema.fields.filter(
  311. i =>
  312. i.data_type !== DataTypeStringEnum.FloatVector &&
  313. i.data_type !== DataTypeStringEnum.BinaryVector
  314. )}
  315. filterDisabled={!collection.loaded}
  316. onSubmit={handleFilterSubmit}
  317. showTitle={false}
  318. showTooltip={false}
  319. />
  320. {/* </div> */}
  321. <CustomSelector
  322. options={CONSISTENCY_LEVEL_OPTIONS}
  323. value={consistencyLevel}
  324. label={collectionTrans('consistency')}
  325. wrapperClass={classes.selector}
  326. disabled={!collection.loaded}
  327. variant="filled"
  328. onChange={(e: { target: { value: unknown } }) => {
  329. const consistency = e.target.value as string;
  330. setConsistencyLevel(consistency);
  331. }}
  332. />
  333. </div>
  334. <div className="right">
  335. <CustomButton
  336. className="btn"
  337. onClick={handleFilterReset}
  338. disabled={!collection.loaded}
  339. >
  340. <ResetIcon classes={{ root: 'icon' }} />
  341. {btnTrans('reset')}
  342. </CustomButton>
  343. <CustomButton
  344. variant="contained"
  345. onClick={() => {
  346. setCurrentPage(0);
  347. if (expr !== expression) {
  348. setExpr(expression);
  349. } else {
  350. // ensure query
  351. query();
  352. }
  353. }}
  354. disabled={!collection.loaded}
  355. >
  356. {btnTrans('query')}
  357. </CustomButton>
  358. </div>
  359. </div>
  360. <AttuGrid
  361. toolbarConfigs={[]}
  362. colDefinitions={fields.map(i => {
  363. return {
  364. id: i.name,
  365. align: 'left',
  366. disablePadding: false,
  367. needCopy: true,
  368. formatter(_: any, cellData: any) {
  369. const itemType = detectItemType(cellData);
  370. switch (itemType) {
  371. case 'json':
  372. case 'array':
  373. case 'bool':
  374. const res = JSON.stringify(cellData);
  375. return <Typography title={res}>{res}</Typography>;
  376. default:
  377. return cellData;
  378. }
  379. },
  380. label:
  381. i.name === DYNAMIC_FIELD
  382. ? searchTrans('dynamicFields')
  383. : i.name,
  384. };
  385. })}
  386. primaryKey={collection.schema.primaryField.name}
  387. openCheckBox={true}
  388. isLoading={tableLoading}
  389. rows={queryResult.data}
  390. rowCount={total}
  391. rowHeight={43}
  392. selected={selectedData}
  393. setSelected={onSelectChange}
  394. page={currentPage}
  395. onPageChange={handlePageChange}
  396. setRowsPerPage={setPageSize}
  397. rowsPerPage={pageSize}
  398. labelDisplayedRows={getLabelDisplayedRows(
  399. `(${queryResult.latency || ''} ms)`
  400. )}
  401. noData={searchTrans(
  402. `${collection.loaded ? 'empty' : 'collectionNotLoaded'}`
  403. )}
  404. />
  405. </>
  406. )}
  407. </div>
  408. );
  409. };
  410. export default Query;