console: use the refactored suggested relationships component for GDC

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9878
GitOrigin-RevId: 1dcefbf82791fe1bbedb1a7bb132ef4484425781
This commit is contained in:
Matthew Goodwin 2023-07-18 09:45:07 -05:00 committed by hasura-bot
parent 2f3defe3a9
commit 5d9d91dbfd
10 changed files with 2 additions and 1046 deletions

View File

@ -2,7 +2,7 @@ import get from 'lodash/get';
import { Analytics, REDACT_EVERYTHING } from '../../Analytics';
import { ManageTrackedTables } from '../ManageTable/components/ManageTrackedTables';
import { ManageTrackedFunctions } from '../TrackResources/TrackFunctions/components/ManageTrackedFunctions';
import { ManageTrackedRelationshipsContainer } from '../TrackResources/components/ManageTrackedRelationshipsContainer';
import { ManageSuggestedRelationships } from '../TrackResources/TrackRelationships/ManageSuggestedRelationships';
import { useDriverCapabilities } from '../hooks/useDriverCapabilities';
import { BreadCrumbs, CollapsibleResource, SourceName } from './parts';
@ -11,7 +11,6 @@ export interface ManageDatabaseProps {
schema?: string;
}
//This component has the code for template gallery but is currently commented out until further notice.
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
const {
data: {
@ -55,9 +54,7 @@ export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
title="Foreign Key Relationships"
tooltip="Track foreign key relationships in your database in your GraphQL API"
>
<ManageTrackedRelationshipsContainer
dataSourceName={dataSourceName}
/>
<ManageSuggestedRelationships dataSourceName={dataSourceName} />
</CollapsibleResource>
)}

View File

@ -1,37 +0,0 @@
import { expect } from '@storybook/jest';
import { StoryObj, Meta } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { Relationship } from '../../../DatabaseRelationships';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { ManageTrackedRelationships } from '../components/ManageTrackedRelationships';
export default {
title: 'Data/Components/ManageTrackedRelationships',
component: ManageTrackedRelationships,
decorators: [ReactQueryDecorator()],
} as Meta<typeof ManageTrackedRelationships>;
const suggestedRelationships: SuggestedRelationshipWithName[] = [];
const trackedFKRelationships: Relationship[] = [];
export const Base: StoryObj<typeof ManageTrackedRelationships> = {
render: () => (
<ManageTrackedRelationships
dataSourceName="chinook"
suggestedRelationships={suggestedRelationships}
trackedFKRelationships={trackedFKRelationships}
isLoading={false}
/>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByText('No untracked relationships found')
).toBeInTheDocument();
},
};

View File

@ -1,51 +0,0 @@
import React from 'react';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { Relationship } from '../../../DatabaseRelationships/types';
import { TrackableResourceTabs } from '../../ManageDatabase/components/TrackableResourceTabs';
import { TrackedRelationshipsContainer } from './TrackedRelationshipsContainer';
import { UntrackedRelationships } from './UntrackedRelationships';
type ManageTrackedRelationshipsProps = {
dataSourceName: string;
suggestedRelationships: SuggestedRelationshipWithName[];
trackedFKRelationships: Relationship[];
isLoading: boolean;
};
export const ManageTrackedRelationships: React.VFC<
ManageTrackedRelationshipsProps
> = ({
dataSourceName,
isLoading,
suggestedRelationships,
trackedFKRelationships,
}) => {
const [tab, setTab] = React.useState<'tracked' | 'untracked'>('untracked');
if (!suggestedRelationships)
return <div className="px-md">Something went wrong</div>;
return (
<TrackableResourceTabs
introText={
'Create and track a relationship to view it in your GraphQL schema.'
}
value={tab}
data-testid="track-relationships"
onValueChange={value => setTab(value)}
isLoading={isLoading}
items={{
tracked: {
amount: trackedFKRelationships.length,
content: (
<TrackedRelationshipsContainer dataSourceName={dataSourceName} />
),
},
untracked: {
amount: suggestedRelationships.length,
content: <UntrackedRelationships dataSourceName={dataSourceName} />,
},
}}
/>
);
};

View File

