console: replace redux fetching logic with react query for insert row tab

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6644
GitOrigin-RevId: ae7809e702edeafa84075bcad1ba37236bdb1d1a
This commit is contained in:
Erik Magnusson 2022-11-03 11:13:20 +02:00 committed by hasura-bot
parent d17b27e91f
commit a9515cdb68
12 changed files with 310 additions and 67 deletions

View File

@ -70,7 +70,7 @@ export interface TableRowProps {
refName: 'valueNode' | 'nullNode' | 'defaultNode' | 'radioNode', refName: 'valueNode' | 'nullNode' | 'defaultNode' | 'radioNode',
node: HTMLInputElement | null node: HTMLInputElement | null
) => void; ) => void;
enumOptions: Record<string, any>; enumOptions: string[];
index: string; index: string;
clone?: Record<string, any>; clone?: Record<string, any>;
onChange?: (e: React.ChangeEvent<HTMLInputElement>, val: unknown) => void; onChange?: (e: React.ChangeEvent<HTMLInputElement>, val: unknown) => void;

View File

@ -178,6 +178,7 @@ const loadSchema = (configOptions = {}) => {
) )
); );
} }
const body = { const body = {
type: 'bulk', type: 'bulk',
source, source,
@ -223,7 +224,6 @@ const loadSchema = (configOptions = {}) => {
return dispatch(requestAction(url, options)).then( return dispatch(requestAction(url, options)).then(
data => { data => {
if (!data || !data[0] || !data[0].result) return; if (!data || !data[0] || !data[0].result) return;
let mergedData = []; let mergedData = [];
switch (currentDriver) { switch (currentDriver) {
case 'postgres': case 'postgres':

View File

@ -49,7 +49,6 @@ import { ordinalColSort } from '../utils';
import Spinner from '../../../Common/Spinner/Spinner'; import Spinner from '../../../Common/Spinner/Spinner';
import { E_SET_EDITITEM } from '../TableEditItem/EditActions'; import { E_SET_EDITITEM } from '../TableEditItem/EditActions';
import { I_SET_CLONE } from '../TableInsertItem/InsertActions';
import { import {
getTableInsertRowRoute, getTableInsertRowRoute,
getTableEditRowRoute, getTableEditRowRoute,
@ -70,6 +69,8 @@ import {
getPersistedColumnsOrder, getPersistedColumnsOrder,
} from './tableUtils'; } from './tableUtils';
import { compareRows, isTableWithPK } from './utils'; import { compareRows, isTableWithPK } from './utils';
import { push } from 'react-router-redux';
import globals from '@/Globals';
const ViewRows = props => { const ViewRows = props => {
const { const {
@ -412,16 +413,19 @@ const ViewRows = props => {
const cloneIcon = <FaClone />; const cloneIcon = <FaClone />;
const handleCloneClick = () => { const handleCloneClick = () => {
dispatch({ type: I_SET_CLONE, clone: row }); const urlPrefix = globals.urlPrefix;
dispatch( dispatch(
_push( push({
getTableInsertRowRoute( pathname:
currentSchema, urlPrefix +
currentSource, getTableInsertRowRoute(
curTableName, currentSchema,
true currentSource,
) curTableName,
) true
),
state: { row },
})
); );
}; };

View File

@ -23,7 +23,6 @@ import { removeAll } from 'react-notification-system-redux';
import { getNotificationDetails } from '../../Common/Notification'; import { getNotificationDetails } from '../../Common/Notification';
import { getTableConfiguration } from '../TableBrowseRows/utils'; import { getTableConfiguration } from '../TableBrowseRows/utils';
const I_SET_CLONE = 'InsertItem/I_SET_CLONE';
const I_RESET = 'InsertItem/I_RESET'; const I_RESET = 'InsertItem/I_RESET';
const I_ONGOING_REQ = 'InsertItem/I_ONGOING_REQ'; const I_ONGOING_REQ = 'InsertItem/I_ONGOING_REQ';
const I_REQUEST_SUCCESS = 'InsertItem/I_REQUEST_SUCCESS'; const I_REQUEST_SUCCESS = 'InsertItem/I_REQUEST_SUCCESS';
@ -302,14 +301,6 @@ const insertReducer = (tableName, state, action) => {
lastSuccess: null, lastSuccess: null,
enumOptions: null, enumOptions: null,
}; };
case I_SET_CLONE:
return {
clone: action.clone,
ongoingRequest: false,
lastError: null,
lastSuccess: null,
enumOptions: null,
};
case I_ONGOING_REQ: case I_ONGOING_REQ:
return { return {
...state, ...state,
@ -360,4 +351,4 @@ const insertReducer = (tableName, state, action) => {
}; };
export default insertReducer; export default insertReducer;
export { fetchEnumOptions, insertItem, I_SET_CLONE, I_RESET, Open, Close }; export { fetchEnumOptions, insertItem, I_RESET, Open, Close };

View File

@ -1,10 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { useAppDispatch, useAppSelector } from '@/store'; import { useAppDispatch, useAppSelector } from '@/store';
import { setTable } from '../DataActions'; import { useMigrationMode, useReadOnlyMode } from '@/hooks';
import { fetchEnumOptions, I_RESET, insertItem } from './InsertActions'; import { useMetadata } from '@/features/MetadataAPI';
import { HasuraMetadataV3 } from '@/metadata/types';
import { insertItem } from './InsertActions';
import { ColumnName, RowValues } from '../TableCommon/DataTableRowItem.types'; import { ColumnName, RowValues } from '../TableCommon/DataTableRowItem.types';
import { DataTableRowItemProps } from '../TableCommon/DataTableRowItem'; import { DataTableRowItemProps } from '../TableCommon/DataTableRowItem';
import { TableInsertItems } from './TableInsertItems'; import { TableInsertItems } from './TableInsertItems';
import {
useTableEnums,
UseTableEnumsResponseArrayType,
} from './hooks/useTableEnums';
import { useSchemas } from './hooks/useSchemas';
import { TableObject } from './types';
type GetButtonTextArgs = { type GetButtonTextArgs = {
insertedRows: number; insertedRows: number;
@ -23,25 +31,93 @@ const getButtonText = ({ insertedRows, ongoingRequest }: GetButtonTextArgs) => {
return 'Save'; return 'Save';
}; };
const getTableWithEnumRelations = (
source: string,
schema: string,
metadata: HasuraMetadataV3 | undefined
) => {
return metadata
? (metadata?.sources
?.find(s => s.name === source)
?.tables.filter((t: any) => {
return t?.is_enum && t?.table?.schema === schema;
})
?.map(t => t?.table) as TableObject[])
: [];
};
const formatEnumOptions = (
tableEnums: UseTableEnumsResponseArrayType | undefined
) =>
tableEnums
? tableEnums?.reduce((tally, curr) => {
return {
...tally,
[curr.from]: curr.values,
};
}, {})
: [];
const getTableMetadata = (
source: string,
table: string,
metadata: HasuraMetadataV3 | undefined
) => {
return metadata?.sources
?.find((s: { name: string }) => s.name === source)
?.tables.filter(
(t: { table: { name: string } }) => t?.table?.name === table
)?.[0];
};
type TableInsertItemContainerContainer = { type TableInsertItemContainerContainer = {
params: { params: {
schema: string; schema: string;
source: string; source: string;
table: string; table: string;
}; };
router: { location: { state: any } };
}; };
export const TableInsertItemContainer = ( export const TableInsertItemContainer = (
props: TableInsertItemContainerContainer props: TableInsertItemContainerContainer
) => { ) => {
const { table: tableName } = props.params; const {
table: tableName,
source: dataSourceName,
schema: schemaName,
} = props.params;
const currentRow = props.router?.location?.state?.row;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isMigration, setIsMigration] = useState(false); const [isMigration, setIsMigration] = useState(false);
const [insertedRows, setInsertedRows] = useState(0); const [insertedRows, setInsertedRows] = useState(0);
const [values, setValues] = useState<Record<ColumnName, RowValues>>({}); const [values, setValues] = useState<Record<ColumnName, RowValues>>({});
const { data: metadata } = useMetadata();
const tableMetadata = getTableMetadata(
dataSourceName,
tableName,
metadata?.metadata
);
const { data: migrationMode } = useMigrationMode();
const { data: readOnlyMode } = useReadOnlyMode();
const { data: schemas, isLoading: schemasIsLoading } = useSchemas({
dataSourceName,
schemaName,
});
const tablesWithEnumRelations = getTableWithEnumRelations(
dataSourceName,
schemaName,
metadata?.metadata
);
const { data: tableEnums } = useTableEnums({
tables: tablesWithEnumRelations,
dataSourceName,
});
const onColumnUpdate: DataTableRowItemProps['onColumnUpdate'] = ( const onColumnUpdate: DataTableRowItemProps['onColumnUpdate'] = (
columnName, columnName,
rowValues rowValues
@ -59,38 +135,9 @@ export const TableInsertItemContainer = (
setValues(newValues); setValues(newValues);
}; };
useEffect(() => {
dispatch(setTable(tableName));
dispatch(fetchEnumOptions());
return () => {
dispatch({ type: I_RESET });
};
}, [tableName]);
const nextInsert = () =>
setInsertedRows(prevInsertedRows => prevInsertedRows + 1);
const toggleMigrationCheckBox = () => const toggleMigrationCheckBox = () =>
setIsMigration(prevIsMigration => !prevIsMigration); setIsMigration(prevIsMigration => !prevIsMigration);
const insert = useAppSelector(store => store.tables.insert);
const allSchemas = useAppSelector(store => store.tables.allSchemas);
const tablesView = useAppSelector(store => store.tables.view);
const currentSchema = useAppSelector(store => store.tables.currentSchema);
const currentDataSource = useAppSelector(
store => store.tables.currentDataSource
);
const migrationMode = useAppSelector(store => store.main.migrationMode);
const readOnlyMode = useAppSelector(store => store.main.readOnlyMode);
const { count } = tablesView;
const { ongoingRequest, lastError, lastSuccess, clone, enumOptions } = insert;
const buttonText = getButtonText({
insertedRows,
ongoingRequest,
});
const onClickClear = () => { const onClickClear = () => {
const form = document.getElementById('insertForm'); const form = document.getElementById('insertForm');
if (!form) { if (!form) {
@ -111,6 +158,22 @@ export const TableInsertItemContainer = (
}); });
}; };
const enumOptions = formatEnumOptions(tableEnums);
// Refactor in next iteration: --- Insert section start ---
const insert = useAppSelector(
(store: { tables: { insert: any } }) => store.tables.insert
);
const { ongoingRequest, lastError, lastSuccess } = insert;
const nextInsert = () =>
setInsertedRows(prevInsertedRows => prevInsertedRows + 1);
const buttonText = getButtonText({
insertedRows,
ongoingRequest,
});
const onClickSave: React.MouseEventHandler = e => { const onClickSave: React.MouseEventHandler = e => {
e.preventDefault(); e.preventDefault();
const inputValues = Object.keys(values).reduce< const inputValues = Object.keys(values).reduce<
@ -133,7 +196,7 @@ export const TableInsertItemContainer = (
dispatch( dispatch(
insertItem( insertItem(
tableName, tableName,
clone ? { ...clone, ...inputValues } : inputValues, currentRow ? { ...currentRow, ...inputValues } : inputValues,
isMigration isMigration
) )
).then(() => { ).then(() => {
@ -141,21 +204,26 @@ export const TableInsertItemContainer = (
}); });
}; };
// --- Insert section end ---
if (schemasIsLoading) return <p>Loading...</p>;
return ( return (
<TableInsertItems <TableInsertItems
isEnum={!!tableMetadata?.is_enum}
toggleMigrationCheckBox={toggleMigrationCheckBox} toggleMigrationCheckBox={toggleMigrationCheckBox}
onColumnUpdate={onColumnUpdate} onColumnUpdate={onColumnUpdate}
isMigration={isMigration} isMigration={isMigration}
dispatch={dispatch} dispatch={dispatch}
tableName={tableName} tableName={tableName}
currentSchema={currentSchema} currentSchema={schemaName}
clone={clone} clone={currentRow}
schemas={allSchemas} schemas={schemas}
migrationMode={migrationMode} migrationMode={!!migrationMode}
readOnlyMode={readOnlyMode} readOnlyMode={!!readOnlyMode}
count={count} count={0}
enumOptions={enumOptions} enumOptions={enumOptions}
currentSource={currentDataSource} currentSource={dataSourceName}
onClickSave={onClickSave} onClickSave={onClickSave}
onClickClear={onClickClear} onClickClear={onClickClear}
lastError={lastError} lastError={lastError}

View File

@ -45,6 +45,7 @@ const Alert = ({ lastError, lastSuccess }: AlertProps) => {
}; };
type TableInsertItemsProps = { type TableInsertItemsProps = {
isEnum: boolean;
tableName: string; tableName: string;
currentSchema: string; currentSchema: string;
clone: Record<string, unknown>; clone: Record<string, unknown>;
@ -66,6 +67,7 @@ type TableInsertItemsProps = {
}; };
export const TableInsertItems = ({ export const TableInsertItems = ({
isEnum,
tableName, tableName,
currentSchema, currentSchema,
clone, clone,
@ -161,7 +163,7 @@ export const TableInsertItems = ({
Clear Clear
</Button> </Button>
</div> </div>
{currentTable.is_enum ? ( {isEnum ? (
<ReloadEnumValuesButton dispatch={dispatch} /> <ReloadEnumValuesButton dispatch={dispatch} />
) : null} ) : null}
</div> </div>

View File

@ -0,0 +1,41 @@
import { getRunSqlQuery } from '@/components/Common/utils/v1QueryUtils';
import { dataSource } from '@/dataSources';
import Endpoints from '@/Endpoints';
import { useHttpClient } from '@/features/Network';
import { useQuery } from 'react-query';
export type UseTableRelationsType = {
dataSourceName: string;
schemaName: string;
};
export const useSchemas = ({
dataSourceName,
schemaName,
}: UseTableRelationsType) => {
const httpClient = useHttpClient();
return useQuery({
queryKey: ['tables-schema', schemaName, dataSourceName],
queryFn: async () => {
try {
// Will always fetch all tables on schema
const runSql = dataSource?.getFetchTablesListQuery({
schemas: [schemaName],
});
const url = Endpoints.query;
const query = getRunSqlQuery(runSql, dataSourceName, false, true);
const response = await httpClient.post(url, {
type: 'bulk',
source: dataSourceName,
args: [query],
});
return JSON.parse(response.data?.[0]?.result?.[1]?.[0]);
} catch (err: any) {
throw new Error(err);
}
},
refetchOnWindowFocus: false,
});
};

View File

@ -0,0 +1,81 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
import Endpoints from '@/Endpoints';
import { useHttpClient } from '@/features/Network';
import { useQuery, UseQueryResult } from 'react-query';
import { useTablesForeignKeys } from './useTableForeignKeys';
import { TableObject, ForeignKeyMapping } from '../types';
export type UseTableEnumOptionsProps = {
tables: TableObject[];
dataSourceName: string;
};
type UseTableEnumsResponseType = {
from: string;
to: string;
column: string[];
values: string[];
};
export type UseTableEnumsResponseArrayType = UseTableEnumsResponseType[];
export const useTableEnums = ({
tables,
dataSourceName,
}: UseTableEnumOptionsProps): UseQueryResult<UseTableEnumsResponseArrayType> => {
const httpClient = useHttpClient();
const { data: foreignKeys } = useTablesForeignKeys({
tables,
dataSourceName,
});
return useQuery({
queryKey: ['tables-enum', JSON.stringify(tables), dataSourceName],
queryFn: async () => {
try {
const enums = [];
if (!foreignKeys) return [];
/* eslint-disable no-await-in-loop */
for (const table of tables) {
const relation = foreignKeys.reduce(
(tally: ForeignKeyMapping | null, fk: ForeignKeyMapping[]) => {
const found = fk.find(f => f.to.table === table.name);
if (!found) return tally;
return found;
},
null
);
if (!relation) return [];
const body = {
type: 'select',
args: {
source: dataSourceName,
columns: relation.to.column,
table,
},
};
const url = Endpoints.query;
const response = await httpClient.post(url, JSON.stringify(body));
const column = Object.keys(response?.data?.[0])?.[0];
const values = response?.data?.map(
(v: Record<string, string>) => v[column]
);
enums.push({
from: relation.from.column[0],
to: relation.to.table,
column,
values,
});
}
return enums;
} catch (err: any) {
throw new Error(err);
}
},
enabled: !!foreignKeys,
refetchOnWindowFocus: true,
});
};

View File

@ -0,0 +1,41 @@
import { useQuery, UseQueryResult } from 'react-query';
import { DataSource } from '@/features/DataSource';
import { useHttpClient } from '@/features/Network';
import { TableObject, ForeignKeyMapping } from '../types';
export type UseTableForeignKeysProps = {
tables: TableObject[];
dataSourceName: string;
};
export const useTablesForeignKeys = ({
tables,
dataSourceName,
}: UseTableForeignKeysProps): UseQueryResult<ForeignKeyMapping[][]> => {
const httpClient = useHttpClient();
return useQuery({
queryKey: ['tables-fk', JSON.stringify(tables), dataSourceName],
queryFn: async () => {
try {
const foreignKeys = [];
/* eslint-disable no-await-in-loop */
for (const table of tables) {
const response = await DataSource(httpClient).getTableFkRelationships(
{
dataSourceName,
table,
}
);
foreignKeys.push(response);
}
return foreignKeys;
} catch (err: any) {
throw new Error(err);
}
},
refetchOnWindowFocus: false,
});
};

View File

@ -0,0 +1,14 @@
export type TableObject = {
name: string;
schema: string;
};
export type ForeignKeyMap = {
table: string;
column: string[];
};
export type ForeignKeyMapping = {
to: ForeignKeyMap;
from: ForeignKeyMap;
};

View File

@ -373,6 +373,7 @@ export const mergeLoadSchemaDataPostgres = (
Table['foreign_key_constraints'][0], Table['foreign_key_constraints'][0],
'is_table_tracked' | 'is_ref_table_tracked' 'is_table_tracked' | 'is_ref_table_tracked'
>[]; >[];
const primaryKeys = JSON.parse(data[2].result[1]) as Table['primary_key'][]; const primaryKeys = JSON.parse(data[2].result[1]) as Table['primary_key'][];
const uniqueKeys = JSON.parse(data[3].result[1]) as any; const uniqueKeys = JSON.parse(data[3].result[1]) as any;

View File

@ -1,6 +1,6 @@
import { SERVER_CONSOLE_MODE } from '@/constants'; import { SERVER_CONSOLE_MODE } from '@/constants';
import Endpoints from '@/Endpoints'; import Endpoints from '@/Endpoints';
import { useQuery, UseQueryOptions } from 'react-query'; import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { useAppSelector } from '../store'; import { useAppSelector } from '../store';
import { Api } from './apiUtils'; import { Api } from './apiUtils';
import { useConsoleConfig } from './useEnvVars'; import { useConsoleConfig } from './useEnvVars';
@ -12,7 +12,7 @@ export interface Config {
export function useMigrationMode( export function useMigrationMode(
queryOptions?: UseQueryOptions<{ migration_mode: boolean }, Error, boolean> queryOptions?: UseQueryOptions<{ migration_mode: boolean }, Error, boolean>
) { ): UseQueryResult<boolean> {
const headers = useAppSelector(s => s.tables.dataHeaders); const headers = useAppSelector(s => s.tables.dataHeaders);
const migrationUrl = Endpoints.hasuractlMigrateSettings; const migrationUrl = Endpoints.hasuractlMigrateSettings;
const { mode } = useConsoleConfig(); const { mode } = useConsoleConfig();