From a9515cdb68da6471cb1c43ab38af0c7d645eec4d Mon Sep 17 00:00:00 2001 From: Erik Magnusson <32518962+ejkkan@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:13:20 +0200 Subject: [PATCH] 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 --- .../Data/Common/Components/TableRow.tsx | 2 +- .../components/Services/Data/DataActions.js | 2 +- .../Services/Data/TableBrowseRows/ViewRows.js | 24 +-- .../Data/TableInsertItem/InsertActions.js | 11 +- .../TableInsertItemContainer.tsx | 152 +++++++++++++----- .../Data/TableInsertItem/TableInsertItems.tsx | 4 +- .../Data/TableInsertItem/hooks/useSchemas.ts | 41 +++++ .../TableInsertItem/hooks/useTableEnums.ts | 81 ++++++++++ .../hooks/useTableForeignKeys.ts | 41 +++++ .../Services/Data/TableInsertItem/types.ts | 14 ++ .../src/components/Services/Data/mergeData.ts | 1 + console/src/hooks/useConfig.ts | 4 +- 12 files changed, 310 insertions(+), 67 deletions(-) create mode 100644 console/src/components/Services/Data/TableInsertItem/hooks/useSchemas.ts create mode 100644 console/src/components/Services/Data/TableInsertItem/hooks/useTableEnums.ts create mode 100644 console/src/components/Services/Data/TableInsertItem/hooks/useTableForeignKeys.ts create mode 100644 console/src/components/Services/Data/TableInsertItem/types.ts diff --git a/console/src/components/Services/Data/Common/Components/TableRow.tsx b/console/src/components/Services/Data/Common/Components/TableRow.tsx index 91bdcf7084b..56a91b86a97 100644 --- a/console/src/components/Services/Data/Common/Components/TableRow.tsx +++ b/console/src/components/Services/Data/Common/Components/TableRow.tsx @@ -70,7 +70,7 @@ export interface TableRowProps { refName: 'valueNode' | 'nullNode' | 'defaultNode' | 'radioNode', node: HTMLInputElement | null ) => void; - enumOptions: Record; + enumOptions: string[]; index: string; clone?: Record; onChange?: (e: React.ChangeEvent, val: unknown) => void; diff --git a/console/src/components/Services/Data/DataActions.js b/console/src/components/Services/Data/DataActions.js index 8310ab8006d..5506392102d 100644 --- a/console/src/components/Services/Data/DataActions.js +++ b/console/src/components/Services/Data/DataActions.js @@ -178,6 +178,7 @@ const loadSchema = (configOptions = {}) => { ) ); } + const body = { type: 'bulk', source, @@ -223,7 +224,6 @@ const loadSchema = (configOptions = {}) => { return dispatch(requestAction(url, options)).then( data => { if (!data || !data[0] || !data[0].result) return; - let mergedData = []; switch (currentDriver) { case 'postgres': diff --git a/console/src/components/Services/Data/TableBrowseRows/ViewRows.js b/console/src/components/Services/Data/TableBrowseRows/ViewRows.js index bdc2be73ffc..47fd13601b9 100644 --- a/console/src/components/Services/Data/TableBrowseRows/ViewRows.js +++ b/console/src/components/Services/Data/TableBrowseRows/ViewRows.js @@ -49,7 +49,6 @@ import { ordinalColSort } from '../utils'; import Spinner from '../../../Common/Spinner/Spinner'; import { E_SET_EDITITEM } from '../TableEditItem/EditActions'; -import { I_SET_CLONE } from '../TableInsertItem/InsertActions'; import { getTableInsertRowRoute, getTableEditRowRoute, @@ -70,6 +69,8 @@ import { getPersistedColumnsOrder, } from './tableUtils'; import { compareRows, isTableWithPK } from './utils'; +import { push } from 'react-router-redux'; +import globals from '@/Globals'; const ViewRows = props => { const { @@ -412,16 +413,19 @@ const ViewRows = props => { const cloneIcon = ; const handleCloneClick = () => { - dispatch({ type: I_SET_CLONE, clone: row }); + const urlPrefix = globals.urlPrefix; dispatch( - _push( - getTableInsertRowRoute( - currentSchema, - currentSource, - curTableName, - true - ) - ) + push({ + pathname: + urlPrefix + + getTableInsertRowRoute( + currentSchema, + currentSource, + curTableName, + true + ), + state: { row }, + }) ); }; diff --git a/console/src/components/Services/Data/TableInsertItem/InsertActions.js b/console/src/components/Services/Data/TableInsertItem/InsertActions.js index edf5085f54c..b6f022e74e8 100644 --- a/console/src/components/Services/Data/TableInsertItem/InsertActions.js +++ b/console/src/components/Services/Data/TableInsertItem/InsertActions.js @@ -23,7 +23,6 @@ import { removeAll } from 'react-notification-system-redux'; import { getNotificationDetails } from '../../Common/Notification'; import { getTableConfiguration } from '../TableBrowseRows/utils'; -const I_SET_CLONE = 'InsertItem/I_SET_CLONE'; const I_RESET = 'InsertItem/I_RESET'; const I_ONGOING_REQ = 'InsertItem/I_ONGOING_REQ'; const I_REQUEST_SUCCESS = 'InsertItem/I_REQUEST_SUCCESS'; @@ -302,14 +301,6 @@ const insertReducer = (tableName, state, action) => { lastSuccess: null, enumOptions: null, }; - case I_SET_CLONE: - return { - clone: action.clone, - ongoingRequest: false, - lastError: null, - lastSuccess: null, - enumOptions: null, - }; case I_ONGOING_REQ: return { ...state, @@ -360,4 +351,4 @@ const insertReducer = (tableName, state, action) => { }; export default insertReducer; -export { fetchEnumOptions, insertItem, I_SET_CLONE, I_RESET, Open, Close }; +export { fetchEnumOptions, insertItem, I_RESET, Open, Close }; diff --git a/console/src/components/Services/Data/TableInsertItem/TableInsertItemContainer.tsx b/console/src/components/Services/Data/TableInsertItem/TableInsertItemContainer.tsx index 780d153d86c..1ea3b125eb4 100644 --- a/console/src/components/Services/Data/TableInsertItem/TableInsertItemContainer.tsx +++ b/console/src/components/Services/Data/TableInsertItem/TableInsertItemContainer.tsx @@ -1,10 +1,18 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useAppDispatch, useAppSelector } from '@/store'; -import { setTable } from '../DataActions'; -import { fetchEnumOptions, I_RESET, insertItem } from './InsertActions'; +import { useMigrationMode, useReadOnlyMode } from '@/hooks'; +import { useMetadata } from '@/features/MetadataAPI'; +import { HasuraMetadataV3 } from '@/metadata/types'; +import { insertItem } from './InsertActions'; import { ColumnName, RowValues } from '../TableCommon/DataTableRowItem.types'; import { DataTableRowItemProps } from '../TableCommon/DataTableRowItem'; import { TableInsertItems } from './TableInsertItems'; +import { + useTableEnums, + UseTableEnumsResponseArrayType, +} from './hooks/useTableEnums'; +import { useSchemas } from './hooks/useSchemas'; +import { TableObject } from './types'; type GetButtonTextArgs = { insertedRows: number; @@ -23,25 +31,93 @@ const getButtonText = ({ insertedRows, ongoingRequest }: GetButtonTextArgs) => { 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 = { params: { schema: string; source: string; table: string; }; + router: { location: { state: any } }; }; export const TableInsertItemContainer = ( 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 [isMigration, setIsMigration] = useState(false); const [insertedRows, setInsertedRows] = useState(0); const [values, setValues] = useState>({}); + 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'] = ( columnName, rowValues @@ -59,38 +135,9 @@ export const TableInsertItemContainer = ( setValues(newValues); }; - useEffect(() => { - dispatch(setTable(tableName)); - dispatch(fetchEnumOptions()); - - return () => { - dispatch({ type: I_RESET }); - }; - }, [tableName]); - - const nextInsert = () => - setInsertedRows(prevInsertedRows => prevInsertedRows + 1); - const toggleMigrationCheckBox = () => 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 form = document.getElementById('insertForm'); 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 => { e.preventDefault(); const inputValues = Object.keys(values).reduce< @@ -133,7 +196,7 @@ export const TableInsertItemContainer = ( dispatch( insertItem( tableName, - clone ? { ...clone, ...inputValues } : inputValues, + currentRow ? { ...currentRow, ...inputValues } : inputValues, isMigration ) ).then(() => { @@ -141,21 +204,26 @@ export const TableInsertItemContainer = ( }); }; + // --- Insert section end --- + + if (schemasIsLoading) return

Loading...

; + return ( { }; type TableInsertItemsProps = { + isEnum: boolean; tableName: string; currentSchema: string; clone: Record; @@ -66,6 +67,7 @@ type TableInsertItemsProps = { }; export const TableInsertItems = ({ + isEnum, tableName, currentSchema, clone, @@ -161,7 +163,7 @@ export const TableInsertItems = ({ Clear - {currentTable.is_enum ? ( + {isEnum ? ( ) : null} diff --git a/console/src/components/Services/Data/TableInsertItem/hooks/useSchemas.ts b/console/src/components/Services/Data/TableInsertItem/hooks/useSchemas.ts new file mode 100644 index 00000000000..9530c2ac285 --- /dev/null +++ b/console/src/components/Services/Data/TableInsertItem/hooks/useSchemas.ts @@ -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, + }); +}; diff --git a/console/src/components/Services/Data/TableInsertItem/hooks/useTableEnums.ts b/console/src/components/Services/Data/TableInsertItem/hooks/useTableEnums.ts new file mode 100644 index 00000000000..3a90e73d9e6 --- /dev/null +++ b/console/src/components/Services/Data/TableInsertItem/hooks/useTableEnums.ts @@ -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 => { + 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) => 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, + }); +}; diff --git a/console/src/components/Services/Data/TableInsertItem/hooks/useTableForeignKeys.ts b/console/src/components/Services/Data/TableInsertItem/hooks/useTableForeignKeys.ts new file mode 100644 index 00000000000..85914d1aae5 --- /dev/null +++ b/console/src/components/Services/Data/TableInsertItem/hooks/useTableForeignKeys.ts @@ -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 => { + 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, + }); +}; diff --git a/console/src/components/Services/Data/TableInsertItem/types.ts b/console/src/components/Services/Data/TableInsertItem/types.ts new file mode 100644 index 00000000000..1294cc2bcc7 --- /dev/null +++ b/console/src/components/Services/Data/TableInsertItem/types.ts @@ -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; +}; diff --git a/console/src/components/Services/Data/mergeData.ts b/console/src/components/Services/Data/mergeData.ts index 2e68e647f5a..a03be17f8a3 100644 --- a/console/src/components/Services/Data/mergeData.ts +++ b/console/src/components/Services/Data/mergeData.ts @@ -373,6 +373,7 @@ export const mergeLoadSchemaDataPostgres = ( Table['foreign_key_constraints'][0], 'is_table_tracked' | 'is_ref_table_tracked' >[]; + const primaryKeys = JSON.parse(data[2].result[1]) as Table['primary_key'][]; const uniqueKeys = JSON.parse(data[3].result[1]) as any; diff --git a/console/src/hooks/useConfig.ts b/console/src/hooks/useConfig.ts index 05dd8fcc997..78820826065 100644 --- a/console/src/hooks/useConfig.ts +++ b/console/src/hooks/useConfig.ts @@ -1,6 +1,6 @@ import { SERVER_CONSOLE_MODE } from '@/constants'; import Endpoints from '@/Endpoints'; -import { useQuery, UseQueryOptions } from 'react-query'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; import { useAppSelector } from '../store'; import { Api } from './apiUtils'; import { useConsoleConfig } from './useEnvVars'; @@ -12,7 +12,7 @@ export interface Config { export function useMigrationMode( queryOptions?: UseQueryOptions<{ migration_mode: boolean }, Error, boolean> -) { +): UseQueryResult { const headers = useAppSelector(s => s.tables.dataHeaders); const migrationUrl = Endpoints.hasuractlMigrateSettings; const { mode } = useConsoleConfig();