From 2298c21ae08750cb59736f2e7a04c734ba2c82fa Mon Sep 17 00:00:00 2001 From: Vijay Prasanna Date: Tue, 18 Oct 2022 10:26:22 +0530 Subject: [PATCH] feature (console): add storybook components for local relationships for GDC PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6290 GitOrigin-RevId: aadac5dc29db2d1fa6550a604e04a6baa3b7c661 --- .../components/DataGrid/QueryDialog.tsx | 5 + .../features/Data/ManageTable/ManageTable.tsx | 4 +- .../Data/hooks/useDatabaseHierarchy.ts | 4 +- .../ManualLocalRelationshipWidget.stories.tsx | 30 +++ .../ManualLocalRelationshipWidget.tsx | 105 +++++++++ .../__mocks__/localrelationships.mock.ts | 200 ++++++++++++++++++ .../hooks/useCreateManualLocalRelationship.ts | 88 ++++++++ .../hooks/useGetTargetOptions.test.ts | 98 +++++++++ .../hooks/useGetTargetOptions.ts | 35 +++ .../hooks/useTableColumns.ts | 27 +++ .../parts/LinkBlockHorizontal.tsx | 20 ++ .../parts/LinkBlockVertical.tsx | 24 +++ .../parts/MapColumns.tsx | 148 +++++++++++++ .../parts/Name.tsx | 13 ++ .../parts/RelationshipType.tsx | 23 ++ .../parts/SourceTable.tsx | 58 +++++ .../parts/TargetTable.stories.tsx | 47 ++++ .../parts/TargetTable.tsx | 68 ++++++ .../ManualLocalRelationshipWidget/schema.ts | 34 +++ .../src/features/DataSource/common/utils.tsx | 6 +- .../gdc/introspection/getTableColumns.ts | 2 +- .../stories/mocks/handlers.mock.ts | 29 ++- .../DatabaseRelationshipTable.tsx | 15 +- .../__test__/mocks.ts | 2 +- .../__test__/utils.spec.ts | 6 +- .../useListAllRelationshipsFromMetadata.ts | 12 +- .../utils.ts | 14 +- console/src/new-components/Form/Form.tsx | 2 +- console/src/new-components/Form/Select.tsx | 2 +- 29 files changed, 1087 insertions(+), 34 deletions(-) create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.stories.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/__mocks__/localrelationships.mock.ts create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useCreateManualLocalRelationship.ts create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.test.ts create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.ts create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useTableColumns.ts create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockHorizontal.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockVertical.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/MapColumns.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/Name.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/RelationshipType.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/SourceTable.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.stories.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.tsx create mode 100644 console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/schema.ts diff --git a/console/src/features/BrowseRows/components/DataGrid/QueryDialog.tsx b/console/src/features/BrowseRows/components/DataGrid/QueryDialog.tsx index ba316be9288..b5dbffbbcbd 100644 --- a/console/src/features/BrowseRows/components/DataGrid/QueryDialog.tsx +++ b/console/src/features/BrowseRows/components/DataGrid/QueryDialog.tsx @@ -144,3 +144,8 @@ export const QueryDialog = ({ ); }; + +QueryDialog.defaultProps = { + filters: [], + sorts: [], +}; diff --git a/console/src/features/Data/ManageTable/ManageTable.tsx b/console/src/features/Data/ManageTable/ManageTable.tsx index 7a4ab8ffb20..66ee5064aaf 100644 --- a/console/src/features/Data/ManageTable/ManageTable.tsx +++ b/console/src/features/Data/ManageTable/ManageTable.tsx @@ -102,8 +102,8 @@ export const ManageTable = (props: ManageTableProps) => { ], ]} > -
-

+
+

{tableName}

