2
0
Эх сурвалжийг харах

Feat/support view longtext (#720)

* add drawer

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* part 1

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* part 2

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* finish

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 4 сар өмнө
parent
commit
b289a29f23

+ 106 - 0
client/src/components/DataListView/DataListView.tsx

@@ -0,0 +1,106 @@
+import { Typography } from '@mui/material';
+import { CollectionFullObject } from '@server/types';
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+import { formatFieldType } from '@/utils';
+import DataView from '@/components/DataView/DataView';
+import { DYNAMIC_FIELD } from '@/consts';
+import CopyButton from '@/components/advancedSearch/CopyButton';
+
+interface DataListViewProps {
+  collection: CollectionFullObject;
+  data: any;
+}
+const useStyles = makeStyles((theme: Theme) => ({
+  root: {
+    padding: 16,
+    cursor: 'initial',
+  },
+  dataTitleContainer: {
+    display: 'flex',
+    justifyContent: 'space-between',
+  },
+  title: {
+    fontSize: 14,
+    fontWeight: 600,
+  },
+  type: {
+    color: theme.palette.text.secondary,
+    fontSize: 11,
+    marginLeft: 4,
+    marginTop: 8,
+  },
+  dataContainer: {
+    display: 'flex',
+    padding: 8,
+    border: `1px solid ${theme.palette.divider}`,
+    backgroundColor: theme.palette.background.paper,
+    borderRadius: 4,
+    marginBottom: 16,
+    maxHeight: 400,
+    overflow: 'auto',
+  },
+  copy: {
+    marginLeft: 0,
+    '& svg': {
+      width: 15,
+    },
+  },
+}));
+
+const DataListView = (props: DataListViewProps) => {
+  // props
+  const { collection, data } = props;
+  // styles
+  const classes = useStyles();
+
+  // page data
+  let row = data[0];
+
+  // merge dymaic fields into row
+  row = {
+    ...row,
+    ...row[DYNAMIC_FIELD],
+  };
+
+  if (row[DYNAMIC_FIELD]) {
+    delete row[DYNAMIC_FIELD];
+  }
+
+  if (!row) {
+    return <Typography>No data</Typography>;
+  }
+
+  return (
+    <div className={classes.root}>
+      {Object.keys(row).map((name: string, index: number) => {
+        const field = collection.schema.fields.find(f => f.name === name);
+        return (
+          <div key={index}>
+            <div className={classes.dataTitleContainer}>
+              <span className={classes.title}>
+                {name}
+                <CopyButton
+                  className={classes.copy}
+                  value={row[name]}
+                  label={name}
+                />
+              </span>
+              <span className={classes.type}>
+                {(field && formatFieldType(field)) || 'meta'}
+              </span>
+            </div>
+            <div className={classes.dataContainer}>
+              <DataView
+                type={(field && field.data_type) || 'any'}
+                value={row[name]}
+              />
+            </div>
+          </div>
+        );
+      })}
+    </div>
+  );
+};
+
+export default DataListView;

+ 7 - 3
client/src/components/DataView/DataView.tsx

@@ -13,7 +13,7 @@ const DataView = (props: { type: string; value: any }) => {
     case 'BFloat16Vector':
     case 'FloatVector':
     case 'Float16Vector':
-      const stringValue = JSON.stringify(value, null, 2);
+      const stringValue = JSON.stringify(value, null);
       // remove escape characters
       const formattedValue = stringValue
         .replace(/\\n/g, '\n')
@@ -22,11 +22,15 @@ const DataView = (props: { type: string; value: any }) => {
 
       // remove first and last double quotes if present
       const trimmedValue = formattedValue.replace(/^"|"$/g, '');
-      return <Typography title={trimmedValue}>{trimmedValue}</Typography>;
+      return (
+        <Typography variant="mono" component="p" title={trimmedValue}>
+          {trimmedValue}
+        </Typography>
+      );
 
     default:
       return (
-        <Typography title={value}>
+        <Typography variant="mono" component="p" title={value}>
           {JSON.stringify(value).replace(/^"|"$/g, '')}
         </Typography>
       );

+ 5 - 3
client/src/components/MediaPreview/MediaPreview.tsx

@@ -97,19 +97,21 @@ const MediaPreview = (props: { value: string }) => {
       <div
         onMouseMove={handleMouseOver}
         onMouseOut={handleMouseOut}
-        style={{ cursor: 'pointer' }}
+        style={isImg ? { cursor: 'pointer' } : {}}
       >
         {isImg ? (
           <>
             <icons.img />
-            <Typography title={value}>
+            <Typography variant="mono" component="p" title={value}>
               <a href={value} target="_blank">
                 {value}
               </a>
             </Typography>
           </>
         ) : (
-          <Typography title={value}>{value}</Typography>
+          <Typography variant="mono" component="p" title={value}>
+            {value}
+          </Typography>
         )}
       </div>
       {showImage && (

+ 39 - 0
client/src/components/customDrawer/CustomDrawer.tsx

@@ -0,0 +1,39 @@
+// CustomDrawer.tsx
+import React, { useContext } from 'react';
+import { Drawer, Box, Typography, Button } from '@mui/material';
+import { rootContext } from '@/context';
+
+const CustomDrawer = () => {
+  const { drawer, setDrawer } = useContext(rootContext);
+
+  const handleCloseDrawer = () => {
+    setDrawer({
+      ...drawer,
+      open: false,
+    });
+  };
+
+  return (
+    <Drawer open={drawer.open} onClose={handleCloseDrawer} anchor="right">
+      <Box sx={{ width: '33vw' }}>
+        <div>{drawer.content}</div>
+
+        {drawer.hasActionBar && (
+          <Box sx={{ display: 'flex', justifyContent: 'flex-end', padding: 1 }}>
+            {drawer.actions?.map((action, index) => (
+              <Button
+                key={index}
+                onClick={action.onClick}
+                sx={{ marginLeft: 1 }}
+              >
+                {action.label}
+              </Button>
+            ))}
+          </Box>
+        )}
+      </Box>
+    </Drawer>
+  );
+};
+
+export default CustomDrawer;

+ 17 - 0
client/src/components/icons/Icons.tsx

@@ -976,6 +976,23 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       ></path>
     </SvgIcon>
   ),
+  eye: (props = {}) => (
+    <SvgIcon
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </SvgIcon>
+  ),
 };
 
 export default icons;

+ 2 - 1
client/src/components/icons/Types.ts

@@ -60,4 +60,5 @@ export type IconsType =
   | 'night'
   | 'img'
   | 'fileplus'
-  | 'file';
+  | 'file'
+  | 'eye';

+ 24 - 0
client/src/context/Root.tsx

@@ -6,9 +6,11 @@ import {
   DialogType,
   SnackBarType,
   OpenSnackBarType,
+  DrawerType,
 } from './Types';
 import CustomSnackBar from '@/components/customSnackBar/CustomSnackBar';
 import CustomDialog from '@/components/customDialog/CustomDialog';
+import CustomDrawer from '@/components/customDrawer/CustomDrawer';
 import { MilvusService } from '@/http';
 
 const DefaultDialogConfigs: DialogType = {
@@ -22,6 +24,14 @@ const DefaultDialogConfigs: DialogType = {
   },
 };
 
+const DefaultDrawerConfigs: DrawerType = {
+  open: false,
+  title: '',
+  content: <></>,
+  hasActionBar: false,
+  actions: [],
+};
+
 export const rootContext = React.createContext<RootContextType>({
   openSnackBar: (
     message,
@@ -36,6 +46,8 @@ export const rootContext = React.createContext<RootContextType>({
   handleCloseDialog: () => {},
   handleCloseDialog2: () => {},
   versionInfo: { attu: '', sdk: '' },
+  drawer: DefaultDrawerConfigs, // Add drawer to context
+  setDrawer: () => {}, // Function to set drawer state
 });
 
 const { Provider } = rootContext;
@@ -57,6 +69,14 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
   const [dialog, setDialog] = useState<DialogType>(DefaultDialogConfigs);
   const [dialog2, setDialog2] = useState<DialogType>(DefaultDialogConfigs);
 
+  const [drawer, setDrawer] = useState<DrawerType>({
+    open: false,
+    title: '',
+    content: <></>,
+    hasActionBar: false,
+    actions: [],
+  });
+
   const [versionInfo, setVersionInfo] = useState({ attu: '', sdk: '' });
 
   const handleSnackBarClose = () => {
@@ -128,6 +148,8 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
         handleCloseDialog,
         handleCloseDialog2,
         versionInfo,
+        drawer,
+        setDrawer,
       }}
     >
       {props.children}
@@ -135,6 +157,8 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
       <CustomSnackBar {...snackBar} onClose={handleSnackBarClose} />
       <CustomDialog {...dialog} onClose={handleCloseDialog} />
       <CustomDialog {...dialog2} onClose={handleCloseDialog2} />
+
+      <CustomDrawer />
     </Provider>
   );
 };

+ 15 - 0
client/src/context/Types.ts

@@ -21,6 +21,8 @@ export type RootContextType = {
   handleCloseDialog: () => void;
   handleCloseDialog2: () => void;
   versionInfo: { attu: string; sdk: string };
+  drawer: DrawerType;
+  setDrawer: (params: DrawerType) => void;
 };
 
 // this is for any custom dialog
@@ -151,3 +153,16 @@ export type DataContextType = {
   };
   setUIPref: (pref: DataContextType['ui']) => void;
 };
+
+export interface DrawerAction {
+  label: string;
+  onClick: () => void;
+}
+
+export interface DrawerType {
+  open: boolean;
+  title: string;
+  content: React.ReactNode;
+  hasActionBar: boolean;
+  actions?: DrawerAction[];
+}

+ 2 - 0
client/src/i18n/cn/button.ts

@@ -44,6 +44,7 @@ const btnTrans = {
   modify: '修改',
   downloadSchema: '下载 Schema',
   editDefaultValue: '编辑默认值',
+  viewData: '查看数据',
 
   // tips
   loadColTooltip: '加载Collection',
@@ -58,6 +59,7 @@ const btnTrans = {
   duplicateTooltip: '复制collection,不包含数据',
   renameTooltip: '重命名collection',
   editEntityTooltip: '编辑entity',
+  viewDataTooltip: '查看详细数据',
 
   // disable tooltip
   downloadDisabledTooltip: '导出前请先选择数据。',

+ 2 - 0
client/src/i18n/en/button.ts

@@ -44,6 +44,7 @@ const btnTrans = {
   modify: 'Modify',
   downloadSchema: 'Download Schema',
   EditDefaultValue: 'Edit Default Value',
+  viewData: 'View Data',
 
   // tips
   loadColTooltip: 'Load Collection',
@@ -58,6 +59,7 @@ const btnTrans = {
   duplicateTooltip: 'Duplicate selected collection without data',
   renameTooltip: 'Rename collection',
   editEntityTooltip: 'Edit entity(JSON)',
+  viewDataTooltip: 'View data detail',
 
   // disable tooltip
   downloadDisabledTooltip: 'Please select data before exporting',

+ 22 - 1
client/src/pages/databases/collections/data/CollectionData.tsx

@@ -30,6 +30,7 @@ import CustomInput from '@/components/customInput/CustomInput';
 import CustomMultiSelector from '@/components/customSelector/CustomMultiSelector';
 import CollectionColHeader from '../CollectionColHeader';
 import DataView from '@/components/DataView/DataView';
+import DataListView from '@/components/DataListView/DataListView';
 
 export interface CollectionDataProps {
   collectionName: string;
@@ -63,7 +64,7 @@ const CollectionData = (props: CollectionDataProps) => {
   ];
 
   // UI functions
-  const { setDialog, handleCloseDialog, openSnackBar } =
+  const { setDialog, handleCloseDialog, openSnackBar, setDrawer } =
     useContext(rootContext);
   const { fetchCollection } = useContext(dataContext);
   // icons
@@ -251,6 +252,26 @@ const CollectionData = (props: CollectionDataProps) => {
       tooltip: btnTrans('emptyTooltip'),
       disabled: () => selectedData?.length > 0 || total == 0,
     },
+    {
+      icon: 'eye',
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        setDrawer({
+          open: true,
+          title: 'custom',
+          content: <DataListView collection={collection} data={selectedData} />,
+          hasActionBar: true,
+          actions: [],
+        });
+      },
+      label: btnTrans('viewData'),
+      tooltip: btnTrans('viewDataTooltip'),
+      disabled: () => selectedData?.length !== 1,
+      hideOnDisable() {
+        return selectedData?.length === 0;
+      },
+    },
     {
       type: 'button',
       btnVariant: 'text',

+ 1 - 4
client/src/pages/dialogs/CreateCollectionDialog.tsx

@@ -244,12 +244,9 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
           fnOutputFields.push(sparseField);
         }
 
-        if (data.analyzer_params && data.enable_analyzer) {
+        if (data.analyzer_params) {
           // if analyzer_params is string, we need to use default value
           data.analyzer_params = getAnalyzerParams(data.analyzer_params);
-        } else {
-          delete data.analyzer_params;
-          delete data.enable_analyzer;
         }
 
         data.is_primary_key && (data.autoID = form.autoID);

+ 10 - 0
client/src/styles/theme.ts

@@ -1,5 +1,10 @@
 import { PaletteMode } from '@mui/material';
 
+declare module '@mui/material/Typography' {
+  interface TypographyPropsVariantOverrides {
+    mono: true; // Custom variant
+  }
+}
 
 const getCommonThemes = (mode: PaletteMode) => ({
   typography: {
@@ -101,6 +106,11 @@ export const getAttuTheme = (mode: PaletteMode) => {
             fontSize: '10px',
             lineHeight: '12px',
           },
+          mono: {
+            fontFamily: 'monospace',
+            fontSize: '12px',
+            lineHeight: 1.5,
+          },
         },
       },
       MuiButton: {

+ 7 - 2
client/src/utils/Format.ts

@@ -105,7 +105,7 @@ export const checkIsBinarySubstructure = (metricLabel: string): boolean => {
 
 export const isVectorType = (field: FieldObject): boolean => {
   return VectorTypes.includes(field.dataType as any);
-}
+};
 
 export const getCreateFieldType = (field: CreateField): CreateFieldType => {
   if (field.is_primary_key) {
@@ -218,7 +218,12 @@ export const formatUtcToMilvus = (bigNumber: number) => {
  * @returns
  */
 export const formatFieldType = (field: FieldObject) => {
-  const { data_type, element_type, maxLength, maxCapacity, dimension } = field;
+  const { name, data_type, element_type, maxLength, maxCapacity, dimension } =
+    field;
+
+  if (name === '$meta') {
+    return `${data_type}`;
+  }
 
   const elementType =
     element_type !== 'None'