Query.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { FC, useEffect, useState, useRef, useMemo, useContext } from 'react';
  2. import { TextField } from '@material-ui/core';
  3. import { useTranslation } from 'react-i18next';
  4. import { Parser } from '@json2csv/plainjs';
  5. import { rootContext } from '../../context/Root';
  6. import EmptyCard from '../../components/cards/EmptyCard';
  7. import icons from '../../components/icons/Icons';
  8. import CustomButton from '../../components/customButton/CustomButton';
  9. import AttuGrid from '../../components/grid/Grid';
  10. import { ToolBarConfig } from '../../components/grid/Types';
  11. import { getQueryStyles } from './Styles';
  12. import Filter from '../../components/advancedSearch';
  13. import { CollectionHttp } from '../../http/Collection';
  14. import { FieldHttp } from '../../http/Field';
  15. import { usePaginationHook } from '../../hooks/Pagination';
  16. // import { useTimeTravelHook } from '../../hooks/TimeTravel';
  17. import CopyButton from '../../components/advancedSearch/CopyButton';
  18. import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
  19. import CustomToolBar from '../../components/grid/ToolBar';
  20. // import { CustomDatePicker } from '../../components/customDatePicker/CustomDatePicker';
  21. import { saveAs } from 'file-saver';
  22. import { DataTypeStringEnum } from '../collections/Types';
  23. const Query: FC<{
  24. collectionName: string;
  25. }> = ({ collectionName }) => {
  26. const [fields, setFields] = useState<any[]>([]);
  27. const [expression, setExpression] = useState('');
  28. const [tableLoading, setTableLoading] = useState<any>();
  29. const [queryResult, setQueryResult] = useState<any>();
  30. const [selectedData, setSelectedData] = useState<any[]>([]);
  31. const [primaryKey, setPrimaryKey] = useState<{ value: string; type: string }>(
  32. { value: '', type: DataTypeStringEnum.Int64 }
  33. );
  34. const { setDialog, handleCloseDialog, openSnackBar } =
  35. useContext(rootContext);
  36. const VectorSearchIcon = icons.vectorSearch;
  37. const ResetIcon = icons.refresh;
  38. const { t: dialogTrans } = useTranslation('dialog');
  39. const { t: successTrans } = useTranslation('success');
  40. const { t: searchTrans } = useTranslation('search');
  41. const { t: collectionTrans } = useTranslation('collection');
  42. const { t: btnTrans } = useTranslation('btn');
  43. const { t: commonTrans } = useTranslation();
  44. const copyTrans = commonTrans('copy');
  45. const classes = getQueryStyles();
  46. // Format result list
  47. const queryResultMemo = useMemo(
  48. () =>
  49. queryResult?.map((resultItem: { [key: string]: any }) => {
  50. // Iterate resultItem keys, then format vector(array) items.
  51. const tmp = Object.keys(resultItem).reduce(
  52. (prev: { [key: string]: any }, item: string) => {
  53. switch (item) {
  54. case 'json':
  55. prev[item] = <div>{JSON.stringify(resultItem[item])}</div>;
  56. break;
  57. case 'vector':
  58. const list2Str = JSON.stringify(resultItem[item]);
  59. prev[item] = (
  60. <div className={classes.vectorTableCell}>
  61. <div>{list2Str}</div>
  62. <CopyButton
  63. label={copyTrans.label}
  64. value={list2Str}
  65. className={classes.copyBtn}
  66. />
  67. </div>
  68. );
  69. break;
  70. default:
  71. prev[item] = `${resultItem[item]}`;
  72. }
  73. return prev;
  74. },
  75. {}
  76. );
  77. return tmp;
  78. }),
  79. [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
  80. );
  81. const {
  82. pageSize,
  83. handlePageSize,
  84. currentPage,
  85. handleCurrentPage,
  86. total,
  87. data: result,
  88. order,
  89. orderBy,
  90. handleGridSort,
  91. } = usePaginationHook(queryResultMemo || []);
  92. const handlePageChange = (e: any, page: number) => {
  93. handleCurrentPage(page);
  94. };
  95. const getFields = async (collectionName: string) => {
  96. const schemaList = await FieldHttp.getFields(collectionName);
  97. const nameList = schemaList.map(v => ({
  98. name: v.name,
  99. type: v.data_type,
  100. }));
  101. const primaryKey = schemaList.find(v => v._isPrimaryKey === true)!;
  102. setPrimaryKey({ value: primaryKey['name'], type: primaryKey['data_type'] });
  103. setFields(nameList);
  104. };
  105. // Get fields at first or collection name changed.
  106. useEffect(() => {
  107. collectionName && getFields(collectionName);
  108. }, [collectionName]);
  109. const filterRef = useRef();
  110. const handleFilterReset = () => {
  111. const currentFilter: any = filterRef.current;
  112. currentFilter?.getReset();
  113. setExpression('');
  114. setTableLoading(null);
  115. setQueryResult(null);
  116. handleCurrentPage(0);
  117. };
  118. const handleFilterSubmit = (expression: string) => {
  119. setExpression(expression);
  120. handleQuery(expression);
  121. };
  122. const handleQuery = async (expr: string = '') => {
  123. setTableLoading(true);
  124. if (expr === '') {
  125. handleFilterReset();
  126. return;
  127. }
  128. try {
  129. const res = await CollectionHttp.queryData(collectionName, {
  130. expr: expr,
  131. output_fields: fields.map(i => i.name),
  132. offset: 0,
  133. limit: 16384,
  134. // travel_timestamp: timeTravelInfo.timestamp,
  135. });
  136. const result = res.data;
  137. setQueryResult(result);
  138. } catch (err) {
  139. setQueryResult([]);
  140. } finally {
  141. setTableLoading(false);
  142. }
  143. };
  144. const handleSelectChange = (value: any) => {
  145. setSelectedData(value);
  146. };
  147. const handleDelete = async () => {
  148. await CollectionHttp.deleteEntities(collectionName, {
  149. expr: `${primaryKey.value} in [${selectedData
  150. .map(v =>
  151. primaryKey.type === DataTypeStringEnum.VarChar
  152. ? `"${v[primaryKey.value]}"`
  153. : v[primaryKey.value]
  154. )
  155. .join(',')}]`,
  156. });
  157. handleCloseDialog();
  158. openSnackBar(successTrans('delete', { name: collectionTrans('entites') }));
  159. handleQuery(expression);
  160. };
  161. const toolbarConfigs: ToolBarConfig[] = [
  162. {
  163. type: 'iconBtn',
  164. onClick: () => {
  165. setDialog({
  166. open: true,
  167. type: 'custom',
  168. params: {
  169. component: (
  170. <DeleteTemplate
  171. label={btnTrans('drop')}
  172. title={dialogTrans('deleteTitle', {
  173. type: collectionTrans('entites'),
  174. })}
  175. text={collectionTrans('deleteDataWarning')}
  176. handleDelete={handleDelete}
  177. />
  178. ),
  179. },
  180. });
  181. },
  182. label: collectionTrans('delete'),
  183. icon: 'delete',
  184. // tooltip: collectionTrans('deleteTooltip'),
  185. disabledTooltip: collectionTrans('deleteTooltip'),
  186. disabled: () => selectedData.length === 0,
  187. },
  188. {
  189. type: 'iconBtn',
  190. onClick: () => {
  191. try {
  192. const opts = {};
  193. const parser = new Parser(opts);
  194. const csv = parser.parse(queryResult);
  195. const csvData = new Blob([csv], {
  196. type: 'text/csv;charset=utf-8',
  197. });
  198. saveAs(csvData, 'milvus_query_result.csv');
  199. } catch (err) {
  200. console.error(err);
  201. }
  202. },
  203. label: '',
  204. icon: 'download',
  205. tooltip: collectionTrans('downloadTooltip'),
  206. disabledTooltip: collectionTrans('downloadDisabledTooltip'),
  207. disabled: () => !queryResult?.length,
  208. },
  209. ];
  210. return (
  211. <div className={classes.root}>
  212. <CustomToolBar toolbarConfigs={toolbarConfigs} />
  213. <div className={classes.toolbar}>
  214. <div className="left">
  215. <TextField
  216. className="textarea"
  217. InputProps={{
  218. classes: {
  219. root: 'textfield',
  220. multiline: 'multiline',
  221. },
  222. }}
  223. placeholder={collectionTrans('exprPlaceHolder')}
  224. value={expression}
  225. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  226. setExpression(e.target.value as string);
  227. }}
  228. onKeyDown={e => {
  229. if (e.key === 'Enter') {
  230. // Do code here
  231. handleQuery(expression);
  232. e.preventDefault();
  233. }
  234. }}
  235. />
  236. <Filter
  237. ref={filterRef}
  238. title="Advanced Filter"
  239. fields={fields.filter(
  240. i =>
  241. i.type !== DataTypeStringEnum.FloatVector &&
  242. i.type !== DataTypeStringEnum.BinaryVector
  243. )}
  244. filterDisabled={false}
  245. onSubmit={handleFilterSubmit}
  246. showTitle={false}
  247. showTooltip={false}
  248. />
  249. {/* </div> */}
  250. {/* <CustomDatePicker
  251. label={timeTravelInfo.label}
  252. onChange={handleDateTimeChange}
  253. date={timeTravel}
  254. setDate={setTimeTravel}
  255. /> */}
  256. </div>
  257. <div className="right">
  258. <CustomButton
  259. className="btn"
  260. onClick={handleFilterReset}
  261. disabled={!expression}
  262. >
  263. <ResetIcon classes={{ root: 'icon' }} />
  264. {btnTrans('reset')}
  265. </CustomButton>
  266. <CustomButton
  267. variant="contained"
  268. disabled={!expression}
  269. onClick={() => handleQuery(expression)}
  270. >
  271. {btnTrans('query')}
  272. </CustomButton>
  273. </div>
  274. </div>
  275. {tableLoading || queryResult?.length ? (
  276. <AttuGrid
  277. toolbarConfigs={[]}
  278. colDefinitions={fields.map(i => ({
  279. id: i.name,
  280. align: 'left',
  281. disablePadding: false,
  282. label: i.name,
  283. }))}
  284. primaryKey={primaryKey.value}
  285. openCheckBox={true}
  286. isLoading={!!tableLoading}
  287. rows={result}
  288. rowCount={total}
  289. selected={selectedData}
  290. setSelected={handleSelectChange}
  291. page={currentPage}
  292. onChangePage={handlePageChange}
  293. rowsPerPage={pageSize}
  294. setRowsPerPage={handlePageSize}
  295. orderBy={orderBy}
  296. order={order}
  297. handleSort={handleGridSort}
  298. />
  299. ) : (
  300. <EmptyCard
  301. wrapperClass={`page-empty-card ${classes.emptyCard}`}
  302. icon={<VectorSearchIcon />}
  303. text={
  304. queryResult?.length === 0
  305. ? searchTrans('empty')
  306. : collectionTrans('startTip')
  307. }
  308. subText={collectionTrans('dataQuerylimits')}
  309. />
  310. )}
  311. </div>
  312. );
  313. };
  314. export default Query;