diff --git a/console/src/features/Data/hooks/useDatabaseHierarchy.ts b/console/src/features/Data/hooks/useDatabaseHierarchy.ts index 7a3fb46def5..5c033002dbd 100644 --- a/console/src/features/Data/hooks/useDatabaseHierarchy.ts +++ b/console/src/features/Data/hooks/useDatabaseHierarchy.ts @@ -1,10 +1,11 @@ import { DataSource } from '@/features/DataSource'; import { useHttpClient } from '@/features/Network'; +import { AxiosError } from 'axios'; import { useQuery } from 'react-query'; export const useDatabaseHierarchy = (dataSourceName: string) => { const httpClient = useHttpClient(); - return useQuery({ + return useQuery({ queryKey: [dataSourceName, 'hierarchy'], queryFn: async () => { const hierarcy = await DataSource(httpClient).getDatabaseHierarchy({ @@ -12,5 +13,6 @@ export const useDatabaseHierarchy = (dataSourceName: string) => { }); return hierarcy; }, + refetchOnWindowFocus: false, }); }; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.stories.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.stories.tsx new file mode 100644 index 00000000000..07fbf75bc2a --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.stories.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import { Story, Meta } from '@storybook/react'; +import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; +import { ManualLocalRelationshipWidget } from './ManualLocalRelationshipWidget'; +import { handlers } from './__mocks__/localrelationships.mock'; + +export default { + title: 'Relationships/Manual Relationship 🧬', + component: ManualLocalRelationshipWidget, + decorators: [ReactQueryDecorator()], + parameters: { + msw: handlers(), + }, +} as Meta; + +export const Primary: Story = () => { + const [formState, updateFormState] = useState(''); + return ( +
+ { + updateFormState('success'); + }} + /> +
{formState}
+
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.tsx new file mode 100644 index 00000000000..97137bd8112 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/ManualLocalRelationshipWidget.tsx @@ -0,0 +1,105 @@ +import { + ManualArrayRelationship, + ManualObjectRelationship, + Table, +} from '@/features/MetadataAPI'; +import { Button } from '@/new-components/Button'; +import { UpdatedForm } from '@/new-components/Form'; +import React from 'react'; +import { schema } from './schema'; +import { MapColumns } from './parts/MapColumns'; +import { Name } from './parts/Name'; +import { RelationshipType } from './parts/RelationshipType'; +import { SourceTable } from './parts/SourceTable'; +import { TargetTable } from './parts/TargetTable'; +import { useCreateManualLocalRelationship } from './hooks/useCreateManualLocalRelationship'; +import { LinkBlockHorizontal } from './parts/LinkBlockHorizontal'; +import { LinkBlockVertical } from './parts/LinkBlockVertical'; + +export type ManualLocalRelationshipWidget = { + dataSourceName: string; + table: Table; + + onSuccess: () => void; + + existingRelationship?: ManualArrayRelationship | ManualObjectRelationship; +}; + +export const ManualLocalRelationshipWidget = ( + props: ManualLocalRelationshipWidget +) => { + // Get current relationship if any for this table + const { dataSourceName, table, onSuccess } = props; + + const { createManualLocalRelationship, isLoading: isSaving } = + useCreateManualLocalRelationship({ onSuccess }); + + return ( + { + createManualLocalRelationship({ + relationshipName: values.name, + relationshipType: values.relationship_type, + fromSource: values.source_name, + fromTable: values.source_table, + toTable: values.target_table, + columnMapping: values.column_mapping, + }); + }} + options={{ + defaultValues: { + name: '', + relationship_type: 'object', + source_name: dataSourceName, + source_table: table, + target_name: dataSourceName, + column_mapping: [{}], + }, + }} + > + {() => ( + <> +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+
+
+ + + + + + + + )} +
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/__mocks__/localrelationships.mock.ts b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/__mocks__/localrelationships.mock.ts new file mode 100644 index 00000000000..f0c1bd52915 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/__mocks__/localrelationships.mock.ts @@ -0,0 +1,200 @@ +import { rest } from 'msw'; +import { Metadata } from '@/features/MetadataAPI'; +import { TableInfo } from '@hasura/dc-api-types'; + +export const schemaList = { + result_type: 'TuplesOk', + result: [['schema_name'], ['public'], ['default']], +}; + +const metadata: Metadata = { + resource_version: 1, + metadata: { + version: 3, + sources: [ + { + name: 'sqlite_test', + kind: 'sqlite', + tables: [{ table: ['Album'] }, { table: ['Artist'] }], + configuration: { + foo: 'bar', + }, + }, + ], + }, +}; + +const AlbumTableResponse: TableInfo = { + name: ['Album'], + columns: [ + { + name: 'AlbumId', + type: 'number', + nullable: false, + }, + { + name: 'Title', + type: 'string', + nullable: false, + }, + { + name: 'ArtistId', + type: 'number', + nullable: false, + }, + ], + primary_key: ['AlbumId'], + foreign_keys: { + 'ArtistId->Artist.ArtistId': { + foreign_table: ['Artist'], + column_mapping: { + ArtistId: 'ArtistId', + }, + }, + }, + description: + 'CREATE TABLE [Album]\n(\n [AlbumId] INTEGER NOT NULL,\n [Title] NVARCHAR(160) NOT NULL,\n [ArtistId] INTEGER NOT NULL,\n CONSTRAINT [PK_Album] PRIMARY KEY ([AlbumId]),\n FOREIGN KEY ([ArtistId]) REFERENCES [Artist] ([ArtistId]) \n\t\tON DELETE NO ACTION ON UPDATE NO ACTION\n)', +}; + +const ArtistTableRespone: TableInfo = { + name: ['Artist'], + columns: [ + { + name: 'ArtistId', + type: 'number', + nullable: false, + }, + { + name: 'Name', + type: 'string', + nullable: true, + }, + ], + primary_key: ['ArtistId'], + description: + 'CREATE TABLE [Artist]\n(\n [ArtistId] INTEGER NOT NULL,\n [Name] NVARCHAR(120),\n CONSTRAINT [PK_Artist] PRIMARY KEY ([ArtistId])\n)', +}; + +export const introspectionQueryResponse = { + data: { + __schema: { + types: [ + { + kind: 'OBJECT', + name: 'Album', + description: + 'CREATE TABLE [Album]\n(\n [AlbumId] INTEGER NOT NULL,\n [Title] NVARCHAR(160) NOT NULL,\n [ArtistId] INTEGER NOT NULL,\n CONSTRAINT [PK_Album] PRIMARY KEY ([AlbumId]),\n FOREIGN KEY ([ArtistId]) REFERENCES [Artist] ([ArtistId]) \n\t\tON DELETE NO ACTION ON UPDATE NO ACTION\n)', + fields: [ + { + name: 'AlbumId', + description: null, + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'decimal', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'ArtistId', + description: null, + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'decimal', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'Title', + description: null, + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + ], + inputFields: null, + interfaces: [], + enumValues: null, + possibleTypes: null, + }, + { + kind: 'OBJECT', + name: 'Artist', + description: + 'CREATE TABLE [Artist]\n(\n [ArtistId] INTEGER NOT NULL,\n [Name] NVARCHAR(120),\n CONSTRAINT [PK_Artist] PRIMARY KEY ([ArtistId])\n)', + fields: [ + { + name: 'ArtistId', + description: null, + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'decimal', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'Name', + description: null, + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, + ], + inputFields: null, + interfaces: [], + enumValues: null, + possibleTypes: null, + }, + ], + }, + }, +}; + +export const handlers = (url = 'http://localhost:8080') => [ + rest.post(`${url}/v1/metadata`, async (_req, res, ctx) => { + const body = await _req.json(); + if (body.type === 'get_table_info' && body.args.table.includes('Album')) + return res(ctx.json(AlbumTableResponse)); + if (body.type === 'get_table_info' && body.args.table.includes('Artist')) + return res(ctx.json(ArtistTableRespone)); + return res(ctx.json(metadata)); + }), + rest.post(`${url}/v1/graphql`, async (_req, res, ctx) => { + const body = await _req.json(); + if (body.operationName === 'IntrospectionQuery') + return res(ctx.json(introspectionQueryResponse)); + return res(ctx.json({})); + }), +]; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useCreateManualLocalRelationship.ts b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useCreateManualLocalRelationship.ts new file mode 100644 index 00000000000..c79a0865e6e --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useCreateManualLocalRelationship.ts @@ -0,0 +1,88 @@ +import { exportMetadata } from '@/features/DataSource'; +import { Table, useMetadataMigration } from '@/features/MetadataAPI'; +import { useHttpClient } from '@/features/Network'; +import { useFireNotification } from '@/new-components/Notifications'; +import { useCallback } from 'react'; + +type CreateManualLocalRelationshipPayload = { + relationshipName: string; + relationshipType: 'object' | 'array'; + fromTable: Table; + fromSource: string; + toTable: Table; + columnMapping: { from: string; to: string }[]; +}; + +export const useCreateManualLocalRelationship = (props: { + onSuccess?: () => void; +}) => { + const { mutate, ...rest } = useMetadataMigration(); + const httpClient = useHttpClient(); + const { fireNotification } = useFireNotification(); + const createManualLocalRelationship = useCallback( + async (values: CreateManualLocalRelationshipPayload) => { + const { metadata, resource_version } = await exportMetadata({ + httpClient, + }); + + if (!metadata) throw Error('Unable to fetch metadata'); + + const metadataSource = metadata.sources.find( + s => s.name === values.fromSource + ); + + if (!metadataSource) throw Error('Unable to fetch metadata source'); + + const driver = metadataSource.kind; + + const type = + values.relationshipType === 'object' + ? 'create_object_relationship' + : 'create_array_relationship'; + + mutate( + { + query: { + resource_version, + type: `${driver}_${type}`, + args: { + table: values.fromTable, + source: values.fromSource, + name: values.relationshipName, + using: { + manual_configuration: { + remote_table: values.toTable, + column_mapping: values.columnMapping.reduce( + (acc, val) => ({ ...acc, [val.from]: val.to }), + {} + ), + }, + }, + }, + }, + }, + { + onSuccess: () => { + props.onSuccess?.(); + + fireNotification({ + type: 'success', + title: 'Success!', + message: 'A relationship was added to Hasura succesfull!', + }); + }, + onError: err => { + fireNotification({ + type: 'error', + title: 'failed to run SQL statement', + message: err?.message, + }); + }, + } + ); + }, + [fireNotification, httpClient, mutate, props] + ); + + return { createManualLocalRelationship, ...rest }; +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.test.ts b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.test.ts new file mode 100644 index 00000000000..fa80c37531f --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.test.ts @@ -0,0 +1,98 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { wrapper } from '../../../../../hooks/__tests__/common/decorator'; +import { Metadata, Table } from '../../../../MetadataAPI'; +import { useGetTargetOptions } from './useGetTargetOptions'; + +describe('useGetTargetOptions', () => { + const mockMetadata: Metadata = { + resource_version: 54, + metadata: { + version: 3, + sources: [ + { + name: 'chinook', + kind: 'postgres', + tables: [ + { + table: { + name: 'Album', + schema: 'public', + }, + }, + { + table: { + name: 'Artist', + schema: 'public', + }, + }, + ], + configuration: { + connection_info: { + database_url: + 'postgres://postgres:test@host.docker.internal:6001/chinook', + isolation_level: 'read-committed', + use_prepared_statements: false, + }, + }, + }, + ], + }, + }; + + const server = setupServer( + rest.post('http://localhost/v1/metadata', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(mockMetadata)); + }) + ); + + beforeAll(() => { + server.listen(); + }); + afterAll(() => { + server.close(); + }); + + it('when invoked, fetches all tables when no option excludeTable options is provided', async () => { + const { result, waitFor } = renderHook( + () => useGetTargetOptions('chinook'), + { wrapper } + ); + + const expectedResult: Table[] = [ + { + name: 'Album', + schema: 'public', + }, + { + name: 'Artist', + schema: 'public', + }, + ]; + + await waitFor(() => result.current.isSuccess); + expect(result.current.data).toEqual(expectedResult); + }); + + it('when invoked, fetches the filtered list of tables when an option excludeTable options is provided', async () => { + const { result, waitFor } = renderHook( + () => + useGetTargetOptions('chinook', { + name: 'Album', + schema: 'public', + }), + { wrapper } + ); + + const expectedResult: Table[] = [ + { + name: 'Artist', + schema: 'public', + }, + ]; + + await waitFor(() => result.current.isSuccess); + expect(result.current.data).toEqual(expectedResult); + }); +}); diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.ts b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.ts new file mode 100644 index 00000000000..fffe787f1a5 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useGetTargetOptions.ts @@ -0,0 +1,35 @@ +import { useHttpClient } from '@/features/Network'; +import { areTablesEqual } from '@/features/RelationshipsTable'; +import { exportMetadata } from '@/features/DataSource'; +import { useQuery } from 'react-query'; +import { Table } from '@/features/MetadataAPI'; +import { AxiosError } from 'axios'; + +export const useGetTargetOptions = ( + dataSourceName: string, + excludeTable?: Table +) => { + const httpClient = useHttpClient(); + + return useQuery({ + queryKey: ['local-db-relationships-target-options', dataSourceName], + queryFn: async () => { + const { metadata } = await exportMetadata({ httpClient }); + + if (!metadata) throw Error('Unable to fetch sources from metadata'); + + const metadataSource = metadata.sources.find( + source => source.name === dataSourceName + ); + + if (!metadataSource) throw Error('Unable to find the source in metadata'); + + const tables = metadataSource.tables.map(t => t.table); + if (excludeTable) + return tables.filter(t => !areTablesEqual(t, excludeTable)); + + return tables; + }, + refetchOnWindowFocus: false, + }); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useTableColumns.ts b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useTableColumns.ts new file mode 100644 index 00000000000..bfc748de4ca --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/hooks/useTableColumns.ts @@ -0,0 +1,27 @@ +import { useHttpClient } from '@/features/Network'; +import { useQuery } from 'react-query'; +import { DataSource, TableColumn } from '@/features/DataSource'; +import { Table } from '@/features/MetadataAPI'; +import { AxiosError } from 'axios'; + +export const useTableColumns = ({ + dataSourceName, + table, +}: { + dataSourceName: string; + table: Table; +}) => { + const httpClient = useHttpClient(); + return useQuery({ + queryKey: ['tableColumns', dataSourceName, table], + queryFn: () => { + const columns = DataSource(httpClient).getTableColumns({ + dataSourceName, + table, + }); + return columns; + }, + refetchOnWindowFocus: false, + enabled: !!table && !!dataSourceName, + }); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockHorizontal.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockHorizontal.tsx new file mode 100644 index 00000000000..0c85976177e --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockHorizontal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { FaLink } from 'react-icons/fa'; + +export const LinkBlockHorizontal = () => { + return ( +
+
+ +
+
+
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockVertical.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockVertical.tsx new file mode 100644 index 00000000000..896e4594e3e --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/LinkBlockVertical.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { FaLink } from 'react-icons/fa'; + +export const LinkBlockVertical = ({ title }: { title: string }) => { + return ( +
+
+ +
+

{title}

+
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/MapColumns.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/MapColumns.tsx new file mode 100644 index 00000000000..b27e04687c0 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/MapColumns.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; +import { FieldError, useFieldArray, useFormContext } from 'react-hook-form'; +import { Select } from '@/new-components/Form'; +import { FaArrowRight, FaCircle, FaTrashAlt } from 'react-icons/fa'; +import { Button } from '@/new-components/Button'; +import { IndicatorCard } from '@/new-components/IndicatorCard'; +import { Schema } from '../schema'; +import { useTableColumns } from '../hooks/useTableColumns'; + +export const MapColumns = () => { + const { fields, append } = useFieldArray({ name: 'column_mapping' }); + + const { + watch, + setValue, + formState: { errors }, + } = useFormContext(); + const formErrorMessage = (errors?.column_mapping as unknown as FieldError) + ?.message; + const sourceTable = watch('source_table'); + const sourceDataSourceName = watch('source_name'); + const targetTable = watch('target_table'); + const targetDataSourceName = watch('target_name'); + + const columnMappings = watch('column_mapping'); + const { + data: sourceTableColumns, + isLoading: areSourceColumnsLoading, + error: sourceColumnsFetchError, + } = useTableColumns({ + dataSourceName: sourceDataSourceName, + table: sourceTable, + }); + + const { + data: targetTableColumns, + isLoading: areTargetColumnsLoading, + error: targetColumnsFetchError, + } = useTableColumns({ + dataSourceName: targetDataSourceName, + // This condition is wait for targetTable to get set/unset, until then `undefined` is passed to react-query, which will stop the + // unnessacary call from being made + table: targetTable ? JSON.parse(targetTable) : undefined, + }); + + if (sourceColumnsFetchError || targetColumnsFetchError) + return ( +
+
+ +
    + {!!sourceColumnsFetchError && ( +
  • + source table error:{' '} + {JSON.stringify(sourceColumnsFetchError.response?.data)} +
  • + )} + {!!targetColumnsFetchError && ( +
  • + target table error: + {JSON.stringify(targetColumnsFetchError.response?.data)} +
  • + )} +
+
+
+
+ ); + + return ( +
+
+
+ Source Column +
+
+ Reference Column +
+
+
{formErrorMessage}
+ {fields.map((field, index) => { + return ( +
+
+ {areSourceColumnsLoading ? ( + + ) : ( + ({ + label: column.name, + value: column.name, + }))} + name={`column_mapping.${index}.to`} + placeholder="Select reference column" + noErrorPlaceholder={false} + /> + )} +
+
+
+
+ ); + })} +
+ +
+
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/Name.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/Name.tsx new file mode 100644 index 00000000000..5a58e8c1785 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/Name.tsx @@ -0,0 +1,13 @@ +import { InputField } from '@/new-components/Form'; +import React from 'react'; + +export const Name = () => { + return ( + + ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/RelationshipType.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/RelationshipType.tsx new file mode 100644 index 00000000000..4341baf1959 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/RelationshipType.tsx @@ -0,0 +1,23 @@ +import { Select } from '@/new-components/Form'; +import React from 'react'; + +export const RelationshipType = () => { + return ( + ({ + value: t, + label: getTableName(t, databaseHierarchy ?? []), + }))} + label="Table" + labelIcon={} + disabled + /> +
+ ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.stories.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.stories.tsx new file mode 100644 index 00000000000..897f0333b77 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.stories.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Story, Meta } from '@storybook/react'; +import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; +import { z } from 'zod'; +import { UpdatedForm } from '@/new-components/Form'; +import { TargetTable } from './TargetTable'; +import { handlers } from '../__mocks__/localrelationships.mock'; + +export default { + title: 'Relationships/TargetTable 🧬', + component: TargetTable, + decorators: [ReactQueryDecorator()], + parameters: { + msw: handlers(), + }, +} as Meta; + +export const Basic: Story = () => { + const [formState, updateFormState] = useState(''); + return ( + <> + { + try { + return JSON.parse(value); + } catch { + return null; + } + }), + })} + onSubmit={data => { + updateFormState(data); + }} + options={{ + defaultValues: { + target_name: 'sqlite_test', + }, + }} + > + {() => } + +
{formState}
+ + ); +}; diff --git a/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.tsx b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.tsx new file mode 100644 index 00000000000..e7f45456755 --- /dev/null +++ b/console/src/features/DataRelationships/components/ManualLocalRelationshipWidget/parts/TargetTable.tsx @@ -0,0 +1,68 @@ +import { useDatabaseHierarchy, getTableName } from '@/features/Data'; +import { Table } from '@/features/MetadataAPI'; +import { InputField, Select } from '@/new-components/Form'; +import React from 'react'; +import { useWatch } from 'react-hook-form'; +import { FaDatabase, FaTable } from 'react-icons/fa'; +import Skeleton from 'react-loading-skeleton'; +import { useGetTargetOptions } from '../hooks/useGetTargetOptions'; + +export const TargetTable = () => { + const table = useWatch>({ name: 'source_table' }); + const dataSourceName = useWatch>({ + name: 'target_name', + }); + const { + data: databaseHierarchy, + isLoading: isDatabaseHierarchyLoading, + error: databaseHierarchyError, + } = useDatabaseHierarchy(dataSourceName); + const { + data: options, + isLoading: areTargetOptionsLoading, + error: targetOptionError, + } = useGetTargetOptions(dataSourceName, table); + + if (databaseHierarchyError) + return ( +
+ {JSON.stringify(databaseHierarchyError.response?.data)} +
+ ); + + if (targetOptionError) + return ( +
+ {JSON.stringify(targetOptionError.response?.data)} +
+ ); + + if (areTargetOptionsLoading || isDatabaseHierarchyLoading) + return ( +
+
+ +
+
+ ); + + return ( +
+ } + disabled + /> +