Bläddra i källkod

add structure table

tumao 4 år sedan
förälder
incheckning
cacbe60bd0

+ 3 - 0
client/src/assets/icons/key.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8808 1.50995C14.1182 1.74522 14.1182 2.12668 13.8808 2.36195L13.0948 3.14086L14.4888 4.52223C14.7262 4.7575 14.7262 5.13896 14.4888 5.37423L12.361 7.48283C12.1236 7.7181 11.7386 7.7181 11.5012 7.48283L10.1073 6.10146L8.43759 7.75607C8.6245 8.00658 8.78102 8.27889 8.90336 8.56747C9.10521 9.04361 9.21003 9.55463 9.21178 10.0711C9.21352 10.5875 9.11216 11.0992 8.91353 11.5767C8.7149 12.0542 8.42291 12.488 8.0544 12.8532C7.68588 13.2184 7.24811 13.5077 6.76628 13.7046C6.28446 13.9014 5.7681 14.0019 5.24694 14.0001C4.72578 13.9984 4.21011 13.8945 3.72963 13.6945C3.24915 13.4945 2.81334 13.2022 2.4473 12.8346L2.44274 12.83C1.72292 12.0915 1.32464 11.1022 1.33365 10.0755C1.34265 9.04874 1.75824 8.06657 2.4909 7.34052C3.22356 6.61447 4.21468 6.20263 5.25078 6.19371C6.08538 6.18652 6.89538 6.44123 7.56841 6.9134L13.0211 1.50995C13.2585 1.27468 13.6434 1.27468 13.8808 1.50995ZM12.2351 3.99287L10.967 5.24946L11.9311 6.20483L13.1991 4.94823L12.2351 3.99287ZM3.31509 11.9906C2.81817 11.4796 2.54326 10.7957 2.54948 10.086C2.55572 9.37514 2.84343 8.69517 3.35066 8.19252C3.85789 7.68987 4.54405 7.40475 5.26135 7.39857C5.97865 7.3924 6.6697 7.66565 7.18567 8.15949C7.19118 8.16477 7.19677 8.16992 7.20243 8.17495C7.4495 8.42177 7.64642 8.71346 7.78238 9.03415C7.92213 9.36379 7.99469 9.71757 7.9959 10.0751C7.99711 10.4327 7.92694 10.7869 7.78942 11.1175C7.65191 11.448 7.44976 11.7484 7.19464 12.0012C6.93951 12.254 6.63644 12.4543 6.30286 12.5906C5.96929 12.7269 5.61181 12.7964 5.25101 12.7952C4.89021 12.794 4.53321 12.7221 4.20057 12.5836C3.86893 12.4456 3.56803 12.244 3.31509 11.9906ZM3.31509 11.9906L3.31733 11.9929L2.88005 12.4115L3.3128 11.9883L3.31509 11.9906Z" fill="#010E29"/>
+</svg>

+ 1 - 1
client/src/components/grid/index.tsx