@ -1,35 +0,0 @@
import { useAllSuggestedRelationships } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useAllSuggestedRelationships';
import { useTrackedRelationships } from './hooks/useTrackedRelationships';
import { ManageTrackedRelationships } from './ManageTrackedRelationships';
export const ManageTrackedRelationshipsContainer = ({
dataSourceName,
}: {
dataSourceName: string;
}) => {
const {
data: trackedFKRelationships,
isLoading: isLoadingTrackedRelationships,
} = useTrackedRelationships(dataSourceName);
const { suggestedRelationships, isLoadingSuggestedRelationships } =
useAllSuggestedRelationships({
dataSourceName,
omitTracked: true,
isEnabled: true,
});
if (!suggestedRelationships)
return <div className="px-md">Something went wrong</div>;
return (
<ManageTrackedRelationships
dataSourceName={dataSourceName}
suggestedRelationships={suggestedRelationships}
trackedFKRelationships={trackedFKRelationships}
isLoading={
isLoadingSuggestedRelationships || isLoadingTrackedRelationships
}
/>
);
};

View File

@ -1,41 +0,0 @@
import { StoryFn, Meta } from '@storybook/react';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { RelationshipRow, RelationshipRowProps } from './RelationshipRow';
import { action } from '@storybook/addon-actions';
export default {
component: RelationshipRow,
decorators: [ReactQueryDecorator()],
} as Meta<typeof RelationshipRow>;
const relationship: SuggestedRelationshipWithName = {
constraintName: 'Album_Artist',
type: 'object',
from: {
table: 'Album',
columns: ['id'],
},
to: {
table: 'Artist',
columns: ['albumId'],
},
};
const baseProps: RelationshipRowProps = {
relationship: relationship,
dataSourceName: 'Chinook',
isChecked: false,
isLoading: false,
onCustomize: () => action('onCustomize')(),
onToggle: () => action('onToggle')(),
onTrack: async () => action('onTrack')(),
};
export const Base: StoryFn<typeof RelationshipRow> = () => (
<RelationshipRow {...baseProps} />
);
export const Checked: StoryFn<typeof RelationshipRow> = () => (
<RelationshipRow {...baseProps} isChecked />
);

View File

@ -1,93 +0,0 @@
import { ChangeEvent, useState } from 'react';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { FaArrowRight, FaColumns, FaDatabase, FaTable } from 'react-icons/fa';
import { capitaliseFirstLetter } from '../../../../components/Common/ConfigureTransformation/utils';
import { getTableDisplayName } from '../../../DatabaseRelationships';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
export type RelationshipRowProps = {
dataSourceName: string;
isChecked: boolean;
isLoading: boolean;
onCustomize: () => void;
onToggle: (e: ChangeEvent<HTMLInputElement>) => void;
onTrack: () => Promise<void>;
relationship: SuggestedRelationshipWithName;
};
export const RelationshipRow: React.VFC<RelationshipRowProps> = ({
dataSourceName,
isChecked,
isLoading,
onCustomize,
onToggle,
onTrack,
relationship,
}) => {
const [isLoadingState, setLoading] = useState(false);
const onTrackHandler = () => {
setLoading(true);
onTrack().finally(() => setLoading(false));
};
return (
<CardedTable.TableBodyRow
className={isChecked ? 'bg-blue-50' : 'bg-transparent'}
>
<td className="w-0 px-sm text-sm font-semibold text-muted uppercase tracking-wider">
<input
type="checkbox"
className="cursor-pointer rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
value={relationship.constraintName}
checked={isChecked}
onChange={onToggle}
/>
</td>
<CardedTable.TableBodyCell>
{relationship.constraintName}
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
<div className="flex items-center gap-2">
<FaDatabase /> <span>{dataSourceName}</span>
</div>
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
{capitaliseFirstLetter(relationship.type)}
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
<div className="flex flex-row items-center gap-2">
<FaTable />
<span>{getTableDisplayName(relationship.from.table)}</span>
/
<FaColumns />
{relationship.from.columns.join(' ')}
<FaArrowRight />
<FaTable />
<span>{getTableDisplayName(relationship.to.table)}</span>
/
<FaColumns />
{relationship.to.columns.join(' ')}
</div>
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
<div className="flex flex-row">
<Button
size="sm"
onClick={() => onTrackHandler()}
isLoading={isLoading || isLoadingState}
>
Track
</Button>
<Button
size="sm"
className="ml-1"
onClick={() => onCustomize()}
isLoading={isLoading}
>
Customize
</Button>
</div>
</CardedTable.TableBodyCell>
</CardedTable.TableBodyRow>
);
};

