mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
2f3defe3a9
commit
5d9d91dbfd
@ -2,7 +2,7 @@ import get from 'lodash/get';
|
|||||||
import { Analytics, REDACT_EVERYTHING } from '../../Analytics';
|
import { Analytics, REDACT_EVERYTHING } from '../../Analytics';
|
||||||
import { ManageTrackedTables } from '../ManageTable/components/ManageTrackedTables';
|
import { ManageTrackedTables } from '../ManageTable/components/ManageTrackedTables';
|
||||||
import { ManageTrackedFunctions } from '../TrackResources/TrackFunctions/components/ManageTrackedFunctions';
|
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 { useDriverCapabilities } from '../hooks/useDriverCapabilities';
|
||||||
import { BreadCrumbs, CollapsibleResource, SourceName } from './parts';
|
import { BreadCrumbs, CollapsibleResource, SourceName } from './parts';
|
||||||
|
|
||||||
@ -11,7 +11,6 @@ export interface ManageDatabaseProps {
|
|||||||
schema?: string;
|
schema?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//This component has the code for template gallery but is currently commented out until further notice.
|
|
||||||
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@ -55,9 +54,7 @@ export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
|
|||||||
title="Foreign Key Relationships"
|
title="Foreign Key Relationships"
|
||||||
tooltip="Track foreign key relationships in your database in your GraphQL API"
|
tooltip="Track foreign key relationships in your database in your GraphQL API"
|
||||||
>
|
>
|
||||||
<ManageTrackedRelationshipsContainer
|
<ManageSuggestedRelationships dataSourceName={dataSourceName} />
|
||||||
dataSourceName={dataSourceName}
|
|
||||||
/>
|
|
||||||
</CollapsibleResource>
|
</CollapsibleResource>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
},
|
|
||||||
};
|
|
@ -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} />,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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 />
|
|
||||||
);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user