@@ -98,7 +98,7 @@ const MilvusGrid: FC<MilvusGridType> = props => {
     searchForm,
     searchForm,
     openCheckBox = true,
     openCheckBox = true,
     disableSelect = false,
     disableSelect = false,
-    noData = t('grid.noData'),
+    noData = gridTrans.noData,
     showHoverStyle = true,
     showHoverStyle = true,
     selected = [],
     selected = [],
     setSelected = () => {},
     setSelected = () => {},

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

@@ -26,6 +26,7 @@ import { ReactComponent as ConsoleIcon } from '../../assets/icons/console.svg';
 import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
+import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
   search: (props = {}) => <SearchIcon {...props} />,
@@ -68,6 +69,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   load: (props = {}) => (
   load: (props = {}) => (
     <SvgIcon viewBox="0 0 24 24" component={LoadIcon} {...props} />
     <SvgIcon viewBox="0 0 24 24" component={LoadIcon} {...props} />
   ),
   ),
+  key: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={KeyIcon} {...props} />
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

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

@@ -23,4 +23,5 @@ export type IconsType =
   | 'info'
   | 'info'
   | 'release'
   | 'release'
   | 'load'
   | 'load'
-  | 'remove';
+  | 'remove'
+  | 'key';

+ 0 - 2
client/src/http/BaseModel.ts

@@ -55,9 +55,7 @@ export default class BaseModel {
    */
    */
   static async create(options: updateParamsType) {
   static async create(options: updateParamsType) {
     const { path, data } = options;
     const { path, data } = options;
-
     const res = await http.post(path, data);
     const res = await http.post(path, data);
-
     return new this(res.data.data || {});
     return new this(res.data.data || {});
   }
   }
 
 

+ 74 - 0
client/src/http/Field.ts

@@ -0,0 +1,74 @@
+import { DataType } from '../pages/collections/Types';
+import { FieldView } from '../pages/structure/Types';
+import { IndexState } from '../types/Milvus';
+import BaseModel from './BaseModel';
+import { IndexHttp } from './Index';
+
+export class FieldHttp extends BaseModel implements FieldView {
+  data_type!: DataType;
+  fieldID!: string;
+  type_params!: { key: string; value: string }[];
+  is_primary_key!: true;
+  name!: string;
+  // data from index http
+  _indexType!: string;
+  _indexParameterPairs!: { key: string; value: string }[];
+  _indexStatus!: IndexState;
+  _createIndexDisabled!: boolean;
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static async getFields(collectionName: string): Promise<FieldHttp[]> {
+    const path = `/collections/${collectionName}`;
+
+    const res = await super.findAll({
+      path,
+      params: {},
+    });
+
+    return res.schema.fields.map((f: any) => new this(f));
+  }
+
+  static async getStructureListWithIndex(
+    collectionName: string
+  ): Promise<FieldHttp[]> {
+    const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+    const indexList = await IndexHttp.getIndexInfo(collectionName);
+    const structureList = [...(await this.getFields(collectionName))];
+    let fields: FieldHttp[] = [];
+    for (const structure of structureList) {
+      if (vectorTypes.includes(structure.data_type)) {
+        const index = indexList.find(i => i._fieldName === structure.name);
+        structure._indexParameterPairs = index?._indexParameterPairs || [];
+        structure._indexType = index?._indexType || '';
+        structure._createIndexDisabled = indexList.length > 0;
+      }
+
+      fields = [...fields, structure];
+    }
+    return fields;
+  }
+
+  get _fieldId() {
+    return this.fieldID;
+  }
+
+  get _isPrimaryKey() {
+    return this.is_primary_key;
+  }
+
+  get _fieldName() {
+    return this.name;
+  }
+
+  get _fieldType() {
+    return this.data_type;
+  }
+
+  get _dimension() {
+    return this.type_params.find(item => item.key === 'dim')?.value || '--';
+  }
+}

+ 48 - 0
client/src/http/Index.ts

@@ -0,0 +1,48 @@
+import { IndexView } from '../pages/structure/Types';
+import { IndexState } from '../types/Milvus';
+import BaseModel from './BaseModel';
+
+export class IndexHttp extends BaseModel implements IndexView {
+  params!: { key: string; value: string }[];
+  field_name!: string;
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static BASE_URL = `/schema/index`;
+
+  static async getIndexStatus(
+    collectionName: string,
+    fieldName: string
+  ): Promise<IndexState> {
+    const path = `${this.BASE_URL}/state`;
+    return super.findAll({
+      path,
+      params: { collection_name: collectionName, field_name: fieldName },
+    });
+  }
+
+  static async getIndexInfo(collectionName: string): Promise<IndexHttp[]> {
+    const path = this.BASE_URL;
+
+    const res = await super.findAll({
+      path,
+      params: { collection_name: collectionName },
+    });
+    return res.index_descriptions.map((index: any) => new this(index));
+  }
+
+  get _indexType() {
+    return this.params.find(p => p.key === 'index_type')?.value || '';
+  }
+
+  get _indexParameterPairs() {
+    return this.params.filter(p => p.key !== 'index_type');
+  }
+
+  get _fieldName() {
+    return this.field_name;
+  }
+}

+ 1 - 0
client/src/i18n/cn/collection.ts

@@ -26,6 +26,7 @@ const collectionTrans = {
   fieldName: 'Field Name',
   fieldName: 'Field Name',
   autoId: 'Auto ID',
   autoId: 'Auto ID',
   dimension: 'Dimension',
   dimension: 'Dimension',
+  dimensionTooltip: 'Only vector type has dimension',
   newBtn: 'add new field',
   newBtn: 'add new field',
 
 
   // load dialog
   // load dialog

+ 7 - 0
client/src/i18n/cn/index.ts

@@ -0,0 +1,7 @@
+const indexTrans = {
+  type: 'Index Type',
+  param: 'Index Parameters',
+  create: 'Create Index',
+};
+
+export default indexTrans;

+ 1 - 0
client/src/i18n/en/collection.ts

@@ -26,6 +26,7 @@ const collectionTrans = {
   fieldName: 'Field Name',
   fieldName: 'Field Name',
   autoId: 'Auto ID',
   autoId: 'Auto ID',
   dimension: 'Dimension',
   dimension: 'Dimension',
+  dimensionTooltip: 'Only vector type has dimension',
   newBtn: 'add new field',
   newBtn: 'add new field',
 
 
   // load dialog
   // load dialog

+ 8 - 0
client/src/i18n/en/index.ts

@@ -0,0 +1,8 @@
+const indexTrans = {
+  type: 'Index Type',
+  param: 'Index Parameters',
+
+  create: 'Create Index',
+};
+
+export default indexTrans;

+ 4 - 0
client/src/i18n/index.ts

@@ -19,6 +19,8 @@ import partitionCn from './cn/partition';
 import partitionEn from './en/partition';
 import partitionEn from './en/partition';
 import successEn from './en/success';
 import successEn from './en/success';
 import successCn from './cn/success';
 import successCn from './cn/success';
+import indexEn from './en/index';
+import indexCn from './cn/index';
 
 
 export const resources = {
 export const resources = {
   cn: {
   cn: {
@@ -31,6 +33,7 @@ export const resources = {
     dialog: dialogCn,
     dialog: dialogCn,
     partition: partitionCn,
     partition: partitionCn,
     success: successCn,
     success: successCn,
+    index: indexCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -42,6 +45,7 @@ export const resources = {
     dialog: dialogEn,
     dialog: dialogEn,
     partition: partitionEn,
     partition: partitionEn,
     success: successEn,
     success: successEn,
+    index: indexEn,
   },
   },
 };
 };
 
 

+ 2 - 1
client/src/pages/collections/Collection.tsx

@@ -7,6 +7,7 @@ import Partitions from '../partitions/partitions';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useMemo } from 'react';
 import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
+import Structure from '../structure/Structure';
 
 
 enum TAB_EMUM {
 enum TAB_EMUM {
   'partition',
   'partition',
@@ -45,7 +46,7 @@ const Collection = () => {
     },
     },
     {
     {
       label: t('structureTab'),
       label: t('structureTab'),
-      component: <section>structure section</section>,
+      component: <Structure collectionName={collectionName} />,
     },
     },
   ];
   ];
 
 

+ 1 - 1
client/src/pages/collections/Create.tsx

@@ -93,7 +93,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   const generalInfoConfigs: ITextfieldConfig[] = [
   const generalInfoConfigs: ITextfieldConfig[] = [
     {
     {
       label: t('name'),
       label: t('name'),
-      key: 'name',
+      key: 'collection_name',
       value: form.collection_name,
       value: form.collection_name,
       onChange: (value: string) => handleInputChange('collection_name', value),
       onChange: (value: string) => handleInputChange('collection_name', value),
       variant: 'filled',
       variant: 'filled',

+ 15 - 14
client/src/pages/collections/CreateFields.tsx

@@ -79,18 +79,20 @@ const CreateFields: FC<CreateFieldsProps> = ({
     label: string,
     label: string,
     value: number,
     value: number,
     onChange: (value: DataTypeEnum) => void
     onChange: (value: DataTypeEnum) => void
-  ) => (
-    <CustomSelector
-      options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
-      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
-        onChange(e.target.value as DataTypeEnum);
-      }}
-      value={value}
-      variant="filled"
-      label={label}
-      classes={{ root: classes.select }}
-    />
-  );
+  ) => {
+    return (
+      <CustomSelector
+        options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
+        onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+          onChange(e.target.value as DataTypeEnum);
+        }}
+        value={value}
+        variant="filled"
+        label={label}
+        classes={{ root: classes.select }}
+      />
+    );
+  };
 
 
   const getInput = (
   const getInput = (
     label: string,
     label: string,
@@ -131,7 +133,6 @@ const CreateFields: FC<CreateFieldsProps> = ({
         [key]: value,
         [key]: value,
       };
       };
     });
     });
-
     setFields(newFields);
     setFields(newFields);
   };
   };
 
 