View File

@ -1,85 +0,0 @@
import { StoryFn, StoryObj, Meta } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { expect } from '@storybook/jest';
import { TrackedRelationships } from './TrackedRelationships';
import { action } from '@storybook/addon-actions';
import { Relationship } from '../../../DatabaseRelationships';
export default {
title: 'Data/Components/TrackedRelationships',
component: TrackedRelationships,
decorators: [ReactQueryDecorator()],
} as Meta<typeof TrackedRelationships>;
const relationships: Relationship[] = [
{
name: 'CUSTOMER_INVOICEs',
fromSource: 'Snow',
fromTable: ['CUSTOMER'],
relationshipType: 'Array',
type: 'localRelationship',
definition: { mapping: {}, toTable: 'INVOICE' },
},
{
name: 'INVOICE_CUSTOMER',
fromSource: 'Snow',
fromTable: ['INVOICE'],
relationshipType: 'Object',
type: 'localRelationship',
definition: {
toTable: ['CUSTOMER'],
mapping: { CUSTOMERID: 'CUSTOMERID' },
},
},
];
export const Base: StoryObj<typeof TrackedRelationships> = {
render: () => (
<TrackedRelationships
dataSourceName="chinook"
isLoading={false}
onRefetchMetadata={() => action('onRefetchMetadata')()}
relationships={relationships}
/>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText('Untrack Selected (0)')).toBeInTheDocument();
await expect(canvas.getByText('Show 10 relationships')).toBeInTheDocument();
await expect(canvas.getByText('RELATIONSHIP NAME')).toBeInTheDocument();
await expect(canvas.getByText('SOURCE')).toBeInTheDocument();
await expect(canvas.getByText('TYPE')).toBeInTheDocument();
await expect(canvas.getByText('RELATIONSHIP')).toBeInTheDocument();
await expect(canvas.getByText('CUSTOMER_INVOICEs')).toBeInTheDocument();
await expect(canvas.getByText('INVOICE_CUSTOMER')).toBeInTheDocument();
await expect(canvas.getByText('Array')).toBeInTheDocument();
await expect(canvas.getByText('Object')).toBeInTheDocument();
await expect(canvas.getAllByText('Snow')).toHaveLength(2);
await expect(canvas.getAllByText('Rename')).toHaveLength(2);
await expect(canvas.getAllByText('Remove')).toHaveLength(2);
},
};
export const Loading: StoryFn<typeof TrackedRelationships> = () => (
<TrackedRelationships
dataSourceName="chinook"
isLoading={true}
onRefetchMetadata={() => action('onRefetchMetadata')()}
relationships={relationships}
/>
);
export const NoRelationships: StoryFn<typeof TrackedRelationships> = () => (
<TrackedRelationships
dataSourceName="chinook"
isLoading={false}
onRefetchMetadata={() => action('onRefetchMetadata')()}
relationships={[]}
/>
);

View File

