mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: support creation of indexes on the console for postgres sources
https://github.com/hasura/graphql-engine-mono/pull/358 Co-authored-by: Ikechukwu Eze <22247592+iykekings@users.noreply.github.com> GitOrigin-RevId: cd4a5073fb361fe1235f5d49a0dc9b025fc1379f
This commit is contained in:
parent
39c9e9f7e3
commit
3f35a9a219
@ -5,6 +5,7 @@
|
||||
(Add entries below in the order of server, console, cli, docs, others)
|
||||
|
||||
- server: fix GraphQL type for single-row returning functions (close #7109)
|
||||
- console: add support for creation of indexes for Postgres data sources
|
||||
|
||||
## v2.0.6
|
||||
|
||||
|
5
console/package-lock.json
generated
5
console/package-lock.json
generated
@ -23169,6 +23169,11 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-essentials": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz",
|
||||
"integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ=="
|
||||
},
|
||||
"ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
|
@ -105,7 +105,8 @@
|
||||
"sql-formatter": "2.3.3",
|
||||
"styled-components": "5.0.1",
|
||||
"styled-system": "5.1.5",
|
||||
"subscriptions-transport-ws": "0.9.16"
|
||||
"subscriptions-transport-ws": "0.9.16",
|
||||
"ts-essentials": "^7.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.13.10",
|
||||
|
@ -47,6 +47,10 @@ import {
|
||||
import { getRunSqlQuery } from '../../Common/utils/v1QueryUtils';
|
||||
import { services } from '../../../dataSources/services';
|
||||
import insertReducer from './TableInsertItem/InsertActions';
|
||||
import {
|
||||
checkFeatureSupport,
|
||||
READ_ONLY_RUN_SQL_QUERIES,
|
||||
} from '../../../helpers/versionUtils';
|
||||
|
||||
const SET_TABLE = 'Data/SET_TABLE';
|
||||
const LOAD_FUNCTIONS = 'Data/LOAD_FUNCTIONS';
|
||||
@ -1004,6 +1008,46 @@ const fetchPartitionDetails = table => {
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchTableIndexDetails = tableInfo => {
|
||||
return (dispatch, getState) => {
|
||||
const url = Endpoints.query;
|
||||
const currState = getState();
|
||||
const { currentDataSource } = currState.tables;
|
||||
const { table_name: table, table_schema: schema } = tableInfo;
|
||||
const query = getRunSqlQuery(
|
||||
dataSource.tableIndexSql({ table, schema }),
|
||||
currentDataSource,
|
||||
false,
|
||||
checkFeatureSupport(READ_ONLY_RUN_SQL_QUERIES) || false
|
||||
);
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(query),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
try {
|
||||
return JSON.parse(data?.result?.[1]?.[0] ?? '[]');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch(
|
||||
showErrorNotification(
|
||||
'Error fetching indexes information',
|
||||
null,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/* ******************************************************* */
|
||||
const dataReducer = (state = defaultState, action) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
|
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||
import IndexFieldsEditor from './IndexFieldsEditor';
|
||||
import { Table } from '../../../../dataSources/types';
|
||||
|
||||
import styles from './ModifyTable.scss';
|
||||
|
||||
type Props = {
|
||||
tableSchema: Table;
|
||||
};
|
||||
|
||||
const tooltipMessage =
|
||||
'Indexes are used to increase query performance based on columns that are queried frequently';
|
||||
|
||||
const IndexFields: React.FC<Props> = props => (
|
||||
<>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Indexes
|
||||
<Tooltip message={tooltipMessage} />
|
||||
</h4>
|
||||
<IndexFieldsEditor currentTableInfo={props.tableSchema} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default IndexFields;
|
@ -0,0 +1,373 @@
|
||||
import React, { useEffect, useReducer, useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import Select, { ValueType } from 'react-select';
|
||||
|
||||
import { Index, IndexType, Table } from '../../../../dataSources/types';
|
||||
import { Button } from '../../../Common';
|
||||
import { mapDispatchToPropsEmpty } from '../../../Common/utils/reactUtils';
|
||||
import { removeIndex, saveIndex } from './ModifyActions';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import ExpandableEditor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||
import TextInput from '../../../Common/TextInput/TextInput';
|
||||
import ToolTip from '../../../Common/Tooltip/Tooltip';
|
||||
import { dataSource, isFeatureSupported } from '../../../../dataSources';
|
||||
import { fetchTableIndexDetails } from '../DataActions';
|
||||
|
||||
import styles from './ModifyTable.scss';
|
||||
|
||||
type IndexState = {
|
||||
index_name: string;
|
||||
index_type: IndexType;
|
||||
index_columns: string[];
|
||||
unique?: boolean;
|
||||
};
|
||||
|
||||
export const defaultIndexState: IndexState = {
|
||||
index_name: '',
|
||||
index_type: 'btree',
|
||||
index_columns: [],
|
||||
unique: false,
|
||||
};
|
||||
|
||||
interface UpdateIndexName {
|
||||
type: 'Indexes/UPDATE_INDEX_NAME';
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface UpdateIndexUniqueState {
|
||||
type: 'Indexes/UPDATE_INDEX_UNIQUE_STATE';
|
||||
data: boolean;
|
||||
}
|
||||
|
||||
interface UpdateIndexType {
|
||||
type: 'Indexes/UPDATE_INDEX_TYPE';
|
||||
data: IndexType;
|
||||
}
|
||||
|
||||
interface UpdateIndexColumns {
|
||||
type: 'Indexes/UPDATE_INDEX_COLUMNS';
|
||||
data: string[];
|
||||
}
|
||||
|
||||
interface ResetIndexState {
|
||||
type: 'Indexes/RESET_INDEX_STATE';
|
||||
}
|
||||
|
||||
type IndexStateAction =
|
||||
| UpdateIndexColumns
|
||||
| UpdateIndexName
|
||||
| UpdateIndexType
|
||||
| UpdateIndexUniqueState
|
||||
| ResetIndexState;
|
||||
|
||||
const indexStateReducer = (
|
||||
state: IndexState,
|
||||
action: IndexStateAction
|
||||
): IndexState => {
|
||||
switch (action.type) {
|
||||
case 'Indexes/UPDATE_INDEX_NAME':
|
||||
return {
|
||||
...state,
|
||||
index_name: action.data,
|
||||
};
|
||||
case 'Indexes/UPDATE_INDEX_TYPE':
|
||||
return {
|
||||
...state,
|
||||
index_type: action.data,
|
||||
};
|
||||
case 'Indexes/UPDATE_INDEX_COLUMNS':
|
||||
return {
|
||||
...state,
|
||||
index_columns: action.data,
|
||||
};
|
||||
case 'Indexes/UPDATE_INDEX_UNIQUE_STATE':
|
||||
return {
|
||||
...state,
|
||||
unique: action.data,
|
||||
};
|
||||
case 'Indexes/RESET_INDEX_STATE':
|
||||
return defaultIndexState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const formTooltips = dataSource.indexFormToolTips;
|
||||
|
||||
const supportedIndex = dataSource.supportedIndex;
|
||||
|
||||
type IndexColumnsSelect = Record<'label' | 'value', string>;
|
||||
|
||||
interface IndexFieldsEditorProps extends ConnectorProps {
|
||||
currentTableInfo: Table;
|
||||
}
|
||||
|
||||
const isUnique = (indexSql: string) => /CREATE\s+UNIQUE/i.test(indexSql);
|
||||
|
||||
const getDefCols = (indexSql: string) =>
|
||||
indexSql.split(/USING \w+ /)?.[1] ?? '';
|
||||
|
||||
interface CreateIndexProps {
|
||||
indexState: IndexState;
|
||||
tableColumnOptions: IndexColumnsSelect[];
|
||||
indexTypeOptions: Array<{
|
||||
label: string;
|
||||
value: IndexType;
|
||||
}>;
|
||||
onChangeIndextypeSelect: (value: ValueType<IndexColumnsSelect>) => void;
|
||||
updateIndexName: (name: string) => void;
|
||||
onChangeIndexColumnsSelect: (value: ValueType<IndexColumnsSelect>) => void;
|
||||
toggleIndexCheckboxState: (currentValue: boolean) => () => void;
|
||||
}
|
||||
|
||||
const CreateIndexForm: React.FC<CreateIndexProps> = ({
|
||||
indexState,
|
||||
tableColumnOptions,
|
||||
indexTypeOptions,
|
||||
onChangeIndexColumnsSelect,
|
||||
updateIndexName,
|
||||
onChangeIndextypeSelect,
|
||||
toggleIndexCheckboxState,
|
||||
}) => (
|
||||
<div className={styles.indexEditorContainer}>
|
||||
<div
|
||||
className={`${styles.add_mar_top_small} ${styles.add_mar_bottom} ${styles.indexEditorGrid}`}
|
||||
>
|
||||
<div className={styles.indexNameWidth}>
|
||||
<label htmlFor="index-name" className={styles.indexCreateFormLabel}>
|
||||
Index Name
|
||||
</label>
|
||||
{formTooltips?.indexName && (
|
||||
<ToolTip message={formTooltips.indexName} />
|
||||
)}
|
||||
<TextInput
|
||||
onChange={e => updateIndexName(e.target.value)}
|
||||
value={indexState.index_name}
|
||||
id="index-name"
|
||||
placeholder="Input Name"
|
||||
bsclass={styles.inputNameStyles}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.modifyIndexSelectContainer}>
|
||||
<label
|
||||
htmlFor="index-type-select"
|
||||
className={styles.indexCreateFormLabel}
|
||||
>
|
||||
Index Type
|
||||
</label>
|
||||
{formTooltips?.indexType && (
|
||||
<ToolTip message={formTooltips.indexType} />
|
||||
)}
|
||||
<Select
|
||||
options={indexTypeOptions}
|
||||
className={`${styles.indexColumnSelectStyles} legacy-input-fix`}
|
||||
placeholder="-- select index type --"
|
||||
onChange={onChangeIndextypeSelect}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="create-index-columns"
|
||||
className={styles.indexCreateFormLabel}
|
||||
>
|
||||
Columns
|
||||
</label>
|
||||
{formTooltips?.indexColumns && (
|
||||
<ToolTip message={formTooltips.indexColumns} />
|
||||
)}
|
||||
<Select
|
||||
isMulti={supportedIndex?.multiColumn.includes(indexState.index_type)}
|
||||
options={tableColumnOptions}
|
||||
className={`${styles.indexColumnSelectStyles} legacy-input-fix`}
|
||||
placeholder="-- select columns --"
|
||||
onChange={onChangeIndexColumnsSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.indexOptionsContainer}>
|
||||
<span className={styles.indexOption}>
|
||||
<label htmlFor="index-unique" className={styles.indexCreateFormLabel}>
|
||||
Unique?
|
||||
</label>
|
||||
{formTooltips?.unique ? (
|
||||
<ToolTip message={formTooltips.unique} />
|
||||
) : null}
|
||||
<input
|
||||
type="checkbox"
|
||||
id="index-unique"
|
||||
onChange={toggleIndexCheckboxState(indexState?.unique ?? false)}
|
||||
checked={indexState?.unique ?? false}
|
||||
className={`${styles.uniqueCheckbox} legacy-input-fix`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FieldsEditor: React.FC<IndexFieldsEditorProps> = props => {
|
||||
const { dispatch, currentTableInfo } = props;
|
||||
const [indexState, indexStateDispatch] = useReducer(
|
||||
indexStateReducer,
|
||||
defaultIndexState
|
||||
);
|
||||
const [indexes, setIndexes] = useState<Index[]>([]);
|
||||
|
||||
const fetchIndexes = () => {
|
||||
dispatch(fetchTableIndexDetails(currentTableInfo)).then((data: Index[]) => {
|
||||
setIndexes(data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(fetchIndexes, []);
|
||||
|
||||
const updateIndexName = (name: string) =>
|
||||
indexStateDispatch({ type: 'Indexes/UPDATE_INDEX_NAME', data: name });
|
||||
|
||||
const toggleIndexCheckboxState = (currentValue: boolean) => () =>
|
||||
indexStateDispatch({
|
||||
type: 'Indexes/UPDATE_INDEX_UNIQUE_STATE',
|
||||
data: !currentValue,
|
||||
});
|
||||
|
||||
const tableColumns = currentTableInfo.columns.map(
|
||||
column => column.column_name
|
||||
);
|
||||
const tableColumnOptions = tableColumns.reduce(
|
||||
(acc: IndexColumnsSelect[], columnName) => [
|
||||
...acc,
|
||||
{ label: columnName, value: columnName },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const indexTypeOptions = Object.entries(dataSource.indexTypes ?? {}).map(
|
||||
([key, value]) => ({
|
||||
label: key,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
const onChangeIndexColumnsSelect = (value: ValueType<IndexColumnsSelect>) => {
|
||||
if (value) {
|
||||
indexStateDispatch({
|
||||
type: 'Indexes/UPDATE_INDEX_COLUMNS',
|
||||
data: Array.isArray(value)
|
||||
? value.map(val => val.value).slice(0, 32)
|
||||
: [(value as IndexColumnsSelect).value],
|
||||
});
|
||||
}
|
||||
};
|
||||
const onChangeIndextypeSelect = (value: ValueType<IndexColumnsSelect>) => {
|
||||
if (value) {
|
||||
indexStateDispatch({
|
||||
type: 'Indexes/UPDATE_INDEX_TYPE',
|
||||
data: (value as IndexColumnsSelect).value as IndexType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetIndexEditState = () =>
|
||||
indexStateDispatch({ type: 'Indexes/RESET_INDEX_STATE' });
|
||||
|
||||
const onSave = (toggleEditor: () => void) => {
|
||||
if (
|
||||
!indexState.index_name ||
|
||||
!indexState.index_columns?.length ||
|
||||
!indexState.index_type
|
||||
) {
|
||||
dispatch(
|
||||
showErrorNotification(
|
||||
'Some Required Fields are Empty',
|
||||
'Index Name, Index Columns and Index Type are all required fields'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const successCb = () => {
|
||||
fetchIndexes();
|
||||
toggleEditor();
|
||||
};
|
||||
dispatch(saveIndex(indexState, successCb));
|
||||
};
|
||||
|
||||
const onClickRemoveIndex = (indexInfo: Index) => () =>
|
||||
dispatch(removeIndex(indexInfo, fetchIndexes));
|
||||
|
||||
const isPrimarykeyIndex = (a: Index) =>
|
||||
a.index_name === currentTableInfo.primary_key?.constraint_name;
|
||||
|
||||
const pkSortFn = (a: Index, b: Index) =>
|
||||
Number(isPrimarykeyIndex(b)) - Number(isPrimarykeyIndex(a));
|
||||
|
||||
const numberOfIndexes = indexes.length;
|
||||
const editorExpanded = () => (
|
||||
<CreateIndexForm
|
||||
indexState={indexState}
|
||||
tableColumnOptions={tableColumnOptions}
|
||||
indexTypeOptions={indexTypeOptions}
|
||||
onChangeIndexColumnsSelect={onChangeIndexColumnsSelect}
|
||||
updateIndexName={updateIndexName}
|
||||
onChangeIndextypeSelect={onChangeIndextypeSelect}
|
||||
toggleIndexCheckboxState={toggleIndexCheckboxState}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.indexesList}>
|
||||
{numberOfIndexes
|
||||
? indexes.sort(pkSortFn).map(indexInfo => {
|
||||
const indexSql = indexInfo.index_definition_sql;
|
||||
return (
|
||||
<div
|
||||
key={indexInfo.index_name}
|
||||
className={styles.indexesListItem}
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
className={styles.indexRemoveBtn}
|
||||
disabled={isPrimarykeyIndex(indexInfo)}
|
||||
onClick={onClickRemoveIndex(indexInfo)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<b className={styles.indexListItemIndexName}>
|
||||
{indexInfo.index_name}
|
||||
</b>
|
||||
<p className={styles.indexDef}>
|
||||
{isPrimarykeyIndex(indexInfo) && <i>PRIMARY KEY, </i>}
|
||||
{isUnique(indexSql) && <i>UNIQUE</i>}
|
||||
<i className={styles.uppercase}>{indexInfo.index_type}</i>
|
||||
<b>
|
||||
<i>on</i>
|
||||
</b>
|
||||
<span>{getDefCols(indexSql)}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
{isFeatureSupported('tables.modify.indexes.edit') ? (
|
||||
<ExpandableEditor
|
||||
editorExpanded={editorExpanded}
|
||||
property="create-index"
|
||||
service="modify-table"
|
||||
saveFunc={onSave}
|
||||
expandButtonText={`Create ${numberOfIndexes ? ' an index' : ''}`}
|
||||
collapsedLabel={() =>
|
||||
`${!numberOfIndexes ? 'No Indexes Present' : ''}`
|
||||
}
|
||||
collapseButtonText="Cancel"
|
||||
collapseCallback={resetIndexEditState}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const indexFieldsEditorConnector = connect(null, mapDispatchToPropsEmpty);
|
||||
type ConnectorProps = ConnectedProps<typeof indexFieldsEditorConnector>;
|
||||
const IndexFieldsEditor = indexFieldsEditorConnector(FieldsEditor);
|
||||
|
||||
export default IndexFieldsEditor;
|
@ -2077,6 +2077,125 @@ export const setViewCustomColumnNames = (
|
||||
);
|
||||
};
|
||||
|
||||
const saveIndex = (indexInfo, successCb, errorCb) => (dispatch, getState) => {
|
||||
if (!indexInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataSource.createIndexSql && !dataSource.dropIndexSql) {
|
||||
// ERROR: this datasource does not support creation/deletion of indexes
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!indexInfo?.index_name?.trim() ||
|
||||
!indexInfo?.index_columns?.length ||
|
||||
!indexInfo?.index_type
|
||||
) {
|
||||
dispatch(
|
||||
showErrorNotification(
|
||||
'Some required fields are missing',
|
||||
'Index Name, Index Columns and Index Type are all required fields'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentSchema, currentTable, currentDataSource } = getState().tables;
|
||||
const upQueries = [];
|
||||
const downQueries = [];
|
||||
|
||||
const upSql = dataSource.createIndexSql({
|
||||
table: { schema: currentSchema, name: currentTable },
|
||||
columns: indexInfo?.index_columns,
|
||||
indexName: indexInfo?.index_name?.trim(),
|
||||
indexType: indexInfo?.index_type,
|
||||
unique: indexInfo?.unique,
|
||||
});
|
||||
const downSql = dataSource.dropIndexSql(indexInfo?.index_name);
|
||||
|
||||
upQueries.push(getRunSqlQuery(upSql, currentDataSource));
|
||||
downQueries.push(getRunSqlQuery(downSql, currentDataSource));
|
||||
|
||||
const migrationName = `create_index_${indexInfo?.index_name || ''}`;
|
||||
const requestMsg = 'Creating index....';
|
||||
const successMsg = `Created index ${indexInfo?.index_name} successfully`;
|
||||
const errorMsg = 'Failed to create index';
|
||||
|
||||
const customOnSuccess = () => successCb?.();
|
||||
|
||||
const customOnError = () => errorCb?.();
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQueries,
|
||||
downQueries,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg
|
||||
);
|
||||
};
|
||||
|
||||
const removeIndex = (indexInfo, successCb, errorCb) => (dispatch, getState) => {
|
||||
if (!indexInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataSource.createIndexSql && !dataSource.dropIndexSql) {
|
||||
// ERROR: this datasource does not support creation/deletion of indexes
|
||||
return;
|
||||
}
|
||||
|
||||
const removeConfirmation = getConfirmation(
|
||||
`You want to remove the index: ${indexInfo?.index_name || ''}`
|
||||
);
|
||||
if (!removeConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentTable, currentSchema, currentDataSource } = getState().tables;
|
||||
const upQueries = [];
|
||||
const downQueries = [];
|
||||
|
||||
const upSql = dataSource.dropIndexSql(indexInfo?.index_name);
|
||||
const downSql = dataSource.createIndexSql({
|
||||
indexName: indexInfo?.index_name,
|
||||
indexType: indexInfo?.index_type,
|
||||
table: { schema: currentSchema, name: currentTable },
|
||||
columns: indexInfo?.index_columns,
|
||||
unique: indexInfo?.unique,
|
||||
});
|
||||
|
||||
upQueries.push(getRunSqlQuery(upSql, currentDataSource));
|
||||
downQueries.push(getRunSqlQuery(downSql, currentDataSource));
|
||||
|
||||
const migrationName = `drop_index_${indexInfo?.index_name || 'indexName'}`;
|
||||
const requestMsg = 'Removing index....';
|
||||
const successMsg = 'Removed index successfully';
|
||||
const errorMsg = 'Failed to remove index';
|
||||
|
||||
const customOnSuccess = () => successCb?.();
|
||||
|
||||
const customOnError = () => errorCb?.();
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQueries,
|
||||
downQueries,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
FETCH_COLUMN_TYPE_CASTS,
|
||||
FETCH_COLUMN_TYPE_CASTS_FAIL,
|
||||
@ -2138,4 +2257,6 @@ export {
|
||||
modifyRootFields,
|
||||
setCheckConstraints,
|
||||
modifyTableCustomName,
|
||||
saveIndex,
|
||||
removeIndex,
|
||||
};
|
||||
|
@ -51,6 +51,7 @@ import { RightContainer } from '../../../Common/Layout/RightContainer';
|
||||
import { NotSupportedNote } from '../../../Common/NotSupportedNote';
|
||||
import ConnectedComputedFields from './ComputedFields';
|
||||
import FeatureDisabled from '../FeatureDisabled';
|
||||
import IndexFields from './IndexFields';
|
||||
import PartitionInfo from './PartitionInfo';
|
||||
|
||||
class ModifyTable extends React.Component {
|
||||
@ -311,7 +312,12 @@ class ModifyTable extends React.Component {
|
||||
<hr className="my-lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isFeatureSupported('tables.modify.indexes.view') ? (
|
||||
<>
|
||||
<IndexFields tableSchema={table} />
|
||||
<hr />
|
||||
</>
|
||||
) : null}
|
||||
{isFeatureSupported('tables.modify.triggers') && (
|
||||
<>
|
||||
<div className={styles.add_mar_bottom}>
|
||||
|
@ -72,6 +72,121 @@ hr {
|
||||
// overflow: auto;
|
||||
}
|
||||
|
||||
.modifyIndexEditorExpand {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modifyIndexSelectContainer {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
&:first-child {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.indexEditorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.indexEditorGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 20px;
|
||||
align-items: center;
|
||||
grid-template-rows: 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.selectIndexMenuHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.indexDef {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.indexesList {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.indexesListItem {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, max-content);
|
||||
grid-column-gap: 8px;
|
||||
align-items: baseline;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.indexRemoveBtn {
|
||||
user-select: none;
|
||||
color: rgb(206, 26, 26);
|
||||
margin-right: 12px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.indexCreateFormLabel {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.indexOptionsContainer {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
grid-row: 3;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.indexOption {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
input[type='checkbox'] {
|
||||
margin-left: 16px;
|
||||
margin-top: 0px !important;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.inputNameStyles {
|
||||
margin-top: 8px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.indexColumnSelectStyles {
|
||||
margin-top: 8px;
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.uniqueCheckbox {
|
||||
outline: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
@ -111,7 +226,3 @@ hr {
|
||||
.paddingTopSm {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
|
||||
.max_width {
|
||||
max-width: 200px;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
|
||||
import { Path, get } from '../components/Common/utils/tsUtils';
|
||||
import { services } from './services';
|
||||
@ -9,6 +10,7 @@ import {
|
||||
ComputedField,
|
||||
TableColumn,
|
||||
FrequentlyUsedColumn,
|
||||
IndexType,
|
||||
PermissionColumnCategories,
|
||||
SupportedFeaturesType,
|
||||
generateTableRowRequestType,
|
||||
@ -19,6 +21,7 @@ import {
|
||||
GenerateDeleteRowRequest,
|
||||
GenerateBulkDeleteRowRequest,
|
||||
ViolationActions,
|
||||
IndexFormTips as IndexFormToolTips,
|
||||
} from './types';
|
||||
import { PGFunction, FunctionState } from './services/postgresql/types';
|
||||
import { Operations } from './common';
|
||||
@ -333,6 +336,21 @@ export interface DataSourcesAPI {
|
||||
schemas: string[];
|
||||
tables: Table[];
|
||||
}) => string;
|
||||
tableIndexSql?: (options: { schema: string; table: string }) => string;
|
||||
createIndexSql?: (indexInfo: {
|
||||
indexName: string;
|
||||
indexType: IndexType;
|
||||
table: QualifiedTable;
|
||||
columns: string[];
|
||||
unique?: boolean;
|
||||
}) => string;
|
||||
dropIndexSql?: (indexName: string) => string;
|
||||
indexFormToolTips?: IndexFormToolTips;
|
||||
indexTypes?: Record<string, IndexType>;
|
||||
supportedIndex?: {
|
||||
multiColumn: string[];
|
||||
singleColumn: string[];
|
||||
};
|
||||
getFKRelations: (options: { schemas: string[]; tables: Table[] }) => string;
|
||||
getReferenceOption: (opt: string) => string;
|
||||
deleteFunctionSql?: (
|
||||
@ -352,7 +370,7 @@ export interface DataSourcesAPI {
|
||||
viewsSupported: boolean;
|
||||
// use null, if all operators are supported
|
||||
supportedColumnOperators: string[] | null;
|
||||
supportedFeatures?: SupportedFeaturesType;
|
||||
supportedFeatures?: DeepRequired<SupportedFeaturesType>;
|
||||
violationActions: ViolationActions[];
|
||||
defaultRedirectSchema?: string;
|
||||
generateInsertRequest?: () => generateInsertRequestType;
|
||||
@ -366,27 +384,25 @@ export interface DataSourcesAPI {
|
||||
export let currentDriver: Driver = 'postgres';
|
||||
export let dataSource: DataSourcesAPI = services[currentDriver || 'postgres'];
|
||||
|
||||
export const isFeatureSupported = (feature: Path<SupportedFeaturesType>) => {
|
||||
export const isFeatureSupported = (
|
||||
feature: Path<DeepRequired<SupportedFeaturesType>>
|
||||
) => {
|
||||
if (dataSource.supportedFeatures)
|
||||
return get(dataSource.supportedFeatures, feature);
|
||||
};
|
||||
|
||||
export const getSupportedDrivers = (
|
||||
feature: Path<SupportedFeaturesType>
|
||||
): Driver[] => {
|
||||
const isEnabled = (supportedFeatures: SupportedFeaturesType) => {
|
||||
return get(supportedFeatures, feature) || false;
|
||||
};
|
||||
|
||||
return [
|
||||
export const getSupportedDrivers = (feature: Path<SupportedFeaturesType>) =>
|
||||
[
|
||||
PGSupportedFeatures,
|
||||
MssqlSupportedFeatures,
|
||||
BigQuerySupportedFeatures,
|
||||
CitusQuerySupportedFeatures,
|
||||
]
|
||||
.filter(d => isEnabled(d))
|
||||
.map(d => d.driver.name) as Driver[];
|
||||
};
|
||||
].reduce((driverList: Driver[], supportedFeaturesObj) => {
|
||||
if (get(supportedFeaturesObj, feature)) {
|
||||
return [...driverList, supportedFeaturesObj.driver.name];
|
||||
}
|
||||
return driverList;
|
||||
}, []);
|
||||
|
||||
class DataSourceChangedEvent extends Event {
|
||||
static type = 'data-source-changed';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { DataSourcesAPI } from '../..';
|
||||
import { QualifiedTable } from '../../../metadata/types';
|
||||
import {
|
||||
@ -78,9 +79,12 @@ export const isJsonColumn = (column: BaseTableColumn): boolean => {
|
||||
return column.data_type_name === 'json' || column.data_type_name === 'jsonb';
|
||||
};
|
||||
|
||||
export const supportedFeatures: SupportedFeaturesType = {
|
||||
export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
driver: {
|
||||
name: 'bigquery',
|
||||
fetchVersion: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
create: {
|
||||
@ -93,6 +97,8 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
tables: {
|
||||
create: {
|
||||
enabled: false,
|
||||
frequentlyUsedColumns: false,
|
||||
columnTypeSelector: false,
|
||||
},
|
||||
browse: {
|
||||
enabled: true,
|
||||
@ -103,11 +109,18 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
enabled: false,
|
||||
},
|
||||
modify: {
|
||||
editableTableName: false,
|
||||
readOnly: false,
|
||||
comments: {
|
||||
view: false,
|
||||
edit: false,
|
||||
},
|
||||
enabled: false,
|
||||
columns: {
|
||||
view: false,
|
||||
edit: false,
|
||||
graphqlFieldName: false,
|
||||
frequentlyUsedColumns: false,
|
||||
},
|
||||
computedFields: false,
|
||||
primaryKeys: {
|
||||
@ -127,6 +140,10 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
view: false,
|
||||
edit: false,
|
||||
},
|
||||
indexes: {
|
||||
view: false,
|
||||
edit: false,
|
||||
},
|
||||
customGqlRoot: false,
|
||||
setAsEnum: false,
|
||||
untrack: false,
|
||||
@ -135,6 +152,7 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
relationships: {
|
||||
enabled: true,
|
||||
track: false,
|
||||
remoteRelationships: false,
|
||||
},
|
||||
permissions: {
|
||||
enabled: true,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
|
||||
import { DataSourcesAPI } from '../..';
|
||||
import { getFetchTablesListQuery, schemaListSql } from './sqlUtils';
|
||||
import {
|
||||
@ -14,10 +16,13 @@ import {
|
||||
supportedFeatures as PgSupportedFeatures,
|
||||
} from '../postgresql';
|
||||
|
||||
export const supportedFeatures: SupportedFeaturesType = {
|
||||
export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
...PgSupportedFeatures,
|
||||
driver: {
|
||||
name: 'citus',
|
||||
fetchVersion: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
tables: {
|
||||
...(PgSupportedFeatures?.tables ?? {}),
|
||||
@ -35,6 +40,10 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
setAsEnum: false,
|
||||
untrack: true,
|
||||
delete: true,
|
||||
indexes: {
|
||||
edit: false,
|
||||
view: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
events: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { DataSourcesAPI } from '../..';
|
||||
import { QualifiedTable } from '../../../metadata/types';
|
||||
import {
|
||||
@ -96,9 +97,12 @@ const violationActions: ViolationActions[] = [
|
||||
'set default',
|
||||
];
|
||||
|
||||
export const supportedFeatures: SupportedFeaturesType = {
|
||||
export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
driver: {
|
||||
name: 'mssql',
|
||||
fetchVersion: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
create: {
|
||||
@ -112,6 +116,7 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
create: {
|
||||
enabled: true,
|
||||
frequentlyUsedColumns: false,
|
||||
columnTypeSelector: false,
|
||||
},
|
||||
browse: {
|
||||
enabled: true,
|
||||
@ -122,6 +127,7 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
enabled: false,
|
||||
},
|
||||
modify: {
|
||||
editableTableName: false,
|
||||
enabled: true,
|
||||
columns: {
|
||||
view: true,
|
||||
@ -148,6 +154,10 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
view: true,
|
||||
edit: false,
|
||||
},
|
||||
indexes: {
|
||||
view: false,
|
||||
edit: false,
|
||||
},
|
||||
customGqlRoot: false,
|
||||
setAsEnum: false,
|
||||
untrack: true,
|
||||
@ -160,6 +170,7 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
relationships: {
|
||||
enabled: true,
|
||||
track: true,
|
||||
remoteRelationships: false,
|
||||
},
|
||||
permissions: {
|
||||
enabled: true,
|
||||
|
@ -238,6 +238,9 @@ WHERE
|
||||
primaryKeysInfoSql,
|
||||
uniqueKeysSql,
|
||||
checkConstraintsSql: undefined,
|
||||
tableIndexSql: undefined,
|
||||
createIndexSql: undefined,
|
||||
dropIndexSql: undefined,
|
||||
getFKRelations,
|
||||
getReferenceOption: (option: string) => option,
|
||||
getEventInvocationInfoByIDSql: undefined,
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableColumn,
|
||||
@ -6,6 +8,7 @@ import {
|
||||
SupportedFeaturesType,
|
||||
BaseTableColumn,
|
||||
ViolationActions,
|
||||
IndexType,
|
||||
} from '../../types';
|
||||
import { QUERY_TYPES, Operations } from '../../common';
|
||||
import { PGFunction } from './types';
|
||||
@ -62,6 +65,9 @@ import {
|
||||
deleteFunctionSql,
|
||||
getEventInvocationInfoByIDSql,
|
||||
getDatabaseInfo,
|
||||
tableIndexSql,
|
||||
getCreateIndexSql,
|
||||
getDropIndexSql,
|
||||
getTableInfo,
|
||||
getDatabaseVersionSql,
|
||||
} from './sqlUtils';
|
||||
@ -513,7 +519,31 @@ const permissionColumnDataTypes = {
|
||||
user_defined: [], // default for all other types
|
||||
};
|
||||
|
||||
export const supportedFeatures: SupportedFeaturesType = {
|
||||
const indexFormToolTips = {
|
||||
unique:
|
||||
'Causes the system to check for duplicate values in the table when the index is created (if data already exist) and each time data is added',
|
||||
indexName:
|
||||
'The name of the index to be created. No schema name can be included here; the index is always created in the same schema as its parent table',
|
||||
indexType:
|
||||
'Only B-Tree, GiST, GIN and BRIN support multi-column indexes on PostgreSQL',
|
||||
indexColumns:
|
||||
'In PostgreSQL, at most 32 fields can be provided while creating an index',
|
||||
};
|
||||
|
||||
const indexTypes: Record<string, IndexType> = {
|
||||
BRIN: 'brin',
|
||||
'SP-GIST': 'spgist',
|
||||
GiST: 'gist',
|
||||
HASH: 'hash',
|
||||
'B-Tree': 'btree',
|
||||
GIN: 'gin',
|
||||
};
|
||||
|
||||
const supportedIndex = {
|
||||
multiColumn: ['brin', 'gist', 'btree', 'gin'],
|
||||
singleColumn: ['hash', 'spgist'],
|
||||
};
|
||||
export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
driver: {
|
||||
name: 'postgres',
|
||||
fetchVersion: {
|
||||
@ -537,11 +567,13 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
browse: {
|
||||
enabled: true,
|
||||
aggregation: true,
|
||||
customPagination: false,
|
||||
},
|
||||
insert: {
|
||||
enabled: true,
|
||||
},
|
||||
modify: {
|
||||
readOnly: false,
|
||||
enabled: true,
|
||||
editableTableName: true,
|
||||
comments: {
|
||||
@ -572,6 +604,10 @@ export const supportedFeatures: SupportedFeaturesType = {
|
||||
view: true,
|
||||
edit: true,
|
||||
},
|
||||
indexes: {
|
||||
view: true,
|
||||
edit: true,
|
||||
},
|
||||
customGqlRoot: true,
|
||||
setAsEnum: true,
|
||||
untrack: true,
|
||||
@ -724,6 +760,12 @@ export const postgres: DataSourcesAPI = {
|
||||
deleteFunctionSql,
|
||||
getEventInvocationInfoByIDSql,
|
||||
getDatabaseInfo,
|
||||
tableIndexSql,
|
||||
createIndexSql: getCreateIndexSql,
|
||||
dropIndexSql: getDropIndexSql,
|
||||
indexFormToolTips,
|
||||
indexTypes,
|
||||
supportedIndex,
|
||||
getTableInfo,
|
||||
operators,
|
||||
generateTableRowRequest,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Table, FrequentlyUsedColumn } from '../../types';
|
||||
import { Table, FrequentlyUsedColumn, IndexType } from '../../types';
|
||||
import { isColTypeString } from '.';
|
||||
import { FunctionState } from './types';
|
||||
import { QualifiedTable } from '../../../metadata/types';
|
||||
@ -1046,6 +1046,71 @@ SELECT n.nspname::text AS table_schema,
|
||||
) AS info;
|
||||
`;
|
||||
|
||||
export const tableIndexSql = (options: { schema: string; table: string }) => `
|
||||
SELECT
|
||||
COALESCE(
|
||||
json_agg(
|
||||
row_to_json(info)
|
||||
),
|
||||
'[]' :: JSON
|
||||
) AS indexes
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
t.relname as table_name,
|
||||
i.relname as index_name,
|
||||
it.table_schema as table_schema,
|
||||
am.amname as index_type,
|
||||
array_agg(DISTINCT a.attname) as index_columns,
|
||||
pi.indexdef as index_definition_sql
|
||||
FROM
|
||||
pg_class t,
|
||||
pg_class i,
|
||||
pg_index ix,
|
||||
pg_attribute a,
|
||||
information_schema.tables it,
|
||||
pg_am am,
|
||||
pg_indexes pi
|
||||
WHERE
|
||||
t.oid = ix.indrelid
|
||||
and i.oid = ix.indexrelid
|
||||
and a.attrelid = t.oid
|
||||
and a.attnum = ANY(ix.indkey)
|
||||
and t.relkind = 'r'
|
||||
and pi.indexname = i.relname
|
||||
and t.relname = '${options.table}'
|
||||
and it.table_schema = '${options.schema}'
|
||||
and am.oid = i.relam
|
||||
GROUP BY
|
||||
t.relname,
|
||||
i.relname,
|
||||
it.table_schema,
|
||||
am.amname,
|
||||
pi.indexdef
|
||||
ORDER BY
|
||||
t.relname,
|
||||
i.relname
|
||||
) as info;
|
||||
`;
|
||||
|
||||
export const getCreateIndexSql = (indexObj: {
|
||||
indexName: string;
|
||||
indexType: IndexType;
|
||||
table: QualifiedTable;
|
||||
columns: string[];
|
||||
unique?: boolean;
|
||||
}) => {
|
||||
const { indexName, indexType, table, columns, unique = false } = indexObj;
|
||||
|
||||
return `
|
||||
CREATE ${unique ? 'UNIQUE' : ''} INDEX "${indexName}" on
|
||||
"${table.schema}"."${table.name}" using ${indexType} (${columns.join(', ')});
|
||||
`;
|
||||
};
|
||||
|
||||
export const getDropIndexSql = (indexName: string) =>
|
||||
`DROP INDEX IF EXISTS "${indexName}"`;
|
||||
|
||||
export const frequentlyUsedColumns: FrequentlyUsedColumn[] = [
|
||||
{
|
||||
name: 'id',
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
getRunSqlQuery,
|
||||
WhereClause,
|
||||
} from '../../src/components/Common/utils/v1QueryUtils';
|
||||
import { Driver } from '.';
|
||||
|
||||
export interface Relationship
|
||||
extends Pick<BaseTable, 'table_name' | 'table_schema'> {
|
||||
@ -90,6 +91,24 @@ export type ComputedField = {
|
||||
comment: string | null;
|
||||
};
|
||||
|
||||
export type IndexType = 'btree' | 'hash' | 'gin' | 'gist' | 'spgist' | 'brin';
|
||||
|
||||
export type Index = {
|
||||
table_name: string;
|
||||
table_schema: string;
|
||||
index_name: string;
|
||||
index_type: IndexType;
|
||||
index_columns: string[];
|
||||
index_definition_sql: string;
|
||||
};
|
||||
|
||||
export type IndexFormTips = {
|
||||
unique: string;
|
||||
indexName: string;
|
||||
indexColumns: string;
|
||||
indexType: string;
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
schema_name: string;
|
||||
};
|
||||
@ -214,7 +233,7 @@ export type PermissionColumnCategories = Record<ColumnCategories, string[]>;
|
||||
|
||||
export type SupportedFeaturesType = {
|
||||
driver: {
|
||||
name: string;
|
||||
name: Driver;
|
||||
fetchVersion?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
@ -273,6 +292,10 @@ export type SupportedFeaturesType = {
|
||||
view: boolean;
|
||||
edit: boolean;
|
||||
};
|
||||
indexes?: {
|
||||
view: boolean;
|
||||
edit: boolean;
|
||||
};
|
||||
customGqlRoot?: boolean;
|
||||
setAsEnum?: boolean;
|
||||
untrack?: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user