CollectionData.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import { useState, useEffect, useRef, useContext } from 'react';
  2. import { Typography } from '@material-ui/core';
  3. import { useTranslation } from 'react-i18next';
  4. import { rootContext, dataContext } from '@/context';
  5. import { DataService } from '@/http';
  6. import { useQuery } from '@/hooks';
  7. import { saveCsvAs, getColumnWidth } from '@/utils';
  8. import icons from '@/components/icons/Icons';
  9. import CustomButton from '@/components/customButton/CustomButton';
  10. import AttuGrid from '@/components/grid/Grid';
  11. import { ToolBarConfig } from '@/components/grid/Types';
  12. import Filter from '@/components/advancedSearch';
  13. import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
  14. import CustomToolBar from '@/components/grid/ToolBar';
  15. import InsertDialog from '@/pages/dialogs/insert/Dialog';
  16. import { getLabelDisplayedRows } from '@/pages/search/Utils';
  17. import { getQueryStyles } from './Styles';
  18. import {
  19. DYNAMIC_FIELD,
  20. DataTypeStringEnum,
  21. CONSISTENCY_LEVEL_OPTIONS,
  22. ConsistencyLevelEnum,
  23. } from '@/consts';
  24. import CustomSelector from '@/components/customSelector/CustomSelector';
  25. import EmptyDataDialog from '@/pages/dialogs/EmptyDataDialog';
  26. import ImportSampleDialog from '@/pages/dialogs/ImportSampleDialog';
  27. import { detectItemType } from '@/utils';
  28. import { CollectionObject, CollectionFullObject } from '@server/types';
  29. import StatusIcon, { LoadingType } from '@/components/status/StatusIcon';
  30. import CustomInput from '@/components/customInput/CustomInput';
  31. import CustomMultiSelector from '@/components/customSelector/CustomMultiSelector';
  32. export interface CollectionDataProps {
  33. collectionName: string;
  34. collections: CollectionObject[];
  35. }
  36. const CollectionData = (props: CollectionDataProps) => {
  37. // props
  38. const { collections } = props;
  39. const collection = collections.find(
  40. i => i.collection_name === props.collectionName
  41. ) as CollectionFullObject;
  42. // collection is not found or collection full object is not ready
  43. if (!collection || !collection.consistency_level) {
  44. return <StatusIcon type={LoadingType.CREATING} />;
  45. }
  46. // UI state
  47. const [tableLoading, setTableLoading] = useState<boolean>();
  48. const [selectedData, setSelectedData] = useState<any[]>([]);
  49. const [expression, setExpression] = useState<string>('');
  50. const [consistencyLevel, setConsistencyLevel] = useState<string>(
  51. collection.consistency_level
  52. );
  53. // collection fields, combine static and dynamic fields
  54. const fields = [
  55. ...collection.schema.fields,
  56. ...collection.schema.dynamicFields,
  57. ];
  58. // UI functions
  59. const { setDialog, handleCloseDialog, openSnackBar } =
  60. useContext(rootContext);
  61. const { fetchCollection } = useContext(dataContext);
  62. // icons
  63. const ResetIcon = icons.refresh;
  64. // translations
  65. const { t: dialogTrans } = useTranslation('dialog');
  66. const { t: successTrans } = useTranslation('success');
  67. const { t: searchTrans } = useTranslation('search');
  68. const { t: collectionTrans } = useTranslation('collection');
  69. const { t: btnTrans } = useTranslation('btn');
  70. const { t: commonTrans } = useTranslation();
  71. const gridTrans = commonTrans('grid');
  72. // classes
  73. const classes = getQueryStyles();
  74. // UI ref
  75. const filterRef = useRef();
  76. const inputRef = useRef<HTMLInputElement>();
  77. // UI event handlers
  78. const handleFilterReset = async () => {
  79. // reset advanced filter
  80. const currentFilter: any = filterRef.current;
  81. currentFilter?.getReset();
  82. // update UI expression
  83. setExpression('');
  84. // reset query
  85. reset();
  86. // ensure not loading
  87. setTableLoading(false);
  88. };
  89. const handleFilterSubmit = async (expression: string) => {
  90. // update UI expression
  91. setExpression(expression);
  92. // update expression
  93. setExpr(expression);
  94. };
  95. const handlePageChange = async (e: any, page: number) => {
  96. // do the query
  97. await query(page, consistencyLevel);
  98. // update page number
  99. setCurrentPage(page);
  100. };
  101. const onSelectChange = (value: any) => {
  102. setSelectedData(value);
  103. };
  104. const onDelete = async () => {
  105. // reset query
  106. reset();
  107. count(ConsistencyLevelEnum.Strong);
  108. await query(0, ConsistencyLevelEnum.Strong);
  109. };
  110. const handleDelete = async () => {
  111. // call delete api
  112. await DataService.deleteEntities(collection.collection_name, {
  113. expr: `${collection!.schema.primaryField.name} in [${selectedData
  114. .map(v =>
  115. collection!.schema.primaryField.data_type ===
  116. DataTypeStringEnum.VarChar
  117. ? `"${v[collection!.schema.primaryField.name]}"`
  118. : v[collection!.schema.primaryField.name]
  119. )
  120. .join(',')}]`,
  121. });
  122. handleCloseDialog();
  123. openSnackBar(successTrans('delete', { name: collectionTrans('entities') }));
  124. setSelectedData([]);
  125. await onDelete();
  126. };
  127. // Query hook
  128. const {
  129. currentPage,
  130. total,
  131. pageSize,
  132. expr,
  133. queryResult,
  134. setPageSize,
  135. setCurrentPage,
  136. setExpr,
  137. query,
  138. reset,
  139. count,
  140. outputFields,
  141. setOutputFields,
  142. } = useQuery({
  143. collection,
  144. consistencyLevel,
  145. fields,
  146. onQueryStart: (expr: string = '') => {
  147. setTableLoading(true);
  148. if (expr === '') {
  149. handleFilterReset();
  150. return;
  151. }
  152. },
  153. onQueryFinally: () => {
  154. setTableLoading(false);
  155. },
  156. });
  157. const onInsert = async (collectionName: string) => {
  158. await fetchCollection(collectionName);
  159. };
  160. // Toolbar settings
  161. const toolbarConfigs: ToolBarConfig[] = [
  162. {
  163. icon: 'uploadFile',
  164. type: 'button',
  165. btnVariant: 'text',
  166. btnColor: 'secondary',
  167. label: btnTrans('importFile'),
  168. tooltip: btnTrans('importFileTooltip'),
  169. onClick: () => {
  170. setDialog({
  171. open: true,
  172. type: 'custom',
  173. params: {
  174. component: (
  175. <InsertDialog
  176. defaultSelectedCollection={collection.collection_name}
  177. // user can't select partition on collection page, so default value is ''
  178. defaultSelectedPartition={''}
  179. collections={[collection!]}
  180. onInsert={onInsert}
  181. />
  182. ),
  183. },
  184. });
  185. },
  186. },
  187. {
  188. type: 'button',
  189. btnVariant: 'text',
  190. onClick: () => {
  191. setDialog({
  192. open: true,
  193. type: 'custom',
  194. params: {
  195. component: (
  196. <ImportSampleDialog
  197. collection={collection!}
  198. cb={async () => {
  199. await onInsert(collection.collection_name);
  200. await onDelete();
  201. }}
  202. />
  203. ),
  204. },
  205. });
  206. },
  207. tooltip: btnTrans('importSampleDataTooltip'),
  208. label: btnTrans('importSampleData'),
  209. icon: 'add',
  210. // tooltip: collectionTrans('deleteTooltip'),
  211. },
  212. {
  213. icon: 'deleteOutline',
  214. type: 'button',
  215. btnVariant: 'text',
  216. onClick: () => {
  217. setDialog({
  218. open: true,
  219. type: 'custom',
  220. params: {
  221. component: (
  222. <EmptyDataDialog
  223. cb={async () => {
  224. openSnackBar(
  225. successTrans('empty', {
  226. name: collectionTrans('collection'),
  227. })
  228. );
  229. await onDelete();
  230. }}
  231. collection={collection!}
  232. />
  233. ),
  234. },
  235. });
  236. },
  237. disabled: () => total == 0,
  238. label: btnTrans('empty'),
  239. tooltip: btnTrans('emptyTooltip'),
  240. },
  241. {
  242. type: 'button',
  243. btnVariant: 'text',
  244. onClick: () => {
  245. saveCsvAs(selectedData, `${collection.collection_name}.query.csv`);
  246. },
  247. label: btnTrans('export'),
  248. icon: 'download',
  249. tooltip: btnTrans('exportTooltip'),
  250. disabledTooltip: btnTrans('downloadDisabledTooltip'),
  251. disabled: () => !selectedData?.length,
  252. },
  253. {
  254. type: 'button',
  255. btnVariant: 'text',
  256. onClick: async () => {
  257. let json = JSON.stringify(selectedData);
  258. try {
  259. await navigator.clipboard.writeText(json);
  260. alert(`${selectedData.length} rows copied to clipboard`);
  261. } catch (err) {
  262. console.error('Failed to copy text: ', err);
  263. }
  264. },
  265. label: btnTrans('copyJson'),
  266. icon: 'copy',
  267. tooltip: btnTrans('copyJsonTooltip'),
  268. disabledTooltip: btnTrans('downloadDisabledTooltip'),
  269. disabled: () => !selectedData?.length,
  270. },
  271. {
  272. type: 'button',
  273. btnVariant: 'text',
  274. onClick: () => {
  275. setDialog({
  276. open: true,
  277. type: 'custom',
  278. params: {
  279. component: (
  280. <DeleteTemplate
  281. label={btnTrans('drop')}
  282. title={dialogTrans('deleteTitle', {
  283. type: collectionTrans('entities'),
  284. })}
  285. text={collectionTrans('deleteDataWarning')}
  286. handleDelete={handleDelete}
  287. />
  288. ),
  289. },
  290. });
  291. },
  292. label: btnTrans('delete'),
  293. icon: 'delete',
  294. tooltip: btnTrans('deleteTooltip'),
  295. disabledTooltip: collectionTrans('deleteDisabledTooltip'),
  296. disabled: () => selectedData.length === 0,
  297. },
  298. ];
  299. useEffect(() => {
  300. if (inputRef.current) {
  301. inputRef.current.focus();
  302. }
  303. }, []);
  304. return (
  305. <div className={classes.root}>
  306. {collection && (
  307. <>
  308. <CustomToolBar toolbarConfigs={toolbarConfigs} hideOnDisable={true} />
  309. <div className={classes.toolbar}>
  310. <div className="left">
  311. <CustomInput
  312. type="text"
  313. textConfig={{
  314. label: expression
  315. ? collectionTrans('queryExpression')
  316. : collectionTrans('exprPlaceHolder'),
  317. key: 'advFilter',
  318. className: 'textarea',
  319. onChange: (value: string) => {
  320. setExpression(value);
  321. },
  322. value: expression,
  323. disabled: !collection.loaded,
  324. variant: 'filled',
  325. required: false,
  326. InputLabelProps: { shrink: true },
  327. InputProps: {
  328. endAdornment: (
  329. <Filter
  330. title={''}
  331. showTitle={false}
  332. fields={collection.schema.scalarFields}
  333. filterDisabled={!collection.loaded}
  334. onSubmit={handleFilterSubmit}
  335. showTooltip={false}
  336. />
  337. ),
  338. },
  339. onKeyDown: (e: any) => {
  340. if (e.key === 'Enter') {
  341. // reset page
  342. setCurrentPage(0);
  343. if (expr !== expression) {
  344. setExpr(expression);
  345. } else {
  346. // ensure query
  347. query();
  348. }
  349. e.preventDefault();
  350. }
  351. },
  352. }}
  353. checkValid={() => true}
  354. />
  355. <CustomSelector
  356. options={CONSISTENCY_LEVEL_OPTIONS}
  357. value={consistencyLevel}
  358. label={collectionTrans('consistency')}
  359. wrapperClass={classes.selector}
  360. disabled={!collection.loaded}
  361. variant="filled"
  362. onChange={(e: { target: { value: unknown } }) => {
  363. const consistency = e.target.value as string;
  364. setConsistencyLevel(consistency);
  365. }}
  366. />
  367. </div>
  368. <div className="right">
  369. <CustomMultiSelector
  370. options={fields.map(f => {
  371. return {
  372. label:
  373. f.name === DYNAMIC_FIELD
  374. ? searchTrans('dynamicFields')
  375. : f.name,
  376. value: f.name,
  377. };
  378. })}
  379. values={outputFields}
  380. renderValue={selected => (
  381. <span>{`${(selected as string[]).length} ${
  382. gridTrans[
  383. (selected as string[]).length > 1 ? 'fields' : 'field'
  384. ]
  385. }`}</span>
  386. )}
  387. label={searchTrans('outputFields')}
  388. wrapperClass="selector"
  389. variant="filled"
  390. onChange={(e: { target: { value: unknown } }) => {
  391. // add value to output fields if not exist, remove if exist
  392. const newOutputFields = [...outputFields];
  393. const values = e.target.value as string[];
  394. const newFields = values.filter(
  395. v => !newOutputFields.includes(v as string)
  396. );
  397. const removeFields = newOutputFields.filter(
  398. v => !values.includes(v as string)
  399. );
  400. newOutputFields.push(...newFields);
  401. removeFields.forEach(f => {
  402. const index = newOutputFields.indexOf(f);
  403. newOutputFields.splice(index, 1);
  404. });
  405. // sort output fields by schema order
  406. newOutputFields.sort(
  407. (a, b) =>
  408. fields.findIndex(f => f.name === a) -
  409. fields.findIndex(f => f.name === b)
  410. );
  411. setOutputFields(newOutputFields);
  412. }}
  413. />
  414. <CustomButton
  415. className="btn"
  416. onClick={handleFilterReset}
  417. disabled={!collection.loaded}
  418. startIcon={<ResetIcon classes={{ root: 'icon' }} />}
  419. >
  420. {btnTrans('reset')}
  421. </CustomButton>
  422. <CustomButton
  423. variant="contained"
  424. onClick={() => {
  425. setCurrentPage(0);
  426. if (expr !== expression) {
  427. setExpr(expression);
  428. } else {
  429. // ensure query
  430. query();
  431. }
  432. }}
  433. disabled={!collection.loaded}
  434. >
  435. {btnTrans('query')}
  436. </CustomButton>
  437. </div>
  438. </div>
  439. <AttuGrid
  440. toolbarConfigs={[]}
  441. colDefinitions={outputFields.map(i => {
  442. return {
  443. id: i,
  444. align: 'left',
  445. disablePadding: false,
  446. needCopy: true,
  447. formatter(_: any, cellData: any) {
  448. const itemType = detectItemType(cellData);
  449. switch (itemType) {
  450. case 'json':
  451. case 'array':
  452. case 'bool':
  453. const res = JSON.stringify(cellData);
  454. return <Typography title={res}>{res}</Typography>;
  455. default:
  456. return cellData;
  457. }
  458. },
  459. field: i,
  460. getStyle: d => {
  461. if (!d || !d.field) {
  462. return {};
  463. }
  464. return {
  465. minWidth: getColumnWidth(d.field),
  466. };
  467. },
  468. label: i === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i,
  469. };
  470. })}
  471. primaryKey={collection.schema.primaryField.name}
  472. openCheckBox={true}
  473. isLoading={tableLoading}
  474. rows={queryResult.data}
  475. rowCount={total}
  476. tableHeaderHeight={46}
  477. rowHeight={43}
  478. selected={selectedData}
  479. setSelected={onSelectChange}
  480. page={currentPage}
  481. onPageChange={handlePageChange}
  482. setRowsPerPage={setPageSize}
  483. rowsPerPage={pageSize}
  484. labelDisplayedRows={getLabelDisplayedRows(
  485. gridTrans[queryResult.data.length > 1 ? 'entities' : 'entity'],
  486. `(${queryResult.latency || ''} ms)`
  487. )}
  488. noData={searchTrans(
  489. `${collection.loaded ? 'empty' : 'collectionNotLoaded'}`
  490. )}
  491. />
  492. </>
  493. )}
  494. </div>
  495. );
  496. };
  497. export default CollectionData;