@@ -248,7 +249,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           'all',
           'all',
           t('fieldType'),
           t('fieldType'),
           field.data_type,
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id, 'type', value)
+          (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
         )}
         )}
 
 
         {getInput(
         {getInput(

+ 10 - 0
client/src/pages/collections/Types.ts

@@ -38,6 +38,16 @@ export enum DataTypeEnum {
   FloatVector = 101,
   FloatVector = 101,
 }
 }
 
 
+export type DataType =
+  | 'Int8'
+  | 'Int16'
+  | 'Int32'
+  | 'Int64'
+  | 'Float'
+  | 'Double'
+  | 'BinaryVector'
+  | 'FloatVector';
+
 export interface Field {
 export interface Field {
   name: string;
   name: string;
   data_type: DataTypeEnum;
   data_type: DataTypeEnum;

+ 94 - 0
client/src/pages/structure/IndexTypeElement.tsx

@@ -0,0 +1,94 @@
+import { FC, useCallback, useEffect, useState } from 'react';
+import Chip from '@material-ui/core/Chip';
+import CustomButton from '../../components/customButton/CustomButton';
+import { IndexHttp } from '../../http/Index';
+import { IndexState } from '../../types/Milvus';
+import { FieldView } from './Types';
+import StatusIcon from '../../components/status/StatusIcon';
+import { ChildrenStatusType } from '../../components/status/Types';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import icons from '../../components/icons/Icons';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  item: {
+    paddingLeft: theme.spacing(1),
+  },
+  btn: {
+    '& span': {
+      textTransform: 'uppercase',
+    },
+  },
+  chip: {
+    backgroundColor: '#e9e9ed',
+  },
+}));
+
+const IndexTypeElement: FC<{ data: FieldView; collectionName: string }> = ({
+  data,
+  collectionName,
+}) => {
+  const classes = useStyles();
+
+  const [status, setStatus] = useState<string>('');
+  const { t } = useTranslation('index');
+
+  const AddIcon = icons.add;
+  const DeleteIcon = icons.delete;
+
+  const fetchStatus = useCallback(async () => {
+    const status = await IndexHttp.getIndexStatus(
+      collectionName,
+      data._fieldName
+    );
+    setStatus(status);
+  }, [collectionName, data._fieldName]);
+
+  useEffect(() => {
+    fetchStatus();
+  }, [fetchStatus]);
+
+  const handleCreate = () => {};
+
+  const handleDelete = () => {};
+
+  const generateElement = () => {
+    if (
+      data._fieldType !== 'BinaryVector' &&
+      data._fieldType !== 'FloatVector'
+    ) {
+      return <div className={classes.item}>--</div>;
+    }
+
+    switch (data._indexType) {
+      case '': {
+        return (
+          <CustomButton
+            disabled={data._createIndexDisabled}
+            className={classes.btn}
+            onClick={handleCreate}
+          >
+            <AddIcon />
+            {t('create')}
+          </CustomButton>
+        );
+      }
+      default: {
+        return status === IndexState.InProgress ? (
+          <StatusIcon type={ChildrenStatusType.CREATING} />
+        ) : (
+          <Chip
+            label={data._indexType}
+            classes={{ root: classes.chip }}
+            deleteIcon={<DeleteIcon />}
+            onDelete={handleDelete}
+          />
+        );
+      }
+    }
+  };
+
+  return <>{generateElement()}</>;
+};
+
+export default IndexTypeElement;

+ 183 - 0
client/src/pages/structure/Structure.tsx

@@ -0,0 +1,183 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC, useCallback, useEffect, useState } from 'react';
+import MilvusGrid from '../../components/grid';
+import { ColDefinitionsType } from '../../components/grid/Types';
+import { useTranslation } from 'react-i18next';
+import { usePaginationHook } from '../../hooks/Pagination';
+import icons from '../../components/icons/Icons';
+import CustomToolTip from '../../components/customToolTip/CustomToolTip';
+import { FieldHttp } from '../../http/Field';
+import { FieldView } from './Types';
+import IndexTypeElement from './IndexTypeElement';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: '100%',
+  },
+  icon: {
+    fontSize: '20px',
+    marginLeft: theme.spacing(0.5),
+  },
+  nameWrapper: {
+    display: 'flex',
+    alignItems: 'center',
+
+    '& .key': {
+      width: '16px',
+      height: '16px',
+      marginLeft: theme.spacing(0.5),
+    },
+  },
+
+  param: {
+    padding: theme.spacing(0.5),
+
+    marginRight: theme.spacing(2),
+
+    '& .key': {
+      color: '#82838e',
+      display: 'inline-block',
+      marginRight: theme.spacing(0.5),
+    },
+
+    '& .value': {
+      color: '#010e29',
+    },
+  },
+}));
+
+const Structure: FC<{
+  collectionName: string;
+}> = ({ collectionName }) => {
+  const classes = useStyles();
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: indexTrans } = useTranslation('index');
+  const InfoIcon = icons.info;
+
+  const [fields, setFields] = useState<FieldView[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+
+  const {
+    pageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: structureList,
+  } = usePaginationHook(fields);
+
+  const fetchFields = useCallback(
+    async (collectionName: string) => {
+      const KeyIcon = icons.key;
+
+      try {
+        const list = await FieldHttp.getStructureListWithIndex(collectionName);
+        const fields: FieldView[] = list.map(f =>
+          Object.assign(f, {
+            _fieldNameElement: (
+              <div className={classes.nameWrapper}>
+                {f._fieldName}
+                {f.is_primary_key && <KeyIcon classes={{ root: 'key' }} />}
+              </div>
+            ),
+            _indexParamElement: (
+              <>
+                {f._indexParameterPairs?.length > 0 ? (
+                  f._indexParameterPairs.map(p => (
+                    <span key={p.key} className={classes.param}>
+                      <Typography variant="caption" className="key">
+                        {`${p.key}:`}
+                      </Typography>
+                      <Typography variant="caption" className="value">
+                        {p.value}
+                      </Typography>
+                    </span>
+                  ))
+                ) : (
+                  <>--</>
+                )}
+              </>
+            ),
+            _indexTypeElement: (
+              <IndexTypeElement data={f} collectionName={collectionName} />
+            ),
+          })
+        );
+
+        setFields(fields);
+        setLoading(false);
+      } catch (err) {
+        setLoading(false);
+        throw err;
+      }
+    },
+    [classes.nameWrapper, classes.param]
+  );
+
+  useEffect(() => {
+    fetchFields(collectionName);
+  }, [collectionName, fetchFields]);
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: '_fieldNameElement',
+      align: 'left',
+      disablePadding: true,
+      label: collectionTrans('fieldName'),
+    },
+    {
+      id: '_fieldType',
+      align: 'left',
+      disablePadding: false,
+      label: collectionTrans('fieldType'),
+    },
+    {
+      id: '_dimension',
+      align: 'left',
+      disablePadding: false,
+      label: (
+        <span className="flex-center">
+          {collectionTrans('dimension')}
+          <CustomToolTip title={collectionTrans('dimensionTooltip')}>
+            <InfoIcon classes={{ root: classes.icon }} />
+          </CustomToolTip>
+        </span>
+      ),
+    },
+    {
+      id: '_indexTypeElement',
+      align: 'left',
+      disablePadding: true,
+      label: indexTrans('type'),
+    },
+    {
+      id: '_indexParamElement',
+      align: 'left',
+      disablePadding: false,
+      label: indexTrans('param'),
+    },
+  ];
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
+  return (
+    <section className={classes.wrapper}>
+      <MilvusGrid
+        toolbarConfigs={[]}
+        colDefinitions={colDefinitions}
+        rows={structureList}
+        rowCount={total}
+        primaryKey="_fieldId"
+        openCheckBox={false}
+        showHoverStyle={false}
+        page={currentPage}
+        onChangePage={handlePageChange}
+        rowsPerPage={pageSize}
+        isLoading={loading}
+      />
+    </section>
+  );
+};
+
+export default Structure;

