2
0

Query.tsx 13 KB

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