console: port template gallery to new ui / tracked tables as links / code gardening

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8759
Co-authored-by: Nicolas Beaussart <7281023+beaussan@users.noreply.github.com>
GitOrigin-RevId: 2c3b2c2d14f45f0f9e10683d55e77b237fde0a79
This commit is contained in:
Matthew Goodwin 2023-04-20 17:28:28 -05:00 committed by hasura-bot
parent 63fe16604e
commit ffe4bc7d1c
23 changed files with 338 additions and 263 deletions

View File

@ -24,7 +24,7 @@ import {
import { Connect } from '../../../features/ConnectDB';
import { ConnectUIContainer } from '../../../features/ConnectDBRedesign';
import { ConnectDatabaseRouteWrapper } from '../../../features/ConnectDBRedesign/ConnectDatabase.route';
import { ManageDatabaseContainer } from '../../../features/Data';
import { ManageDatabaseRoute } from '../../../features/Data';
import { ManageTable } from '../../../features/Data/ManageTable';
import { setDriver } from '../../../dataSources';
import { exportMetadata } from '../../../metadata/actions';
@ -63,7 +63,7 @@ const makeDataRouter = (
<IndexRedirect to="modify" />
<Route path=":operation" component={ManageTable} />
</Route>
<Route path="database" component={ManageDatabaseContainer} />
<Route path="database" component={ManageDatabaseRoute} />
</Route>
<Route path="edit" component={Connect.EditConnection} />
</Route>

View File

@ -1,29 +1,29 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { Driver } from '../../../../../dataSources';
import { ReduxState } from '../../../../../types';
import requestAction from '../../../../../utils/requestAction';
import type { AsyncThunkConfig } from '../../../../../store';
import { makeMigrationCall } from '../../DataActions';
import { getRunSqlQuery } from '../../../../Common/utils/v1QueryUtils';
import Endpoints from '../../../../../Endpoints';
import { SupportedDrivers } from '../../../../../features/hasura-metadata-types';
import {
exportMetadataQuery,
generateReplaceMetadataQuery,
} from '../../../../../metadata/queryUtils';
import { HasuraMetadataV3 } from '../../../../../metadata/types';
import {
TemplateGallerySection,
TemplateGalleryStore,
TemplateGalleryTemplateDetailFull,
TemplateGalleryTemplateItem,
ServerJsonRootConfig,
ServerJsonTemplateDefinition,
} from './types';
import type { AsyncThunkConfig } from '../../../../../store';
import { ReduxState } from '../../../../../types';
import requestAction from '../../../../../utils/requestAction';
import { getRunSqlQuery } from '../../../../Common/utils/v1QueryUtils';
import { makeMigrationCall } from '../../DataActions';
import {
BASE_URL_PUBLIC,
BASE_URL_TEMPLATE,
ROOT_CONFIG_PATH,
} from './templateGalleryConfig';
import {
ServerJsonRootConfig,
ServerJsonTemplateDefinition,
TemplateGallerySection,
TemplateGalleryStore,
TemplateGalleryTemplateDetailFull,
TemplateGalleryTemplateItem,
} from './types';
const mapRootJsonFromServerToState = (
data: ServerJsonRootConfig,
@ -102,7 +102,7 @@ export const schemaSharingSelectors = {
}
return maybeTemplate;
},
getSchemasForDb: (driver: Driver) => (state: ReduxState) =>
getSchemasForDb: (driver: SupportedDrivers) => (state: ReduxState) =>
state.templateGallery.templates?.sections
.map(section => ({
...section,

View File

@ -3,8 +3,12 @@ import React from 'react';
import { TemplateGalleryBody } from './TemplateGalleryTable';
import { TemplateGalleryModal } from './TemplateGalleryModal';
import { ModalType } from './types';
import { SupportedDrivers } from '../../../../../features/hasura-metadata-types';
const TemplateGallery: React.VFC = () => {
const TemplateGallery: React.VFC<{
showHeader?: boolean;
driver?: SupportedDrivers;
}> = ({ showHeader = true, driver }) => {
const [modalState, setShowModal] = React.useState<ModalType | undefined>(
undefined
);
@ -12,7 +16,11 @@ const TemplateGallery: React.VFC = () => {
return (
<>
<TemplateGalleryBody onModalOpen={setShowModal} />
<TemplateGalleryBody
showHeader={showHeader}
driver={driver ?? undefined}
onModalOpen={setShowModal}
/>
{modalState !== undefined ? (
<TemplateGalleryModal closeModal={closeModal} content={modalState} />
) : null}

View File

@ -1,14 +1,15 @@
import React, { Fragment } from 'react';
import { FaShareSquare, FaSpinner } from 'react-icons/fa';
import { SupportedDrivers } from '../../../../../features/hasura-metadata-types';
import { useAppDispatch, useAppSelector } from '../../../../../storeHooks';
import globalStyles from '../../../../Common/Common.module.scss';
import {
fetchGlobalSchemaSharingConfiguration,
schemaSharingSelectors,
} from './Actions';
import { currentDriver } from '../../../../../dataSources';
import styles from './TemplateGallery.module.scss';
import globalStyles from '../../../../Common/Common.module.scss';
import { modalOpenFn, TemplateGalleryTemplateItem } from './types';
import { TemplateGalleryTemplateItem, modalOpenFn } from './types';
import { currentDriver } from '../../../../../dataSources';
export const TemplateGalleryContentRow: React.VFC<{
template: TemplateGalleryTemplateItem;
@ -26,16 +27,18 @@ export const TemplateGalleryContentRow: React.VFC<{
);
};
export const TemplateGalleryBody: React.VFC<{ onModalOpen: modalOpenFn }> = ({
onModalOpen,
}) => {
export const TemplateGalleryBody: React.VFC<{
onModalOpen: modalOpenFn;
showHeader?: boolean;
driver?: SupportedDrivers;
}> = ({ onModalOpen, showHeader = true, driver = currentDriver }) => {
const dispatch = useAppDispatch();
const globalStatusFetching = useAppSelector(
schemaSharingSelectors.getGlobalConfigState
);
const templateForDb = useAppSelector(
schemaSharingSelectors.getSchemasForDb(currentDriver)
schemaSharingSelectors.getSchemasForDb(driver)
);
if (globalStatusFetching === 'none') {
@ -61,11 +64,13 @@ export const TemplateGalleryBody: React.VFC<{ onModalOpen: modalOpenFn }> = ({
return (
<>
<h2
className={`${globalStyles.heading_text} ${styles.header_table_description} mb-sm`}
>
Template Gallery
</h2>
{showHeader && (
<h2
className={`${globalStyles.heading_text} ${styles.header_table_description} mb-sm`}
>
Template Gallery
</h2>
)}
<p className={styles.mb_none}>
Templates are a utility for applying pre-created sets of SQL migrations
and Hasura metadata.

View File

@ -1,8 +1,8 @@
import React from 'react';
import { useTableDefinition } from './hooks';
import { ManageDatabase } from './ManageDatabase/ManageDatabase';
import { useTableDefinition } from '../hooks';
import { ManageDatabase } from './ManageDatabase';
export const ManageDatabaseContainer = () => {
export const ManageDatabaseRoute = () => {
const urlData = useTableDefinition(window.location);
if (urlData.querystringParseResult === 'error')

View File

@ -1,12 +1,19 @@
import { Analytics, REDACT_EVERYTHING } from '../../Analytics';
import { ManageTrackedRelationshipsContainer } from '../TrackResources/components/ManageTrackedRelationshipsContainer';
//import { MetadataSelectors, useMetadata } from '../../hasura-metadata-api';
import { ManageTrackedTables } from '../ManageTable/components/ManageTrackedTables';
import { BreadCrumbs, SourceName } from './components';
import { ManageTrackedRelationshipsContainer } from '../TrackResources/components/ManageTrackedRelationshipsContainer';
//import TemplateGallery from '../../../components/Services/Data/Schema/TemplateGallery/TemplateGallery';
import { BreadCrumbs, SourceName, CollapsibleResource } from './parts';
export interface ManageDatabaseProps {
dataSourceName: string;
}
//This component has the code for template gallery but is currently commented out until further notice.
export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
// const { data: source } = useMetadata(
// MetadataSelectors.findSource(dataSourceName)
// );
return (
<Analytics name="ManageDatabaseV2" {...REDACT_EVERYTHING}>
<div className="w-full overflow-y-auto bg-gray-50">
@ -14,11 +21,34 @@ export const ManageDatabase = ({ dataSourceName }: ManageDatabaseProps) => {
<BreadCrumbs dataSourceName={dataSourceName} />
<SourceName dataSourceName={dataSourceName} />
</div>
<div className="px-md group relative">
<ManageTrackedTables dataSourceName={dataSourceName} />
<ManageTrackedRelationshipsContainer
dataSourceName={dataSourceName}
/>
<div className="px-md group relative gap-2 flex-col flex">
{/* {(source?.kind === 'postgres' || source?.kind === 'mssql') && (
<CollapsibleResource
title="Template Gallery"
tooltip="Apply pre-created sets of SQL migrations and Hasura metadata."
>
<TemplateGallery driver={source?.kind} showHeader={false} />
</CollapsibleResource>
)} */}
<CollapsibleResource
title="Tables/Views"
tooltip="Expose the tables available in your database via the GraphQL API"
defaultOpen
>
<ManageTrackedTables
dataSourceName={dataSourceName}
key={dataSourceName}
/>
</CollapsibleResource>
<CollapsibleResource
title="Foreign Key Relationships"
tooltip="Track foreign key relationships in your database in your GraphQL API"
>
<ManageTrackedRelationshipsContainer
dataSourceName={dataSourceName}
/>
</CollapsibleResource>
</div>
</div>
</Analytics>

View File

@ -0,0 +1,76 @@
import React from 'react';
import { Tabs } from '../../../../new-components/Tabs';
import clsx from 'clsx';
import Skeleton from 'react-loading-skeleton';
export type TabState = 'tracked' | 'untracked';
type ManageResourceTabsProps = Omit<
React.ComponentProps<typeof Tabs>,
'items' | 'onValueChange'
> & {
items: {
tracked: { amount: number; content: React.ReactNode };
untracked: { amount: number; content: React.ReactNode };
};
onValueChange: (value: TabState) => void;
introText?: string;
isLoading?: boolean;
};
/**
*
* This is a wrapper around the `<Tabs />` component that simplifies and specializes the props API to be used to display tabbed lists of Trackable Resources
*
*/
export const TrackableResourceTabs = ({
items,
onValueChange,
className,
introText,
isLoading,
...rest
}: ManageResourceTabsProps) => {
const { untracked, tracked } = items;
return isLoading ? (
<div className="mx-sm">
<Skeleton count={8} height={25} className="mb-2" />
</div>
) : (
<div data-testid="trackable-resource-tabs" className="mx-sm">
{!!introText && <p className="text-muted my-2">{introText}</p>}
<Tabs
className={clsx('space-y-4', className)}
onValueChange={value => onValueChange(value as TabState)}
items={[
{
value: 'untracked',
label: (
<div className="flex items-center" data-testid="untracked-tab">
Untracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{untracked.amount}
</span>
</div>
),
content: untracked.content,
},
{
value: 'tracked',
label: (
<div className="flex items-center" data-testid="tracked-tab">
Tracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{tracked.amount}
</span>
</div>
),
content: tracked.content,
},
]}
{...rest}
/>
</div>
);
};

View File

@ -1,2 +1 @@
export { BreadCrumbs } from './BreadCrumbs';
export { SourceName } from './SourceName';
export { TrackableResourceTabs } from './TrackableResourceTabs';

View File

@ -0,0 +1,33 @@
import { RiInformationFill } from 'react-icons/ri';
import {
Collapsible,
CollapsibleProps,
} from '../../../../new-components/Collapsible';
import { IconTooltip } from '../../../../new-components/Tooltip';
export const CollapsibleResource: React.FC<
{
title: string;
tooltip: string;
} & Omit<CollapsibleProps, 'triggerChildren'>
> = ({ title, tooltip, children, ...rest }) => (
<Collapsible
triggerChildren={
<div>
<div className="flex mb-1 items-center">
<div className="font-semibold inline-flex items-center text-lg">
{title}
</div>
<IconTooltip
icon={<RiInformationFill />}
message={tooltip}
side="right"
/>
</div>
</div>
}
{...rest}
>
{children}
</Collapsible>
);

View File

@ -3,18 +3,18 @@ import { BsDatabaseFillGear } from 'react-icons/bs';
import { FaRegTrashAlt } from 'react-icons/fa';
import { GrTableAdd } from 'react-icons/gr';
import { MdOutlineCreateNewFolder } from 'react-icons/md';
import { DropDown } from '../../../../../new-components/AdvancedDropDown';
import { DropDown } from '../../../../new-components/AdvancedDropDown';
import {
useDestructiveAlert,
useHasuraAlert,
} from '../../../../../new-components/Alert';
import { Button } from '../../../../../new-components/Button';
import { usePushRoute } from '../../../../ConnectDBRedesign/hooks/usePushRoute';
} from '../../../../new-components/Alert';
import { Button } from '../../../../new-components/Button';
import { usePushRoute } from '../../../ConnectDBRedesign/hooks/usePushRoute';
import {
useCreateDatabaseSchema,
useDeleteDatabaseSchema,
} from '../../../hooks/modify';
import { addMutateAsyncTuple, handleRunSqlError } from '../../../hooks/utils';
} from '../../hooks/modify';
import { addMutateAsyncTuple, handleRunSqlError } from '../../hooks/utils';
type SchemaDropdownProps = {
schemas: string[];

View File

@ -1,6 +1,6 @@
import { useGetDatabaseSchemas } from '../../../Data/hooks/introspection/useDatabaseSchemas';
import { useGetDatabaseSchemas } from '../../hooks/introspection/useDatabaseSchemas';
import { ManageDatabaseProps } from '../ManageDatabase';
import { SchemaDropdown } from './parts/SchemaDropdown';
import { SchemaDropdown } from './SchemaDropdown';
export function SourceName({ dataSourceName }: ManageDatabaseProps) {
const { data } = useGetDatabaseSchemas(dataSourceName);

View File

@ -0,0 +1,4 @@
export { BreadCrumbs } from './BreadCrumbs';
export { CollapsibleResource } from './CollapsibleResource';
export { SchemaDropdown } from './SchemaDropdown';
export { SourceName } from './SourceName';

View File

@ -33,10 +33,12 @@ UntrackedTables.play = async ({ canvasElement }) => {
resetMetadata();
// Wait until it finishes loading
await waitFor(() => canvas.findByTestId('track-tables'), {
await waitFor(() => canvas.findByTestId('trackable-resource-tabs'), {
timeout: 5000,
});
await userEvent.click(canvas.getByTestId('untracked-tab'));
// Verify it correctly displays untracked tables by default
await expect(canvas.getByText('public / Invoice')).toBeInTheDocument();
await expect(canvas.getByText('public / InvoiceLine')).toBeInTheDocument();
@ -58,7 +60,7 @@ Untrack.play = async ({ canvasElement }) => {
resetMetadata();
// Wait until it finishes loading
await waitFor(() => canvas.findByTestId('track-tables'), {
await waitFor(() => canvas.findByTestId('trackable-resource-tabs'), {
timeout: 5000,
});
@ -94,10 +96,12 @@ Track.play = async ({ canvasElement }) => {
resetMetadata();
// Wait until it finishes loading
await waitFor(() => canvas.findByTestId('track-tables'), {
await waitFor(() => canvas.findByTestId('trackable-resource-tabs'), {
timeout: 5000,
});
await userEvent.click(canvas.getByTestId('untracked-tab'));
// Wait for the button to appear on the screen using findBy. Store it in a variable to click it afterwards.
const button = await canvas.findByTestId(`track-public.Invoice`);
@ -131,7 +135,7 @@ TrackedTables.play = async ({ canvasElement }) => {
resetMetadata();
// Wait until it finishes loading
await waitFor(() => canvas.findByTestId('track-tables'), {
await waitFor(() => canvas.findByTestId('trackable-resource-tabs'), {
timeout: 5000,
});

View File

@ -1,32 +1,23 @@
import { RiInformationFill } from 'react-icons/ri';
import { Collapsible } from '../../../../new-components/Collapsible';
import * as Tabs from '@radix-ui/react-tabs';
import { Tooltip } from '../../../../new-components/Tooltip';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { TableList } from '../parts/TableList';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { useMetadata } from '../../../hasura-metadata-api';
import { TrackableResourceTabs } from '../../ManageDatabase/components/TrackableResourceTabs';
import { useIntrospectedTables } from '../../hooks/useIntrospectedTables';
import { TableList } from '../parts/TableList';
import {
adaptTrackedTables,
adaptUntrackedTables,
selectTrackedTables,
} from '../selectors';
import { useIntrospectedTables } from '../../hooks/useIntrospectedTables';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
const classNames = {
selected:
'border-yellow-500 text-yellow-500 whitespace-nowrap p-xs border-b-2 font-semibold -mb-0.5',
unselected:
'border-transparent text-muted whitespace-nowrap p-xs border-b-2 font-semibold -mb-0.5 hover:border-gray-300 hover:text-gray-900',
};
type TabState = 'tracked' | 'untracked';
export const ManageTrackedTables = ({
dataSourceName,
}: {
dataSourceName: string;
}) => {
const [tab, setTab] = React.useState<'tracked' | 'untracked'>('untracked');
const [tab, setTab] = React.useState<TabState>('tracked');
const {
data: metadataTables = [],
@ -50,6 +41,12 @@ export const ManageTrackedTables = ({
}),
enabled: isFetched,
refetchOnWindowFocus: false,
onSuccess: data => {
if (data.trackedTables.length === 0) {
// if user has no tracked tables, switch to the untracked list
setTab('untracked');
}
},
},
});
@ -69,91 +66,37 @@ export const ManageTrackedTables = ({
);
return (
<Collapsible
triggerChildren={
<div>
<div className="flex mb-1 items-center">
<div className="font-semibold inline-flex items-center text-lg text-gray-600">
Tables/Views
</div>
<Tooltip
tooltipContentChildren="Expose the tables available in your database via the GraphQL API"
side="right"
>
<RiInformationFill className="text-muted" />
</Tooltip>
</div>
</div>
<TrackableResourceTabs
introText={
'Tracking tables adds them to your GraphQL API. All objects will be admin-only until permissions have been set.'
}
defaultOpen
>
{!isSuccess ? (
<div className="px-md">
<Skeleton count={8} height={25} className="mb-2" />
</div>
) : (
<Tabs.Root
defaultValue="untracked"
data-testid="track-tables"
className="space-y-4"
onValueChange={value =>
setTab(value === 'tracked' ? 'tracked' : 'untracked')
}
>
<p className="text-muted">
Tracking tables adds them to your GraphQL API. All objects will be
admin-only until permissions have been set.
</p>
<Tabs.List
className="border-b border-gray-300 px-4 flex space-x-4"
aria-label="Tabs"
>
<Tabs.Trigger
value="untracked"
className={
tab === 'untracked'
? classNames.selected
: classNames.unselected
}
>
<div className="flex items-center">
Untracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{untrackedTables.length}
</span>
</div>
</Tabs.Trigger>
<Tabs.Trigger
value="tracked"
className={
tab === 'tracked' ? classNames.selected : classNames.unselected
}
>
<div className="flex items-center">
Tracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{trackedTables.length}
</span>
</div>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tracked" className="px-md">
<TableList
mode={'track'}
dataSourceName={dataSourceName}
tables={trackedTables}
/>
</Tabs.Content>
<Tabs.Content value="untracked" className="px-md">
value={tab}
onValueChange={value => {
setTab(value);
}}
isLoading={!isSuccess}
items={{
untracked: {
amount: untrackedTables.length,
content: (
<TableList
mode={'untrack'}
dataSourceName={dataSourceName}
tables={untrackedTables}
/>
</Tabs.Content>
</Tabs.Root>
)}
</Collapsible>
),
},
tracked: {
amount: trackedTables.length,
content: (
<TableList
mode={'track'}
dataSourceName={dataSourceName}
tables={trackedTables}
/>
),
},
}}
/>
);
};

View File

@ -1,19 +1,20 @@
import { Table } from '../../../hasura-metadata-types';
import { Link } from '../../TrackResources/components/parts/Link';
import { getQualifiedTable } from '../utils';
import { FaTable } from 'react-icons/fa';
export const TableDisplayName = ({
dataSourceName,
table,
onClick,
}: {
onClick?: () => void;
dataSourceName?: string;
table: Table;
}) => {
const tableName = getQualifiedTable(table);
console.log(tableName);
return (
<div>
const content = () => (
<>
<FaTable className="text-sm text-muted mr-xs" />
{dataSourceName ? (
<>
@ -22,6 +23,12 @@ export const TableDisplayName = ({
) : (
<>{tableName.join(' / ')}</>
)}
</div>
</>
);
return onClick ? (
<Link onClick={onClick}>{content()}</Link>
) : (
<div>{content()}</div>
);
};

View File

@ -15,6 +15,7 @@ import { TrackableTable } from '../types';
import { paginate, search } from '../utils';
import { SearchBar } from './SearchBar';
import { TableRow } from './TableRow';
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
import { useTrackTables } from '../../hooks/useTrackTables';
import { hasuraToast } from '../../../../new-components/Toasts';
import { APIError } from '../../../../hooks/error';
@ -94,6 +95,8 @@ export const TableList = (props: TableListProps) => {
reset();
};
const pushRoute = usePushRoute();
if (!tables.length) {
return (
<div className="space-y-4">
@ -182,6 +185,17 @@ export const TableList = (props: TableListProps) => {
checked={checkedIds.includes(table.id)}
reset={reset}
onChange={() => onCheck(table.id)}
onTableNameClick={
mode === 'track'
? () => {
pushRoute(
`data/v2/manage/table/browse?database=${dataSourceName}&table=${encodeURIComponent(
JSON.stringify(table.table)
)}`
);
}
: undefined
}
/>
))}
</CardedTable.TableBody>

View File

@ -1,14 +1,14 @@
import { CustomFieldNames } from '../..';
import { MetadataTable } from '../../../hasura-metadata-types';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import React from 'react';
import { FiSettings } from 'react-icons/fi';
import { useTrackTables } from '../../hooks/useTrackTables';
import { TrackableTable } from '../../TrackResources/types';
import { hasuraToast } from '../../../../new-components/Toasts';
import { CustomFieldNames } from '../..';
import { APIError } from '../../../../hooks/error';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { hasuraToast } from '../../../../new-components/Toasts';
import { MetadataTable } from '../../../hasura-metadata-types';
import { useTrackTables } from '../../hooks/useTrackTables';
import { TableDisplayName } from '../components/TableDisplayName';
import { TrackableTable } from '../types';
interface TableRowProps {
dataSourceName: string;
@ -16,10 +16,18 @@ interface TableRowProps {
checked: boolean;
reset: () => void;
onChange: () => void;
onTableNameClick?: () => void;
}
export const TableRow = React.memo(
({ checked, dataSourceName, table, reset, onChange }: TableRowProps) => {
({
checked,
dataSourceName,
table,
reset,
onChange,
onTableNameClick,
}: TableRowProps) => {
const [showCustomModal, setShowCustomModal] = React.useState(false);
const { trackTables, untrackTables, isLoading } = useTrackTables({
dataSourceName,
@ -89,7 +97,7 @@ export const TableRow = React.memo(
/>
</td>
<CardedTable.TableBodyCell>
<TableDisplayName table={table.table} />
<TableDisplayName onClick={onTableNameClick} table={table.table} />
</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>{table.type}</CardedTable.TableBodyCell>
<CardedTable.TableBodyCell>

View File

@ -1,11 +1,11 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { expect } from '@storybook/jest';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { ManageTrackedRelationships } from '../components/ManageTrackedRelationships';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { Relationship } from '../../../DatabaseRelationships';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { ManageTrackedRelationships } from '../components/ManageTrackedRelationships';
export default {
title: 'Data/Components/ManageTrackedRelationships',
@ -28,9 +28,6 @@ export const Base: ComponentStory<typeof ManageTrackedRelationships> = () => (
Base.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Reset initial metadata to make sure tests start from a clean slate everytime
userEvent.click(await canvas.findByText('Foreign Key Relationships'));
await expect(
canvas.getByText('No untracked relationships found')

View File

@ -1,20 +1,9 @@
import { RiInformationFill } from 'react-icons/ri';
import { Collapsible } from '../../../../new-components/Collapsible';
import * as Tabs from '@radix-ui/react-tabs';
import { Tooltip } from '../../../../new-components/Tooltip';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { TrackedRelationshipsContainer } from './TrackedRelationshipsContainer';
import { UntrackedRelationships } from './UntrackedRelationships';
import { SuggestedRelationshipWithName } from '../../../DatabaseRelationships/components/SuggestedRelationships/hooks/useSuggestedRelationships';
import { Relationship } from '../../../DatabaseRelationships/types';
const classNames = {
selected:
'border-yellow-500 text-yellow-500 whitespace-nowrap p-xs border-b-2 font-semibold -mb-0.5',
unselected:
'border-transparent text-muted whitespace-nowrap p-xs border-b-2 font-semibold -mb-0.5 hover:border-gray-300 hover:text-gray-900',
};
import { TrackableResourceTabs } from '../../ManageDatabase/components/TrackableResourceTabs';
import { TrackedRelationshipsContainer } from './TrackedRelationshipsContainer';
import { UntrackedRelationships } from './UntrackedRelationships';
type ManageTrackedRelationshipsProps = {
dataSourceName: string;
@ -37,80 +26,26 @@ export const ManageTrackedRelationships: React.VFC<
return <div className="px-md">Something went wrong</div>;
return (
<Collapsible
triggerChildren={
<div>
<div className="flex mb-1 items-center">
<div className="font-semibold inline-flex items-center text-lg">
Foreign Key Relationships
</div>
<Tooltip
tooltipContentChildren="Expose the tables available in your database via the GraphQL API"
side="right"
>
<RiInformationFill />
</Tooltip>
</div>
</div>
<TrackableResourceTabs
introText={
'Create and track a relationship to view it in your GraphQL schema.'
}
// defaultOpen
>
<Tabs.Root
defaultValue="untracked"
data-testid="track-relationships"
className="space-y-4"
onValueChange={value =>
setTab(value === 'tracked' ? 'tracked' : 'untracked')
}
>
<p className="text-muted">
Tracking tables adds them to your GraphQL API. All objects will be
admin-only until permissions have been set.
</p>
<Tabs.List
className="border-b border-gray-300 flex space-x-4"
aria-label="Tabs"
>
<Tabs.Trigger
value="untracked"
className={
tab === 'untracked' ? classNames.selected : classNames.unselected
}
>
Untracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{suggestedRelationships.length}
</span>
</Tabs.Trigger>
<Tabs.Trigger
value="tracked"
className={
tab === 'tracked' ? classNames.selected : classNames.unselected
}
>
Tracked
<span className="bg-gray-300 ml-1 px-1.5 py-0.5 rounded text-xs">
{trackedFKRelationships.length}
</span>
</Tabs.Trigger>
</Tabs.List>
{isLoading ? (
<div className="px-md">
<Skeleton count={8} height={25} className="mb-2" />
</div>
) : (
<>
<Tabs.Content value="untracked" className="px-md">
<UntrackedRelationships dataSourceName={dataSourceName} />
</Tabs.Content>
<Tabs.Content value="tracked" className="px-md">
<TrackedRelationshipsContainer dataSourceName={dataSourceName} />
</Tabs.Content>
</>
)}
</Tabs.Root>
</Collapsible>
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

@ -0,0 +1,12 @@
type LinkProps = {
onClick: () => void;
};
export const Link: React.FC<LinkProps> = ({ onClick, children }) => (
<button
onClick={onClick}
className={`text-secondary focus-visible:ring-secondary hover:bg-secondary-light active:bg-secondary active:bg-opacity-25 py-2 px-3 rounded transition-all duration-100`}
>
{children}
</button>
);

View File

@ -1,4 +1,4 @@
export * from './ManageDatabaseContainer';
export * from './ManageDatabase/ManageDatabase.Route';
export * from './components';
export * from './hooks';
export { useTrackTables } from './hooks/useTrackTables';

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
interface TabsItem {
value: string;
label: string;
label: React.ReactNode;
icon?: React.ReactNode;
content: React.ReactNode;
}
@ -23,7 +23,7 @@ export const Tabs: React.FC<TabsProps> = props => {
<RadixTabs.List aria-label="Tabs">
<div className="border-b border-gray-200 bg-legacybg flex space-x-4">
{items.map(({ value: itemValue, label, icon }) => (
<RadixTabs.Trigger key={label} value={itemValue} asChild>
<RadixTabs.Trigger key={itemValue} value={itemValue} asChild>
<button
className={clsx(
'whitespace-nowrap py-xs px-sm border-b-2 font-semibold',