mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
63fe16604e
commit
ffe4bc7d1c
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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.
|
||||
|
@ -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')
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,2 +1 @@
|
||||
export { BreadCrumbs } from './BreadCrumbs';
|
||||
export { SourceName } from './SourceName';
|
||||
export { TrackableResourceTabs } from './TrackableResourceTabs';
|
||||
|
@ -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>
|
||||
);
|
@ -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[];
|
@ -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);
|
@ -0,0 +1,4 @@
|
||||
export { BreadCrumbs } from './BreadCrumbs';
|
||||
export { CollapsibleResource } from './CollapsibleResource';
|
||||
export { SchemaDropdown } from './SchemaDropdown';
|
||||
export { SourceName } from './SourceName';
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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')
|
||||
|
@ -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} />,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
@ -1,4 +1,4 @@
|
||||
export * from './ManageDatabaseContainer';
|
||||
export * from './ManageDatabase/ManageDatabase.Route';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export { useTrackTables } from './hooks/useTrackTables';
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user