mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
feature (console): add storybook components for local relationships for GDC
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6290 GitOrigin-RevId: aadac5dc29db2d1fa6550a604e04a6baa3b7c661
This commit is contained in:
parent
5c51ff4288
commit
2298c21ae0
@ -144,3 +144,8 @@ export const QueryDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QueryDialog.defaultProps = {
|
||||||
|
filters: [],
|
||||||
|
sorts: [],
|
||||||
|
};
|
||||||
|
@ -102,8 +102,8 @@ export const ManageTable = (props: ManageTableProps) => {
|
|||||||
],
|
],
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="flex gap-0.5 items-center">
|
<div className="flex items-center">
|
||||||
<h1 className="inline-flex items-center text-xl font-semibold mb-1">
|
<h1 className="inline-flex items-center text-xl font-semibold mb-1 pr-xs">
|
||||||
{tableName}
|
{tableName}
|
||||||
</h1>
|
</h1>
|
||||||
<FaChevronDown className="text-gray-400 text-sm transition-transform group-radix-state-open:rotate-180" />
|
<FaChevronDown className="text-gray-400 text-sm transition-transform group-radix-state-open:rotate-180" />
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { DataSource } from '@/features/DataSource';
|
import { DataSource } from '@/features/DataSource';
|
||||||
import { useHttpClient } from '@/features/Network';
|
import { useHttpClient } from '@/features/Network';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
export const useDatabaseHierarchy = (dataSourceName: string) => {
|
export const useDatabaseHierarchy = (dataSourceName: string) => {
|
||||||
const httpClient = useHttpClient();
|
const httpClient = useHttpClient();
|
||||||
return useQuery({
|
return useQuery<string[], AxiosError>({
|
||||||
queryKey: [dataSourceName, 'hierarchy'],
|
queryKey: [dataSourceName, 'hierarchy'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const hierarcy = await DataSource(httpClient).getDatabaseHierarchy({
|
const hierarcy = await DataSource(httpClient).getDatabaseHierarchy({
|
||||||
@ -12,5 +13,6 @@ export const useDatabaseHierarchy = (dataSourceName: string) => {
|
|||||||
});
|
});
|
||||||
return hierarcy;
|
return hierarcy;
|
||||||
},
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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<ManualLocalRelationshipWidget> = () => {
|
||||||
|
const [formState, updateFormState] = useState('');
|
||||||
|
return (
|
||||||
|
<div className="w-2/3">
|
||||||
|
<ManualLocalRelationshipWidget
|
||||||
|
dataSourceName="sqlite_test"
|
||||||
|
table={['Album']}
|
||||||
|
onSuccess={() => {
|
||||||
|
updateFormState('success');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>{formState}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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 (
|
||||||
|
<UpdatedForm
|
||||||
|
schema={schema}
|
||||||
|
onSubmit={values => {
|
||||||
|
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: [{}],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<div className="w-full sm:w-6/12 mb-md">
|
||||||
|
<div className="mb-sm">
|
||||||
|
<Name />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-sm">
|
||||||
|
<RelationshipType />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-12">
|
||||||
|
<div className="col-span-5">
|
||||||
|
<div className="rounded bg-gray-50 border border-gray-300 p-md gap-y-4 border-l-4 border-l-green-600">
|
||||||
|
<SourceTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LinkBlockHorizontal />
|
||||||
|
<div className="col-span-5">
|
||||||
|
<div className="rounded bg-gray-50 border border-gray-300 p-md gap-y-4 border-l-4 border-l-indigo-600">
|
||||||
|
<TargetTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LinkBlockVertical title="Columns Mapped To" />
|
||||||
|
|
||||||
|
<MapColumns />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
mode="primary"
|
||||||
|
type="submit"
|
||||||
|
loadingText="Saving relationship"
|
||||||
|
data-testid="add-local-db-relationship"
|
||||||
|
isLoading={isSaving}
|
||||||
|
>
|
||||||
|
Save Relationship
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UpdatedForm>
|
||||||
|
);
|
||||||
|
};
|
@ -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({}));
|
||||||
|
}),
|
||||||
|
];
|
@ -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 };
|
||||||
|
};
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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<Table[], AxiosError>({
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
@ -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<TableColumn[], AxiosError>({
|
||||||
|
queryKey: ['tableColumns', dataSourceName, table],
|
||||||
|
queryFn: () => {
|
||||||
|
const columns = DataSource(httpClient).getTableColumns({
|
||||||
|
dataSourceName,
|
||||||
|
table,
|
||||||
|
});
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: !!table && !!dataSourceName,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FaLink } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export const LinkBlockHorizontal = () => {
|
||||||
|
return (
|
||||||
|
<div className="col-span-2 flex relative items-center justify-center w-full py-md">
|
||||||
|
<div
|
||||||
|
className="flex z-10 items-center justify-center border border-gray-300 bg-white"
|
||||||
|
style={{
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
borderRadius: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaLink />
|
||||||
|
</div>
|
||||||
|
<div className="absolute w-full border-b border-gray-300" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FaLink } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export const LinkBlockVertical = ({ title }: { title: string }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center w-full ml-lg border-l border-gray-300 py-lg t-"
|
||||||
|
style={{ marginTop: '0px' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center border border-gray-300 bg-white mr-md"
|
||||||
|
style={{
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
marginLeft: -17,
|
||||||
|
borderRadius: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaLink />
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold text-muted">{title}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<Schema>({ name: 'column_mapping' });
|
||||||
|
|
||||||
|
const {
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<Schema>();
|
||||||
|
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 (
|
||||||
|
<div className="rounded bg-gray-50 border border-gray-300 p-md mb-md mt-0 h">
|
||||||
|
<div className="items-center mb-sm font-semibold text-gray-600">
|
||||||
|
<IndicatorCard
|
||||||
|
status="negative"
|
||||||
|
headline="Errors while fetching columns"
|
||||||
|
showIcon
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
{!!sourceColumnsFetchError && (
|
||||||
|
<li>
|
||||||
|
source table error:{' '}
|
||||||
|
{JSON.stringify(sourceColumnsFetchError.response?.data)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{!!targetColumnsFetchError && (
|
||||||
|
<li>
|
||||||
|
target table error:
|
||||||
|
{JSON.stringify(targetColumnsFetchError.response?.data)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</IndicatorCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded bg-gray-50 border border-gray-300 p-md mb-md mt-0 h">
|
||||||
|
<div className="grid grid-cols-12 items-center mb-sm font-semibold text-gray-600">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<FaCircle className="text-green-600" /> Source Column
|
||||||
|
</div>
|
||||||
|
<div className="col-span-6">
|
||||||
|
<FaCircle className="text-indigo-600" /> Reference Column
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-red-500">{formErrorMessage}</div>
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-12 items-center mb-sm"
|
||||||
|
key={`${index}_column_map_row`}
|
||||||
|
>
|
||||||
|
<div className="col-span-5">
|
||||||
|
{areSourceColumnsLoading ? (
|
||||||
|
<Skeleton height={35} />
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
options={(sourceTableColumns ?? []).map(column => ({
|
||||||
|
label: column.name,
|
||||||
|
value: column.name,
|
||||||
|
}))}
|
||||||
|
name={`column_mapping.${index}.from`}
|
||||||
|
placeholder="Select source column"
|
||||||
|
noErrorPlaceholder={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-around">
|
||||||
|
<FaArrowRight />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5">
|
||||||
|
{areTargetColumnsLoading ? (
|
||||||
|
<Skeleton height={35} />
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
options={(targetTableColumns ?? []).map(column => ({
|
||||||
|
label: column.name,
|
||||||
|
value: column.name,
|
||||||
|
}))}
|
||||||
|
name={`column_mapping.${index}.to`}
|
||||||
|
placeholder="Select reference column"
|
||||||
|
noErrorPlaceholder={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-around">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
icon={<FaTrashAlt />}
|
||||||
|
onClick={() => {
|
||||||
|
setValue(
|
||||||
|
'column_mapping',
|
||||||
|
columnMappings.filter((_, i) => index !== i)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="my-4">
|
||||||
|
<Button type="button" onClick={() => append({})}>
|
||||||
|
Add New Mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { InputField } from '@/new-components/Form';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Name = () => {
|
||||||
|
return (
|
||||||
|
<InputField
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Relationship name"
|
||||||
|
dataTest="local-db-to-db-rel-name"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Select } from '@/new-components/Form';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const RelationshipType = () => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
name="relationship_type"
|
||||||
|
label="Relationship Type"
|
||||||
|
dataTest="local-db-to-db-select-rel-type"
|
||||||
|
placeholder="Select a relationship type..."
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Object Relationship',
|
||||||
|
value: 'object',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Array Relationship',
|
||||||
|
value: 'array',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export const SourceTable = () => {
|
||||||
|
const table = useWatch<Record<string, Table>>({ name: 'source_table' });
|
||||||
|
|
||||||
|
const dataSourceName = useWatch<Record<string, string>>({
|
||||||
|
name: 'source_name',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: databaseHierarchy,
|
||||||
|
isLoading: isDatabaseHierarchyLoading,
|
||||||
|
error: databaseHierarchyError,
|
||||||
|
} = useDatabaseHierarchy(dataSourceName);
|
||||||
|
|
||||||
|
if (databaseHierarchyError)
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
{JSON.stringify(databaseHierarchyError.response?.data)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDatabaseHierarchyLoading)
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<div data-testid="target_table_loading">
|
||||||
|
<Skeleton count={3} height={40} className="my-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputField
|
||||||
|
name="source_name"
|
||||||
|
label="Database"
|
||||||
|
labelIcon={<FaDatabase />}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
name="source_table"
|
||||||
|
options={[table].map(t => ({
|
||||||
|
value: t,
|
||||||
|
label: getTableName(t, databaseHierarchy ?? []),
|
||||||
|
}))}
|
||||||
|
label="Table"
|
||||||
|
labelIcon={<FaTable />}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<typeof TargetTable> = () => {
|
||||||
|
const [formState, updateFormState] = useState<any>('');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UpdatedForm
|
||||||
|
schema={z.object({
|
||||||
|
target_name: z.string().min(1, 'Reference source must be provided!'),
|
||||||
|
target_table: z.any().transform(value => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
onSubmit={data => {
|
||||||
|
updateFormState(data);
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
defaultValues: {
|
||||||
|
target_name: 'sqlite_test',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => <TargetTable />}
|
||||||
|
</UpdatedForm>
|
||||||
|
<div>{formState}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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<Record<string, Table>>({ name: 'source_table' });
|
||||||
|
const dataSourceName = useWatch<Record<string, string>>({
|
||||||
|
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 (
|
||||||
|
<div className="h-full">
|
||||||
|
{JSON.stringify(databaseHierarchyError.response?.data)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetOptionError)
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
{JSON.stringify(targetOptionError.response?.data)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (areTargetOptionsLoading || isDatabaseHierarchyLoading)
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<div data-testid="target_table_loading">
|
||||||
|
<Skeleton count={3} height={40} className="my-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<InputField
|
||||||
|
name="target_name"
|
||||||
|
label="Reference Database"
|
||||||
|
labelIcon={<FaDatabase />}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
name="target_table"
|
||||||
|
label="Reference Table"
|
||||||
|
labelIcon={<FaTable />}
|
||||||
|
options={(options ?? []).map(t => ({
|
||||||
|
value: JSON.stringify(t),
|
||||||
|
label: getTableName(t, databaseHierarchy ?? []),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const schema = z.object({
|
||||||
|
name: z.string().min(1, 'Name is a required field!'),
|
||||||
|
relationship_type: z.union([z.literal('object'), z.literal('array')], {
|
||||||
|
required_error: 'Relationship type is required field!',
|
||||||
|
}),
|
||||||
|
source_table: z.any().transform(value => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
source_name: z.string().min(1, 'Source source must be provided!'),
|
||||||
|
target_name: z.string().min(1, 'Reference source must be provided!'),
|
||||||
|
target_table: z.any().transform(value => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
column_mapping: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
from: z.string().min(1, 'Please provide a column!'),
|
||||||
|
to: z.string().min(1, 'Please provide a column!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, 'Please provide at least one column mapping!'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Schema = z.infer<typeof schema>;
|
@ -55,8 +55,12 @@ export const convertToTreeData = (
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
...uniqueLevelValues.map(levelValue => {
|
...uniqueLevelValues.map(levelValue => {
|
||||||
|
const { database, ...rest } = JSON.parse(name);
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
const _key = JSON.stringify({ ...JSON.parse(name), [key]: levelValue });
|
const _key = JSON.stringify({
|
||||||
|
database,
|
||||||
|
table: { ...rest.table, [key]: levelValue },
|
||||||
|
});
|
||||||
const children = convertToTreeData(
|
const children = convertToTreeData(
|
||||||
tables.filter((t: any) => t[key] === levelValue),
|
tables.filter((t: any) => t[key] === levelValue),
|
||||||
hierarchy.slice(1),
|
hierarchy.slice(1),
|
||||||
|
@ -50,7 +50,7 @@ export const getTableColumns = async (props: GetTableColumnsProps) => {
|
|||||||
sourceCustomization: metadataSource?.customization,
|
sourceCustomization: metadataSource?.customization,
|
||||||
configuration: metadataTable.configuration,
|
configuration: metadataTable.configuration,
|
||||||
});
|
});
|
||||||
|
console.log(queryRoot);
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
const graphQLFields =
|
const graphQLFields =
|
||||||
introspectionResult.data.__schema.types.find(
|
introspectionResult.data.__schema.types.find(
|
||||||
|
@ -1,12 +1,35 @@
|
|||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import { metadata } from './metadata';
|
import { Metadata } from '../../metadataTypes';
|
||||||
import { queryData } from './querydata';
|
|
||||||
|
|
||||||
const baseUrl = 'http://localhost:8080';
|
const baseUrl = 'http://localhost:8080';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
resource_version: 1,
|
||||||
|
metadata: {
|
||||||
|
version: 3,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
name: 'sqlite_test',
|
||||||
|
kind: 'sqlite_test',
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
table: ['Album'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: ['Artist'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configuration: {
|
||||||
|
some_value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const handlers = (url = baseUrl) => [
|
export const handlers = (url = baseUrl) => [
|
||||||
rest.post(`${url}/v2/query`, (req, res, ctx) => {
|
rest.post(`${url}/v2/query`, (req, res, ctx) => {
|
||||||
return res(ctx.json(queryData));
|
return res(ctx.json({}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
|
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
FaTable,
|
FaTable,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { Button } from '@/new-components/Button';
|
|
||||||
import { CardedTable } from '@/new-components/CardedTable';
|
import { CardedTable } from '@/new-components/CardedTable';
|
||||||
import { IndicatorCard } from '@/new-components/IndicatorCard';
|
import { IndicatorCard } from '@/new-components/IndicatorCard';
|
||||||
import { Relationship } from './types';
|
import { Relationship } from './types';
|
||||||
@ -20,6 +19,8 @@ import { useListAllRelationshipsFromMetadata } from './hooks/useListAllRelations
|
|||||||
export const columns = ['NAME', 'SOURCE', 'TYPE', 'RELATIONSHIP', null];
|
export const columns = ['NAME', 'SOURCE', 'TYPE', 'RELATIONSHIP', null];
|
||||||
|
|
||||||
const getTableDisplayName = (table: Table): string => {
|
const getTableDisplayName = (table: Table): string => {
|
||||||
|
if (Array.isArray(table)) return table.join();
|
||||||
|
|
||||||
if (!table) return 'Empty Object';
|
if (!table) return 'Empty Object';
|
||||||
|
|
||||||
if (typeof table === 'string') return table;
|
if (typeof table === 'string') return table;
|
||||||
@ -131,17 +132,7 @@ export const DatabaseRelationshipsTable = ({
|
|||||||
{relationships.map(relationship => (
|
{relationships.map(relationship => (
|
||||||
<CardedTable.TableBodyRow key={relationship.name}>
|
<CardedTable.TableBodyRow key={relationship.name}>
|
||||||
<CardedTable.TableBodyCell>
|
<CardedTable.TableBodyCell>
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
onEditRow({
|
|
||||||
dataSourceName,
|
|
||||||
table,
|
|
||||||
relationship,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{relationship.name}
|
{relationship.name}
|
||||||
</Button>
|
|
||||||
</CardedTable.TableBodyCell>
|
</CardedTable.TableBodyCell>
|
||||||
|
|
||||||
<CardedTable.TableBodyCell>
|
<CardedTable.TableBodyCell>
|
||||||
|
@ -82,7 +82,7 @@ export const expectedManualLocalRelationshipOutput: Relationship & {
|
|||||||
name: 'Employee',
|
name: 'Employee',
|
||||||
schema: 'public',
|
schema: 'public',
|
||||||
},
|
},
|
||||||
relationship_type: 'Object',
|
relationship_type: 'Array',
|
||||||
mapping: {
|
mapping: {
|
||||||
from: {
|
from: {
|
||||||
source: 'chinook',
|
source: 'chinook',
|
||||||
|
@ -105,7 +105,8 @@ describe('test adapters', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
'Array'
|
||||||
);
|
);
|
||||||
expect(result).toEqual(expectedManualLocalRelationshipOutput);
|
expect(result).toEqual(expectedManualLocalRelationshipOutput);
|
||||||
});
|
});
|
||||||
@ -147,7 +148,8 @@ describe('test adapters', () => {
|
|||||||
column: ['EmployeeId'],
|
column: ['EmployeeId'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
'Object'
|
||||||
);
|
);
|
||||||
expect(result).toEqual(expectedLocalTableRelationships);
|
expect(result).toEqual(expectedLocalTableRelationships);
|
||||||
});
|
});
|
||||||
|
@ -113,7 +113,8 @@ export const useListAllRelationshipsFromMetadata = (
|
|||||||
return adaptManualRelationship(
|
return adaptManualRelationship(
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
table,
|
table,
|
||||||
relationship
|
relationship,
|
||||||
|
'Object'
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,7 +125,8 @@ export const useListAllRelationshipsFromMetadata = (
|
|||||||
dataSourceName,
|
dataSourceName,
|
||||||
table,
|
table,
|
||||||
relationship,
|
relationship,
|
||||||
fkRelationships
|
fkRelationships,
|
||||||
|
'Object'
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,14 +155,16 @@ export const useListAllRelationshipsFromMetadata = (
|
|||||||
return adaptManualRelationship(
|
return adaptManualRelationship(
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
table,
|
table,
|
||||||
relationship
|
relationship,
|
||||||
|
'Array'
|
||||||
);
|
);
|
||||||
|
|
||||||
return adaptLocalTableRelationship(
|
return adaptLocalTableRelationship(
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
table,
|
table,
|
||||||
relationship,
|
relationship,
|
||||||
fkRelationships
|
fkRelationships,
|
||||||
|
'Array'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -106,13 +106,16 @@ export const adaptRemoteDBRelationship = (
|
|||||||
export const adaptManualRelationship = (
|
export const adaptManualRelationship = (
|
||||||
dataSourceName: string,
|
dataSourceName: string,
|
||||||
table: Table,
|
table: Table,
|
||||||
relationship: ManualObjectRelationship | ManualArrayRelationship
|
relationship: ManualObjectRelationship | ManualArrayRelationship,
|
||||||
): Relationship & { type: 'toLocalTableManual' } => {
|
relationship_type: 'Object' | 'Array'
|
||||||
|
): Relationship & {
|
||||||
|
type: 'toLocalTableManual';
|
||||||
|
} => {
|
||||||
return {
|
return {
|
||||||
name: relationship.name,
|
name: relationship.name,
|
||||||
type: 'toLocalTableManual',
|
type: 'toLocalTableManual',
|
||||||
toLocalTable: table,
|
toLocalTable: table,
|
||||||
relationship_type: 'Object',
|
relationship_type,
|
||||||
mapping: {
|
mapping: {
|
||||||
from: {
|
from: {
|
||||||
source: dataSourceName,
|
source: dataSourceName,
|
||||||
@ -136,7 +139,8 @@ export const adaptLocalTableRelationship = (
|
|||||||
dataSourceName: string,
|
dataSourceName: string,
|
||||||
table: Table,
|
table: Table,
|
||||||
relationship: LocalTableObjectRelationship | LocalTableArrayRelationship,
|
relationship: LocalTableObjectRelationship | LocalTableArrayRelationship,
|
||||||
fkRelationships: TableFkRelationships[]
|
fkRelationships: TableFkRelationships[],
|
||||||
|
relationship_type: 'Array' | 'Object'
|
||||||
): Relationship & { type: 'toLocalTableFk' } => {
|
): Relationship & { type: 'toLocalTableFk' } => {
|
||||||
const columns = isLegacyFkConstraint(
|
const columns = isLegacyFkConstraint(
|
||||||
relationship.using.foreign_key_constraint_on
|
relationship.using.foreign_key_constraint_on
|
||||||
@ -148,7 +152,7 @@ export const adaptLocalTableRelationship = (
|
|||||||
name: relationship.name,
|
name: relationship.name,
|
||||||
type: 'toLocalTableFk',
|
type: 'toLocalTableFk',
|
||||||
toLocalTable: table,
|
toLocalTable: table,
|
||||||
relationship_type: 'Object',
|
relationship_type,
|
||||||
mapping: {
|
mapping: {
|
||||||
from: {
|
from: {
|
||||||
source: dataSourceName,
|
source: dataSourceName,
|
||||||
|
@ -123,7 +123,7 @@ export const UpdatedForm = <FormSchema extends Schema>(
|
|||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
id={id}
|
id={id}
|
||||||
className={`space-y-md bg-legacybg ${className || ''}`}
|
className={`bg-legacybg ${className || ''}`}
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
@ -6,7 +6,7 @@ import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper';
|
|||||||
|
|
||||||
type SelectItem = {
|
type SelectItem = {
|
||||||
label: ReactText;
|
label: ReactText;
|
||||||
value: ReactText;
|
value: any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user