+ 36 - 0
client/src/pages/structure/Types.ts

@@ -0,0 +1,36 @@
+import { ReactElement } from 'react';
+import { IndexState } from '../../types/Milvus';
+import { DataType } from '../collections/Types';
+
+export enum INDEX_TYPES_ENUM {
+  IVF_FLAT = 'IVF_FLAT',
+  IVF_PQ = 'IVF_PQ',
+  IVF_SQ8 = 'IVF_SQ8',
+  IVF_SQ8_HYBRID = 'IVF_SQ8_HYBRID',
+  FLAT = 'FLAT',
+  HNSW = 'HNSW',
+  ANNOY = 'ANNOY',
+  RNSG = 'RNSG',
+}
+
+export interface FieldView extends IndexView {
+  _fieldId: string;
+  _isPrimaryKey: boolean;
+  _fieldName: string;
+  _fieldNameElement?: ReactElement;
+  _fieldType: DataType;
+  _dimension: string;
+  _createIndexDisabled: boolean;
+}
+
+export interface Index {
+  params: { key: string; value: string }[];
+}
+
+export interface IndexView {
+  _fieldName: string;
+  _indexType: string;
+  _indexTypeElement?: ReactElement;
+  _indexParameterPairs: { key: string; value: string }[];
+  _indexParamElement?: ReactElement;
+}