@ -1,378 +0,0 @@
import React, { useEffect, useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import Skeleton from 'react-loading-skeleton';
import { APIError } from '../../../../hooks/error';
import { Badge } from '../../../../new-components/Badge';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { useFireNotification } from '../../../../new-components/Notifications';
import { hasuraToast } from '../../../../new-components/Toasts';
import { exportMetadata } from '../../../DataSource';
import { RelationshipMapping } from '../../../DatabaseRelationships/components/AvailableRelationshipsList/parts/RelationshipMapping';
import { RowActions } from '../../../DatabaseRelationships/components/AvailableRelationshipsList/parts/RowActions';
import { TargetName } from '../../../DatabaseRelationships/components/AvailableRelationshipsList/parts/TargetName';
import { RenderWidget } from '../../../DatabaseRelationships/components/RenderWidget/RenderWidget';
import { NOTIFICATIONS } from '../../../DatabaseRelationships/components/constants';
import { useCheckRows } from '../../../DatabaseRelationships/hooks/useCheckRows';
import { MODE, Relationship } from '../../../DatabaseRelationships/types';
import {
generateDeleteLocalRelationshipRequest,
generateRemoteRelationshipDeleteRequest,
} from '../../../DatabaseRelationships/utils/generateRequest';
import { useMetadataMigration } from '../../../MetadataAPI';
import { useHttpClient } from '../../../Network';
import { BulkKeepGoingResponse, Source } from '../../../hasura-metadata-types';
import {
DEFAULT_PAGE_NUMBER,
DEFAULT_PAGE_SIZE,
DEFAULT_PAGE_SIZES,
} from '../constants';
import { paginate } from '../utils';
import { SearchBar } from './SearchBar';
const getQueryFunction = (relationship: Relationship) => {
if (relationship.type === 'localRelationship') {
return generateDeleteLocalRelationshipRequest;
}
if (
relationship.type === 'remoteDatabaseRelationship' ||
relationship.type === 'remoteSchemaRelationship'
) {
return generateRemoteRelationshipDeleteRequest;
}
return undefined;
};
const getSerializedRelationshipNames = (relationships: Relationship[]) =>
relationships.map(rel => rel.name).join('-');
type RelationshipAction = {
mode?: MODE;
relationship?: Relationship;
};
interface TrackedRelationshipsProps {
dataSourceName: string;
driver?: Source['kind'];
isLoading: boolean;
onUpdate: () => void;
relationships: Relationship[];
}
export const TrackedRelationships: React.VFC<TrackedRelationshipsProps> = ({
dataSourceName,
driver,
isLoading,
onUpdate,
relationships,
}) => {
const httpClient = useHttpClient();
const { mutateAsync } = useMetadataMigration<BulkKeepGoingResponse>();
const [isTrackingSelectedRelationships, setTrackingSelectedRelationships] =
useState(false);
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [searchText, setSearchText] = useState('');
const checkboxRef = React.useRef<HTMLInputElement>(null);
const { checkedIds, onCheck, allChecked, toggleAll, reset, inputStatus } =
useCheckRows(relationships.map(rel => ({ id: rel.name })));
useEffect(() => {
if (!checkboxRef.current) return;
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
}, [inputStatus]);
const [filteredRelationships, setFilteredRelationships] =
useState<Relationship[]>(relationships);
const serializedRelationshipNames =
getSerializedRelationshipNames(relationships);
// apply the search text to the relationships
useEffect(() => {
reset();
if (!searchText) {
setFilteredRelationships(relationships);
return;
}
setFilteredRelationships(
relationships.filter(rel =>
rel.name.toLowerCase().includes(searchText.toLowerCase())
)
);
}, [serializedRelationshipNames, searchText]);
const onUntrackSelected = async () => {
setTrackingSelectedRelationships(true);
try {
const selectedRelationships = relationships.filter(rel =>
checkedIds.includes(rel.name)
);
if (driver) {
const recentMetadata = await exportMetadata({ httpClient });
const queries = selectedRelationships.map(relationship => {
const queryFunction = getQueryFunction(relationship);
const query = queryFunction
? queryFunction({
driver,
resource_version: recentMetadata.resource_version,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
relationship,
})
: {};
return query;
});
await mutateAsync(
{
query: {
type: 'bulk_keep_going',
args: queries,
},
},
{
onSuccess: response => {
response.forEach(result => {
if ('error' in result) {
hasuraToast({
type: 'error',
title: 'Error while tracking table',
children: result.error,
});
}
});
const successfullyTrackedCounter = response.filter(
result => 'message' in result && result.message === 'success'
).length;
const plural = successfullyTrackedCounter > 1 ? 's' : '';
onUpdate();
hasuraToast({
type: 'success',
title: 'Successfully untracked',
message: `${successfullyTrackedCounter} object${plural} tracked`,
});
},
onError: err => {
hasuraToast({
type: 'error',
title: 'Unable to perform operation',
message: (err as APIError).message,
});
},
}
);
}
} catch (err) {
console.error(err);
setTrackingSelectedRelationships(false);
}
reset();
setTrackingSelectedRelationships(false);
};
const { fireNotification } = useFireNotification();
const [{ mode, relationship }, setRelationshipAction] =
useState<RelationshipAction>({
mode: undefined,
relationship: undefined,
});
const onRelationshipActionCancel = () => {
setRelationshipAction({
mode: undefined,
relationship: undefined,
});
};
const onRelationshipActionError = (err: Error) => {
if (mode)
fireNotification({
type: 'error',
title: NOTIFICATIONS.onError[mode],
message: err?.message ?? '',
});
};
const onRelationshipActionSuccess = () => {
onUpdate();
if (mode)
fireNotification({
type: 'success',
title: 'Success!',
message: NOTIFICATIONS.onSuccess[mode],
});
setRelationshipAction({
mode: undefined,
relationship: undefined,
});
};
if (isLoading) {
return (
<div className="px-md">
<Skeleton count={4} height={25} className="mb-2" />
</div>
);
}
if (!isLoading && relationships.length === 0) {
return (
<div className="space-y-4">
<IndicatorCard>No tracked relationships found</IndicatorCard>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between space-x-4">
<div className="flex gap-5">
<Button
mode="primary"
disabled={!checkedIds.length}
onClick={onUntrackSelected}
isLoading={isTrackingSelectedRelationships}
loadingText="Please Wait"
>
Untrack Selected ({checkedIds.length})
</Button>
<span className="border-r border-slate-300"></span>
<div className="flex gap-2">
<SearchBar onSearch={data => setSearchText(data)} />
{searchText.length ? (
<Badge>{filteredRelationships.length} results found</Badge>
) : null}
</div>
</div>
<div className="flex gap-1">
<Button
icon={<FaAngleLeft />}
onClick={() => setPageNumber(pageNumber - 1)}
disabled={pageNumber === 1}
/>
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
className="block w-full max-w-xl h-8 min-h-full shadow-sm rounded pl-3 pr-6 py-0.5 border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400"
>
{DEFAULT_PAGE_SIZES.map(_pageSize => (
<option key={_pageSize} value={_pageSize}>
Show {_pageSize} relationships
</option>
))}
</select>
<Button
icon={<FaAngleRight />}
onClick={() => setPageNumber(pageNumber + 1)}
disabled={pageNumber >= relationships.length / pageSize}
/>
</div>
</div>
<CardedTable.Table>
<CardedTable.TableHead>
<CardedTable.TableHeadRow>
<th className="w-0 bg-gray-50 px-sm text-sm font-semibold text-muted uppercase tracking-wider border-r">
<input
ref={checkboxRef}
type="checkbox"
className="cursor-pointer
rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
checked={allChecked}
onChange={toggleAll}
/>
</th>
<CardedTable.TableHeadCell>
RELATIONSHIP NAME
</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>SOURCE</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>TYPE</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>RELATIONSHIP</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell></CardedTable.TableHeadCell>
</CardedTable.TableHeadRow>
</CardedTable.TableHead>
<CardedTable.TableBody>
{paginate({
data: filteredRelationships,
pageSize,
pageNumber,
}).data.map(relationship => {
return (
<CardedTable.TableBodyRow key={relationship.name}>
<td className="w-0 px-sm text-sm font-semibold text-muted uppercase tracking-wider">
<input
type="checkbox"
className="cursor-pointer rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
value={relationship.name}
checked={checkedIds.includes(relationship.name)}
onChange={() => onCheck(relationship.name)}
/>
</td>
<CardedTable.TableBodyCell>
{relationship.name}
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
<div className="flex items-center gap-2">
<TargetName relationship={relationship} />
</div>
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
{relationship.relationshipType}
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>
<RelationshipMapping relationship={relationship} />
</CardedTable.TableBodyCell>
<CardedTable.TableBodyActionCell>
<RowActions
relationship={relationship}
onActionClick={(_relationship, _mode) => {
setRelationshipAction({
mode: _mode,
relationship: _relationship,
});
}}
/>
</CardedTable.TableBodyActionCell>
</CardedTable.TableBodyRow>
);
})}
</CardedTable.TableBody>
</CardedTable.Table>
<div>
{mode && (
<RenderWidget
dataSourceName={dataSourceName}
table={relationship?.fromTable}
mode={mode}
relationship={relationship}
onSuccess={onRelationshipActionSuccess}
onCancel={onRelationshipActionCancel}
onError={onRelationshipActionError}
/>
)}
</div>
</div>
);
};

View File

@ -1,43 +0,0 @@
import React from 'react';
import {
MetadataSelectors,
useMetadata,
useSyncResourceVersionOnMount,
} from '../../../hasura-metadata-api';
import { TrackedRelationships } from './TrackedRelationships';
import { useTrackedRelationships } from './hooks/useTrackedRelationships';
interface TrackedRelationshipsContainerProps {
dataSourceName: string;
}
export const TrackedRelationshipsContainer: React.VFC<
TrackedRelationshipsContainerProps
> = ({ dataSourceName }) => {
useSyncResourceVersionOnMount({
componentName: 'TrackedRelationshipsContainer',
});
const {
data: relationships,
isLoading: isLoadingRelationships,
refetchRelationships,
} = useTrackedRelationships(dataSourceName);
const { data: metadataSource, isLoading: isLoadingMetadata } = useMetadata(
MetadataSelectors.findSource(dataSourceName)
);
return (
<TrackedRelationships
dataSourceName={dataSourceName}
isLoading={isLoadingRelationships || isLoadingMetadata}
relationships={relationships}
onUpdate={async () => {
await refetchRelationships();
}}
driver={metadataSource?.kind}
/>
);
};

View File

@ -1,278 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { DEFAULT_PAGE_SIZES } from '../constants';
import { FaAngleLeft, FaAngleRight, FaMagic } from 'react-icons/fa';
import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE } from '../constants';
import { paginate } from '../utils';
import { SearchBar } from './SearchBar';
import { Badge } from '../../../../new-components/Badge';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { RelationshipRow } from './RelationshipRow';
import { SuggestedRelationshipTrackModal } from '../../../DatabaseRelationships/components/SuggestedRelationshipTrackModal/SuggestedRelationshipTrackModal';
import Skeleton from 'react-loading-skeleton';
import { useAllSuggestedRelationships } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useAllSuggestedRelationships';
import { useCheckRows } from '../../../DatabaseRelationships/hooks/useCheckRows';
import { useCreateTableRelationships } from '../../../DatabaseRelationships/hooks/useCreateTableRelationships/useCreateTableRelationships';
import { hasuraToast } from '../../../../new-components/Toasts';
import { DisplayToastErrorMessage } from '../../components/DisplayErrorMessage';
interface UntrackedRelationshipsProps {
dataSourceName: string;
}
export const UntrackedRelationships: React.VFC<UntrackedRelationshipsProps> = ({
dataSourceName,
}) => {
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [searchText, setSearchText] = useState('');
const [isModalVisible, setModalVisible] = useState(false);
const [selectedRelationship, setSelectedRelationship] =
useState<SuggestedRelationshipWithName | null>(null);
const { suggestedRelationships, isLoadingSuggestedRelationships } =
useAllSuggestedRelationships({
dataSourceName,
isEnabled: true,
omitTracked: true,
});
const { createTableRelationships, isLoading } =
useCreateTableRelationships(dataSourceName);
// const { data: trackedRelationships } =
// useTrackedRelationships(dataSourceName);
const checkboxRef = React.useRef<HTMLInputElement>(null);
const { checkedIds, onCheck, allChecked, toggleAll, reset, inputStatus } =
useCheckRows(
suggestedRelationships.map(rel => ({ id: rel.constraintName }))
);
const [filteredRelationships, setFilteredRelationships] = useState<
SuggestedRelationshipWithName[]
>(suggestedRelationships);
// console.log('trackedRelationships', trackedRelationships);
// console.log('suggestedRelationships', suggestedRelationships);
const serializedRelationshipNames = suggestedRelationships
.map(rel => rel.constraintName)
.join('-');
useEffect(() => {
reset();
if (!searchText) {
setFilteredRelationships(suggestedRelationships);
return;
}
setFilteredRelationships(
suggestedRelationships.filter(rel =>
rel.constraintName.toLowerCase().includes(searchText.toLowerCase())
)
);
}, [serializedRelationshipNames, searchText]);
useEffect(() => {
if (!checkboxRef.current) return;
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
}, [inputStatus]);
const onTrackRelationship = async (rel: SuggestedRelationshipWithName) => {
await createTableRelationships({
data: [
{
name: rel.constraintName,
source: {
fromSource: dataSourceName,
fromTable: rel.from.table,
},
definition: {
target: {
toSource: dataSourceName,
toTable: rel.to.table,
},
type: rel.type,
detail: {
fkConstraintOn:
'constraint_name' in rel.from ? 'fromTable' : 'toTable',
fromColumns: rel.from.columns,
toColumns: rel.to.columns,
},
},
},
],
});
};
const onTrackSelected = async () => {
const selectedRelationships = suggestedRelationships.filter(rel =>
checkedIds.includes(rel.constraintName)
);
createTableRelationships({
data: selectedRelationships.map(rel => ({
name: rel.constraintName,
source: {
fromSource: dataSourceName,
fromTable: rel.from.table,
},
definition: {
target: {
toSource: dataSourceName,
toTable: rel.to.table,
},
type: rel.type,
detail: {
fkConstraintOn:
'constraint_name' in rel.from ? 'fromTable' : 'toTable',
fromColumns: rel.from.columns,
toColumns: rel.to.columns,
},
},
})),
onSettled: () => {
reset();
},
onSuccess: () => {
hasuraToast({
type: 'success',
title: 'Successfully tracked relationships',
});
},
onError: err => {
hasuraToast({
type: 'error',
title: 'Error while tracking relationships',
children: <DisplayToastErrorMessage message={err.message} />,
});
},
});
};
if (isLoadingSuggestedRelationships) {
return (
<div className="px-sm -mt-2 mb-xs">
<Skeleton width={200} height={20} />
</div>
);
}
if (!isLoadingSuggestedRelationships && suggestedRelationships.length === 0) {
return (
<div className="space-y-4">
<IndicatorCard>No untracked relationships found</IndicatorCard>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between space-x-4">
<div className="flex gap-5">
<Button
mode="primary"
disabled={!checkedIds.length}
onClick={onTrackSelected}
isLoading={isLoading}
loadingText="Please Wait"
>
Track Selected ({checkedIds.length})
</Button>
<span className="border-r border-slate-300"></span>
<div className="flex gap-2">
<SearchBar onSearch={data => setSearchText(data)} />
{searchText.length ? (
<Badge>{filteredRelationships.length} results found</Badge>
) : null}
</div>
</div>
<div className="flex gap-1">
<Button
icon={<FaAngleLeft />}
onClick={() => setPageNumber(pageNumber - 1)}
disabled={pageNumber === 1}
/>
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
className="block w-full max-w-xl h-8 min-h-full shadow-sm rounded pl-3 pr-6 py-0.5 border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400"
>
{DEFAULT_PAGE_SIZES.map(_pageSize => (
<option key={_pageSize} value={_pageSize}>
Show {_pageSize} relationships
</option>
))}
</select>
<Button
icon={<FaAngleRight />}
onClick={() => setPageNumber(pageNumber + 1)}
disabled={pageNumber >= suggestedRelationships.length / pageSize}
/>
</div>
</div>
<CardedTable.Table>
<CardedTable.TableHead>
<CardedTable.TableHeadRow>
<th className="w-0 bg-gray-50 px-sm text-sm font-semibold text-muted uppercase tracking-wider border-r">
<input
ref={checkboxRef}
type="checkbox"
className="cursor-pointer
rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
checked={allChecked}
onChange={toggleAll}
/>
</th>
<CardedTable.TableHeadCell>
<div>
<FaMagic className="fill-muted" /> SUGGESTED RELATIONSHIPS
</div>
</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>SOURCE</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>TYPE</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>RELATIONSHIP</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>ACTIONS</CardedTable.TableHeadCell>
</CardedTable.TableHeadRow>
</CardedTable.TableHead>
<CardedTable.TableBody>
{paginate({
data: filteredRelationships,
pageSize,
pageNumber,
}).data.map(relationship => (
<RelationshipRow
key={relationship.constraintName}
isChecked={checkedIds.includes(relationship.constraintName)}
isLoading={false}
relationship={relationship}
onToggle={() => onCheck(relationship.constraintName)}
onTrack={() => onTrackRelationship(relationship)}
onCustomize={() => {
setSelectedRelationship(relationship);
setModalVisible(true);
}}
dataSourceName={dataSourceName}
/>
))}
</CardedTable.TableBody>
</CardedTable.Table>
{isModalVisible && selectedRelationship && (
<SuggestedRelationshipTrackModal
relationship={selectedRelationship}
dataSourceName={dataSourceName}
onClose={() => setModalVisible(false)}
/>
)}
</div>
);
};