mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Track nested logical models and Logical models details route
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9497 GitOrigin-RevId: bd5b38b0f8ad3f1f317418631af9f85dd14b9983
This commit is contained in:
parent
b309fc9211
commit
e8c67b1bfc
@ -37,9 +37,11 @@ import { TableEditItemContainer } from './TableEditItem/TableEditItemContainer';
|
|||||||
import { TableInsertItemContainer } from './TableInsertItem/TableInsertItemContainer';
|
import { TableInsertItemContainer } from './TableInsertItem/TableInsertItemContainer';
|
||||||
import { ModifyTableContainer } from './TableModify/ModifyTableContainer';
|
import { ModifyTableContainer } from './TableModify/ModifyTableContainer';
|
||||||
import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
|
import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
|
||||||
|
|
||||||
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
||||||
import { LogicalModelPermissionsRoute } from '../../../features/Data/LogicalModels/LogicalModelPermissions/LogicalModelPermissionsPage';
|
import {
|
||||||
|
ViewLogicalModelRoute,
|
||||||
|
LogicalModelPermissionsRoute,
|
||||||
|
} from '../../../features/Data/LogicalModels';
|
||||||
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
|
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
|
||||||
import {
|
import {
|
||||||
UpdateNativeQueryRoute,
|
UpdateNativeQueryRoute,
|
||||||
@ -95,6 +97,7 @@ const makeDataRouter = (
|
|||||||
<IndexRoute component={NativeQueries} />
|
<IndexRoute component={NativeQueries} />
|
||||||
<Redirect from=":source" to="/data/native-queries/logical-models" />
|
<Redirect from=":source" to="/data/native-queries/logical-models" />
|
||||||
<Route path=":source">
|
<Route path=":source">
|
||||||
|
<Route path=":name" component={ViewLogicalModelRoute} />
|
||||||
<Route
|
<Route
|
||||||
path=":name/permissions"
|
path=":name/permissions"
|
||||||
component={LogicalModelPermissionsRoute}
|
component={LogicalModelPermissionsRoute}
|
||||||
|
@ -169,7 +169,7 @@ export const LandingPage = ({ pathname }: { pathname: string }) => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onEditClick={model => {
|
onEditClick={model => {
|
||||||
push?.(
|
push?.(
|
||||||
`/data/native-queries/logical-models/${model.source.name}/${model.name}/permissions`
|
`/data/native-queries/logical-models/${model.source.name}/${model.name}`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onRemoveClick={handleRemoveLogicalModel}
|
onRemoveClick={handleRemoveLogicalModel}
|
||||||
|
@ -8,6 +8,8 @@ import Skeleton from 'react-loading-skeleton';
|
|||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../../new-components/Button';
|
||||||
import { CardedTableFromReactTable } from '../../components/CardedTableFromReactTable';
|
import { CardedTableFromReactTable } from '../../components/CardedTableFromReactTable';
|
||||||
import { LogicalModelWithSource } from '../../types';
|
import { LogicalModelWithSource } from '../../types';
|
||||||
|
import { CgDetailsMore } from 'react-icons/cg';
|
||||||
|
import { FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<LogicalModelWithSource>();
|
const columnHelper = createColumnHelper<LogicalModelWithSource>();
|
||||||
|
|
||||||
@ -39,11 +41,15 @@ export const ListLogicalModels = ({
|
|||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
cell: ({ cell, row }) => (
|
cell: ({ cell, row }) => (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<Button onClick={() => onEditClick(row.original)}>
|
<Button
|
||||||
Edit Permissions
|
icon={<CgDetailsMore />}
|
||||||
|
onClick={() => onEditClick(row.original)}
|
||||||
|
>
|
||||||
|
View
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
mode="destructive"
|
mode="destructive"
|
||||||
|
icon={<FaTrash />}
|
||||||
onClick={() => onRemoveClick(row.original)}
|
onClick={() => onRemoveClick(row.original)}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import { InjectedRouter, withRouter } from 'react-router';
|
||||||
|
import { RouteWrapper } from '../components/RouteWrapper';
|
||||||
|
import { LogicalModelTabs } from '../components/LogicalModelTabs';
|
||||||
|
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||||
|
import { MetadataUtils, useMetadata } from '../../../hasura-metadata-api';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
|
||||||
|
import { logicalModelFieldToFormField } from './utils/logicalModelFieldToFormField';
|
||||||
|
|
||||||
|
export const ViewLogicalModelRoute = withRouter<{
|
||||||
|
location: Location;
|
||||||
|
router: InjectedRouter;
|
||||||
|
params: {
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}>(({ params }) => {
|
||||||
|
if (!params.source || !params.name) {
|
||||||
|
return (
|
||||||
|
<IndicatorCard status="negative">
|
||||||
|
Unable to parse data from url.
|
||||||
|
</IndicatorCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<RouteWrapper
|
||||||
|
route={'/data/native-queries/logical-models/{{source}}/{{name}}'}
|
||||||
|
itemSourceName={params.source}
|
||||||
|
itemName={params.name}
|
||||||
|
>
|
||||||
|
<LogicalModelTabs
|
||||||
|
source={params.source}
|
||||||
|
name={params.name}
|
||||||
|
defaultValue="logical-model"
|
||||||
|
/>
|
||||||
|
</RouteWrapper>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ViewLogicalModelPage = ({
|
||||||
|
source,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
const { data: logicalModels, isLoading } = useMetadata(
|
||||||
|
m => MetadataUtils.findMetadataSource(source, m)?.logical_models
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Skeleton count={10} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logicalModel = logicalModels?.find(l => l.name === name);
|
||||||
|
|
||||||
|
if (!logicalModel) {
|
||||||
|
return (
|
||||||
|
<IndicatorCard status="negative">
|
||||||
|
Logical Model {name} not found in {source}
|
||||||
|
</IndicatorCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-md max-w">
|
||||||
|
<LogicalModelWidget
|
||||||
|
disabled={{
|
||||||
|
name: true,
|
||||||
|
dataSourceName: true,
|
||||||
|
fields: true,
|
||||||
|
callToAction: true,
|
||||||
|
}}
|
||||||
|
defaultValues={{
|
||||||
|
name: logicalModel.name,
|
||||||
|
dataSourceName: source,
|
||||||
|
fields: logicalModel.fields.map(logicalModelFieldToFormField),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { LogicalModel } from '../../../../hasura-metadata-types';
|
||||||
|
import {
|
||||||
|
isArrayLogicalModelType,
|
||||||
|
isLogicalModelType,
|
||||||
|
isScalarFieldType,
|
||||||
|
} from '../../../../hasura-metadata-types/source/typeGuards';
|
||||||
|
import { AddLogicalModelFormData } from '../../LogicalModelWidget/validationSchema';
|
||||||
|
|
||||||
|
export function logicalModelFieldToFormField(
|
||||||
|
f: LogicalModel['fields'][number]
|
||||||
|
): AddLogicalModelFormData['fields'][number] {
|
||||||
|
if (isScalarFieldType(f.type)) {
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
type: f.type.scalar,
|
||||||
|
typeClass: 'scalar',
|
||||||
|
nullable: f.type.nullable,
|
||||||
|
array: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isLogicalModelType(f.type)) {
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
type: f.type.logical_model,
|
||||||
|
typeClass: 'logical_model',
|
||||||
|
nullable: f.type.nullable,
|
||||||
|
array: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isArrayLogicalModelType(f.type)) {
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
type: f.type.array.logical_model,
|
||||||
|
typeClass: 'logical_model',
|
||||||
|
nullable: f.type.array.nullable,
|
||||||
|
array: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('Unknown field type');
|
||||||
|
}
|
@ -25,8 +25,20 @@ const noPermissionArgs: Partial<
|
|||||||
tables: [],
|
tables: [],
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'one', nullable: false, type: 'text' },
|
{
|
||||||
{ name: 'two', nullable: false, type: 'text' },
|
name: 'one',
|
||||||
|
type: {
|
||||||
|
scalar: 'text',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'two',
|
||||||
|
type: {
|
||||||
|
scalar: 'text',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
name: 'hello_world',
|
name: 'hello_world',
|
||||||
},
|
},
|
||||||
@ -47,8 +59,20 @@ const existingPermissionArgs: Partial<
|
|||||||
tables: [],
|
tables: [],
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'one', nullable: false, type: 'text' },
|
{
|
||||||
{ name: 'two', nullable: false, type: 'text' },
|
name: 'one',
|
||||||
|
type: {
|
||||||
|
scalar: 'text',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'two',
|
||||||
|
type: {
|
||||||
|
scalar: 'text',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
name: 'hello_world',
|
name: 'hello_world',
|
||||||
select_permissions: [
|
select_permissions: [
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { InjectedRouter, withRouter } from 'react-router';
|
import { InjectedRouter, withRouter } from 'react-router';
|
||||||
import { RouteWrapper } from '../components/RouteWrapper';
|
import { RouteWrapper } from '../components/RouteWrapper';
|
||||||
import { Tabs } from '../../../../new-components/Tabs';
|
|
||||||
import { LogicalModelPermissions } from './LogicalModelPermissions';
|
import { LogicalModelPermissions } from './LogicalModelPermissions';
|
||||||
import { useCreateLogicalModelsPermissions } from './hooks/useCreateLogicalModelsPermissions';
|
import { useCreateLogicalModelsPermissions } from './hooks/useCreateLogicalModelsPermissions';
|
||||||
import { useRemoveLogicalModelsPermissions } from './hooks/useRemoveLogicalModelsPermissions';
|
import { useRemoveLogicalModelsPermissions } from './hooks/useRemoveLogicalModelsPermissions';
|
||||||
@ -8,6 +7,7 @@ import { useMetadata } from '../../../hasura-metadata-api';
|
|||||||
import { usePermissionComparators } from '../../../Permissions/PermissionsForm/components/RowPermissionsBuilder/hooks/usePermissionComparators';
|
import { usePermissionComparators } from '../../../Permissions/PermissionsForm/components/RowPermissionsBuilder/hooks/usePermissionComparators';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import { extractModelsAndQueriesFromMetadata } from '../../../hasura-metadata-api/selectors';
|
import { extractModelsAndQueriesFromMetadata } from '../../../hasura-metadata-api/selectors';
|
||||||
|
import { LogicalModelTabs } from '../components/LogicalModelTabs';
|
||||||
|
|
||||||
export const LogicalModelPermissionsPage = ({
|
export const LogicalModelPermissionsPage = ({
|
||||||
source,
|
source,
|
||||||
@ -32,58 +32,47 @@ export const LogicalModelPermissionsPage = ({
|
|||||||
logicalModels,
|
logicalModels,
|
||||||
source: logicalModel?.source,
|
source: logicalModel?.source,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div
|
||||||
|
className="mt-md"
|
||||||
// Recreate the key when the logical model permissions change to reset the form
|
// Recreate the key when the logical model permissions change to reset the form
|
||||||
key={logicalModel?.select_permissions?.length}
|
key={logicalModel?.select_permissions?.length}
|
||||||
data-testid="logical-model-permissions-tab"
|
>
|
||||||
defaultValue={'logical-model-permissions'}
|
{isLoading ? (
|
||||||
items={[
|
<div
|
||||||
{
|
className="flex items-center justify-center h-64"
|
||||||
value: 'logical-model-permissions',
|
data-testid="loading-logical-model-permissions"
|
||||||
label: `Permissions`,
|
>
|
||||||
content: (
|
<Skeleton />
|
||||||
<div className="mt-md">
|
</div>
|
||||||
{isLoading ? (
|
) : !logicalModel ? (
|
||||||
<div
|
<div className="flex items-center justify-center h-64">
|
||||||
className="flex items-center justify-center h-64"
|
<span className="text-gray-500">
|
||||||
data-testid="loading-logical-model-permissions"
|
Logical model with name {name} and driver {source} not found
|
||||||
>
|
</span>
|
||||||
<Skeleton />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : !logicalModel ? (
|
<LogicalModelPermissions
|
||||||
<div className="flex items-center justify-center h-64">
|
onSave={async permission => {
|
||||||
<span className="text-gray-500">
|
create({
|
||||||
Logical model with name {name} and driver {source} not found
|
logicalModelName: logicalModel?.name,
|
||||||
</span>
|
permission,
|
||||||
</div>
|
});
|
||||||
) : (
|
}}
|
||||||
<LogicalModelPermissions
|
onDelete={async permission => {
|
||||||
onSave={async permission => {
|
remove({
|
||||||
create({
|
logicalModelName: logicalModel?.name,
|
||||||
logicalModelName: logicalModel?.name,
|
permission,
|
||||||
permission,
|
});
|
||||||
});
|
}}
|
||||||
}}
|
isCreating={isCreating}
|
||||||
onDelete={async permission => {
|
isRemoving={isRemoving}
|
||||||
remove({
|
comparators={comparators}
|
||||||
logicalModelName: logicalModel?.name,
|
logicalModelName={logicalModel?.name}
|
||||||
permission,
|
logicalModels={logicalModels}
|
||||||
});
|
/>
|
||||||
}}
|
)}
|
||||||
isCreating={isCreating}
|
</div>
|
||||||
isRemoving={isRemoving}
|
|
||||||
comparators={comparators}
|
|
||||||
logicalModelName={logicalModel?.name}
|
|
||||||
logicalModels={logicalModels}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,7 +92,11 @@ export const LogicalModelPermissionsRoute = withRouter<{
|
|||||||
itemSourceName={params.source}
|
itemSourceName={params.source}
|
||||||
itemName={params.name}
|
itemName={params.name}
|
||||||
>
|
>
|
||||||
<LogicalModelPermissionsPage source={params.source} name={params.name} />
|
<LogicalModelTabs
|
||||||
|
source={params.source}
|
||||||
|
name={params.name}
|
||||||
|
defaultValue={'logical-model-permissions'}
|
||||||
|
/>
|
||||||
</RouteWrapper>
|
</RouteWrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,36 +1,27 @@
|
|||||||
import { StoryObj, Meta } from '@storybook/react';
|
import { StoryObj, Meta } from '@storybook/react';
|
||||||
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
||||||
import { LogicalModelWidget } from './LogicalModelWidget';
|
import { LogicalModelWidget } from './LogicalModelWidget';
|
||||||
import { ReduxDecorator } from '../../../../storybook/decorators/redux-decorator';
|
|
||||||
import { fireEvent, userEvent, within } from '@storybook/testing-library';
|
import { fireEvent, userEvent, within } from '@storybook/testing-library';
|
||||||
import { handlers } from './mocks/handlers';
|
import { handlers } from './mocks/handlers';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { defaultEmptyValues } from './validationSchema';
|
import { defaultEmptyValues } from './validationSchema';
|
||||||
|
import { waitForRequest } from '../../../../storybook/utils/waitForRequest';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: LogicalModelWidget,
|
component: LogicalModelWidget,
|
||||||
decorators: [
|
decorators: [ReactQueryDecorator()],
|
||||||
ReactQueryDecorator(),
|
|
||||||
ReduxDecorator({
|
|
||||||
tables: {
|
|
||||||
dataHeaders: {
|
|
||||||
'x-hasura-admin-secret': 'myadminsecretkey',
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
} as Meta<typeof LogicalModelWidget>;
|
} as Meta<typeof LogicalModelWidget>;
|
||||||
|
|
||||||
export const DefaultView: StoryObj<typeof LogicalModelWidget> = {
|
export const DefaultView: StoryObj<typeof LogicalModelWidget> = {
|
||||||
render: () => <LogicalModelWidget />,
|
|
||||||
|
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: handlers['200'],
|
msw: handlers['200'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DialogVariant: StoryObj<typeof LogicalModelWidget> = {
|
export const DialogVariant: StoryObj<typeof LogicalModelWidget> = {
|
||||||
render: () => <LogicalModelWidget asDialog />,
|
args: {
|
||||||
|
asDialog: true,
|
||||||
|
},
|
||||||
|
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: handlers['200'],
|
msw: handlers['200'],
|
||||||
@ -39,17 +30,15 @@ export const DialogVariant: StoryObj<typeof LogicalModelWidget> = {
|
|||||||
|
|
||||||
export const PreselectedAndDisabledInputs: StoryObj<typeof LogicalModelWidget> =
|
export const PreselectedAndDisabledInputs: StoryObj<typeof LogicalModelWidget> =
|
||||||
{
|
{
|
||||||
render: () => (
|
args: {
|
||||||
<LogicalModelWidget
|
defaultValues: {
|
||||||
defaultValues={{
|
...defaultEmptyValues,
|
||||||
...defaultEmptyValues,
|
dataSourceName: 'chinook',
|
||||||
dataSourceName: 'chinook',
|
},
|
||||||
}}
|
disabled: {
|
||||||
disabled={{
|
dataSourceName: true,
|
||||||
dataSourceName: true,
|
},
|
||||||
}}
|
},
|
||||||
/>
|
|
||||||
),
|
|
||||||
|
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: handlers['200'],
|
msw: handlers['200'],
|
||||||
@ -57,8 +46,6 @@ export const PreselectedAndDisabledInputs: StoryObj<typeof LogicalModelWidget> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BasicUserFlow: StoryObj<typeof LogicalModelWidget> = {
|
export const BasicUserFlow: StoryObj<typeof LogicalModelWidget> = {
|
||||||
render: () => <LogicalModelWidget />,
|
|
||||||
|
|
||||||
name: '🧪 Basic user flow',
|
name: '🧪 Basic user flow',
|
||||||
|
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -79,14 +66,17 @@ export const BasicUserFlow: StoryObj<typeof LogicalModelWidget> = {
|
|||||||
|
|
||||||
await userEvent.type(canvas.getByTestId('fields[0].name'), 'id');
|
await userEvent.type(canvas.getByTestId('fields[0].name'), 'id');
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
canvas.getByTestId('fields[0].type'),
|
canvas.getByTestId('fields-input-type-0'),
|
||||||
'integer'
|
'scalar:integer'
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(canvas.getByText('Add Field'));
|
fireEvent.click(canvas.getByText('Add Field'));
|
||||||
|
|
||||||
await userEvent.type(canvas.getByTestId('fields[1].name'), 'name');
|
await userEvent.type(canvas.getByTestId('fields[1].name'), 'name');
|
||||||
await userEvent.selectOptions(canvas.getByTestId('fields[1].type'), 'text');
|
await userEvent.selectOptions(
|
||||||
|
canvas.getByTestId('fields-input-type-1'),
|
||||||
|
'scalar:text'
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(canvas.getByText('Create Logical Model'));
|
fireEvent.click(canvas.getByText('Create Logical Model'));
|
||||||
|
|
||||||
@ -97,7 +87,6 @@ export const BasicUserFlow: StoryObj<typeof LogicalModelWidget> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NetworkErrorOnSubmit: StoryObj<typeof LogicalModelWidget> = {
|
export const NetworkErrorOnSubmit: StoryObj<typeof LogicalModelWidget> = {
|
||||||
render: () => <LogicalModelWidget />,
|
|
||||||
name: '🧪 Network Error On Submit',
|
name: '🧪 Network Error On Submit',
|
||||||
|
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -118,14 +107,17 @@ export const NetworkErrorOnSubmit: StoryObj<typeof LogicalModelWidget> = {
|
|||||||
|
|
||||||
await userEvent.type(canvas.getByTestId('fields[0].name'), 'id');
|
await userEvent.type(canvas.getByTestId('fields[0].name'), 'id');
|
||||||
await userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
canvas.getByTestId('fields[0].type'),
|
canvas.getByTestId('fields-input-type-0'),
|
||||||
'integer'
|
'scalar:integer'
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(canvas.getByText('Add Field'));
|
fireEvent.click(canvas.getByText('Add Field'));
|
||||||
|
|
||||||
await userEvent.type(canvas.getByTestId('fields[1].name'), 'name');
|
await userEvent.type(canvas.getByTestId('fields[1].name'), 'name');
|
||||||
await userEvent.selectOptions(canvas.getByTestId('fields[1].type'), 'text');
|
await userEvent.selectOptions(
|
||||||
|
canvas.getByTestId('fields-input-type-1'),
|
||||||
|
'scalar:text'
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(canvas.getByText('Create Logical Model'));
|
fireEvent.click(canvas.getByText('Create Logical Model'));
|
||||||
|
|
||||||
@ -134,3 +126,134 @@ export const NetworkErrorOnSubmit: StoryObj<typeof LogicalModelWidget> = {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultLogicalModelValues = {
|
||||||
|
name: 'Logical Model',
|
||||||
|
dataSourceName: 'chinook',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'integer',
|
||||||
|
array: false,
|
||||||
|
nullable: false,
|
||||||
|
typeClass: 'scalar' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested',
|
||||||
|
type: 'logical_model_1',
|
||||||
|
array: false,
|
||||||
|
nullable: false,
|
||||||
|
typeClass: 'logical_model' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested_array',
|
||||||
|
type: 'logical_model_2',
|
||||||
|
array: true,
|
||||||
|
nullable: false,
|
||||||
|
typeClass: 'logical_model' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NestedLogicalModels: StoryObj<typeof LogicalModelWidget> = {
|
||||||
|
args: {
|
||||||
|
defaultValues: defaultLogicalModelValues,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: handlers['200'],
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
// Establish a request listener but don't resolve it yet.
|
||||||
|
const pendingRequest = waitForRequest(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8080/v1/metadata',
|
||||||
|
'_track_logical_model'
|
||||||
|
);
|
||||||
|
fireEvent.click(await canvas.findByText('Edit Logical Model'));
|
||||||
|
// Await the request and get its reference.
|
||||||
|
const request = await pendingRequest;
|
||||||
|
const payload = await request.clone().json();
|
||||||
|
expect(payload.args.fields).toMatchObject([
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: {
|
||||||
|
scalar: 'integer',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested',
|
||||||
|
type: {
|
||||||
|
logical_model: 'logical_model_1',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested_array',
|
||||||
|
type: {
|
||||||
|
array: {
|
||||||
|
logical_model: 'logical_model_2',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditingNestedLogicalModels: StoryObj<typeof LogicalModelWidget> = {
|
||||||
|
args: {
|
||||||
|
defaultValues: defaultLogicalModelValues,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: handlers['200'],
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
// Establish a request listener but don't resolve it yet.
|
||||||
|
const pendingRequest = waitForRequest(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8080/v1/metadata',
|
||||||
|
'_track_logical_model'
|
||||||
|
);
|
||||||
|
// Change from logical model to scalar, and then back should result in the same initial logical model type
|
||||||
|
await userEvent.click(await canvas.findByTestId('fields-input-type-1'));
|
||||||
|
// Set array to true
|
||||||
|
await userEvent.click(await canvas.findByTestId('fields-input-array-1'));
|
||||||
|
// Change it to scalar
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
await canvas.findByTestId('fields-input-type-1'),
|
||||||
|
'scalar:integer'
|
||||||
|
);
|
||||||
|
fireEvent.click(await canvas.findByText('Edit Logical Model'));
|
||||||
|
// Await the request and get its reference.
|
||||||
|
const request = await pendingRequest;
|
||||||
|
const payload = await request.clone().json();
|
||||||
|
expect(payload.args.fields).toMatchObject([
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: {
|
||||||
|
scalar: 'integer',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested',
|
||||||
|
type: {
|
||||||
|
scalar: 'integer',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested_array',
|
||||||
|
type: {
|
||||||
|
array: {
|
||||||
|
logical_model: 'logical_model_2',
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -19,13 +19,20 @@ import {
|
|||||||
AddLogicalModelFormData,
|
AddLogicalModelFormData,
|
||||||
addLogicalModelValidationSchema,
|
addLogicalModelValidationSchema,
|
||||||
} from './validationSchema';
|
} from './validationSchema';
|
||||||
|
import { extractModelsAndQueriesFromMetadata } from '../../../hasura-metadata-api/selectors';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { formFieldToLogicalModelField } from './mocks/utils/formFieldToLogicalModelField';
|
||||||
import { useEnvironmentState } from '../../../ConnectDBRedesign/hooks';
|
import { useEnvironmentState } from '../../../ConnectDBRedesign/hooks';
|
||||||
|
|
||||||
export type AddLogicalModelDialogProps = {
|
export type AddLogicalModelDialogProps = {
|
||||||
defaultValues?: AddLogicalModelFormData;
|
defaultValues?: AddLogicalModelFormData;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
disabled?: CreateBooleanMap<AddLogicalModelFormData>;
|
disabled?: CreateBooleanMap<
|
||||||
|
AddLogicalModelFormData & {
|
||||||
|
callToAction?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
asDialog?: boolean;
|
asDialog?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,9 +91,19 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: modelsAndQueries } = useMetadata(m =>
|
||||||
|
extractModelsAndQueriesFromMetadata(m)
|
||||||
|
);
|
||||||
|
|
||||||
|
const logicalModels = modelsAndQueries?.models || [];
|
||||||
|
|
||||||
const onSubmit = (data: AddLogicalModelFormData) => {
|
const onSubmit = (data: AddLogicalModelFormData) => {
|
||||||
trackLogicalModel({
|
trackLogicalModel({
|
||||||
data,
|
data: {
|
||||||
|
dataSourceName: data.dataSourceName,
|
||||||
|
name: data.name,
|
||||||
|
fields: data.fields.map(formFieldToLogicalModelField),
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
hasuraToast({
|
hasuraToast({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -116,18 +133,38 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => {
|
|||||||
return isMetadataLoading || isIntrospectionLoading ? (
|
return isMetadataLoading || isIntrospectionLoading ? (
|
||||||
<Skeleton count={8} height={20} />
|
<Skeleton count={8} height={20} />
|
||||||
) : (
|
) : (
|
||||||
<Form onSubmit={onSubmit}>
|
<>
|
||||||
<LogicalModelFormInputs
|
{isEditMode && (
|
||||||
sourceOptions={sourceOptions}
|
<IndicatorCard status="info">
|
||||||
typeOptions={typeOptions}
|
The current release does not support editing Logical Models. This
|
||||||
disabled={props.disabled}
|
feature will be available in a future release. You can still{' '}
|
||||||
/>
|
<Link
|
||||||
<div className="flex justify-end">
|
to={`/data/native-queries/logical-models/${props.defaultValues?.dataSourceName}/${props.defaultValues?.name}/permissions`}
|
||||||
<Button type="submit" mode="primary" isLoading={isLoading}>
|
>
|
||||||
{isEditMode ? 'Edit Logical Model' : 'Create Logical Model'}
|
edit permissions
|
||||||
</Button>
|
</Link>{' '}
|
||||||
</div>
|
or edit logical models directly by modifying the Metadata.
|
||||||
</Form>
|
</IndicatorCard>
|
||||||
|
)}
|
||||||
|
<Form onSubmit={onSubmit}>
|
||||||
|
<LogicalModelFormInputs
|
||||||
|
sourceOptions={sourceOptions}
|
||||||
|
typeOptions={typeOptions}
|
||||||
|
disabled={props.disabled}
|
||||||
|
logicalModels={logicalModels}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
disabled={props.disabled?.callToAction}
|
||||||
|
type="submit"
|
||||||
|
mode="primary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{isEditMode ? 'Edit Logical Model' : 'Create Logical Model'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -160,6 +197,7 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => {
|
|||||||
sourceOptions={sourceOptions}
|
sourceOptions={sourceOptions}
|
||||||
typeOptions={typeOptions}
|
typeOptions={typeOptions}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
logicalModels={logicalModels}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
@ -10,7 +10,6 @@ import { FaPlusCircle } from 'react-icons/fa';
|
|||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../../new-components/Button';
|
||||||
import {
|
import {
|
||||||
GraphQLSanitizedInputField,
|
GraphQLSanitizedInputField,
|
||||||
Select,
|
|
||||||
fieldLabelStyles,
|
fieldLabelStyles,
|
||||||
} from '../../../../../new-components/Form';
|
} from '../../../../../new-components/Form';
|
||||||
import { BooleanInput } from '../../components/BooleanInput';
|
import { BooleanInput } from '../../components/BooleanInput';
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
AddLogicalModelField,
|
AddLogicalModelField,
|
||||||
AddLogicalModelFormData,
|
AddLogicalModelFormData,
|
||||||
} from '../validationSchema';
|
} from '../validationSchema';
|
||||||
|
import { LogicalModel } from '../../../../hasura-metadata-types';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<AddLogicalModelField>();
|
const columnHelper = createColumnHelper<AddLogicalModelField>();
|
||||||
|
|
||||||
@ -26,12 +26,15 @@ export const FieldsInput = ({
|
|||||||
name,
|
name,
|
||||||
types,
|
types,
|
||||||
disabled,
|
disabled,
|
||||||
|
logicalModels,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
types: string[];
|
types: string[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
}) => {
|
}) => {
|
||||||
const { control } = useFormContext<AddLogicalModelFormData>();
|
const { control, setValue, watch } =
|
||||||
|
useFormContext<AddLogicalModelFormData>();
|
||||||
|
|
||||||
const { append, remove, fields } = useFieldArray({
|
const { append, remove, fields } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@ -58,18 +61,58 @@ export const FieldsInput = ({
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor('type', {
|
columnHelper.accessor('type', {
|
||||||
id: 'type',
|
id: 'type',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Select
|
const typeValue = watch(`fields.${row.index}.type`);
|
||||||
noErrorPlaceholder
|
const typeClassValue = watch(`fields.${row.index}.typeClass`);
|
||||||
dataTestId={`${name}[${row.index}].type`}
|
return (
|
||||||
name={`fields.${row.index}.type`}
|
<select
|
||||||
options={types.map(t => ({ label: t, value: t }))}
|
className={clsx(
|
||||||
disabled={disabled}
|
'block w-full h-input shadow-sm rounded 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',
|
||||||
/>
|
'text-black',
|
||||||
),
|
disabled
|
||||||
|
? 'cursor-not-allowed bg-gray-200 border-gray-200 hover:border-gray-200'
|
||||||
|
: 'hover:border-gray-400'
|
||||||
|
)}
|
||||||
|
value={`${typeClassValue}:${typeValue}`}
|
||||||
|
data-testid={`fields-input-type-${row.index}`}
|
||||||
|
onChange={e => {
|
||||||
|
const [typeClass, selectedValue] = e.target.value.split(':');
|
||||||
|
setValue(`fields.${row.index}.type`, selectedValue);
|
||||||
|
setValue(`fields.${row.index}.typeClass`, typeClass as any);
|
||||||
|
if (typeClass === 'scalar') {
|
||||||
|
setValue(`fields.${row.index}.array`, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<option value="" data-default-selected hidden>
|
||||||
|
Select a type
|
||||||
|
</option>
|
||||||
|
<optgroup
|
||||||
|
label="Logical Models"
|
||||||
|
data-testid={`fields-input-type-${row.index}-logical-models`}
|
||||||
|
>
|
||||||
|
{logicalModels.map(l => (
|
||||||
|
<option key={l.name} value={`logical_model:${l.name}`}>
|
||||||
|
{l.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup
|
||||||
|
label="Types"
|
||||||
|
data-testid={`fields-input-type-${row.index}-scalars`}
|
||||||
|
>
|
||||||
|
{types.map(t => (
|
||||||
|
<option key={t} value={`scalar:${t}`}>
|
||||||
|
{t}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
},
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor('nullable', {
|
columnHelper.accessor('nullable', {
|
||||||
id: 'nullable',
|
id: 'nullable',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@ -80,6 +123,20 @@ export const FieldsInput = ({
|
|||||||
),
|
),
|
||||||
header: 'Nullable',
|
header: 'Nullable',
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor('array', {
|
||||||
|
id: 'array',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const typeClassValue = watch(`fields.${row.index}.typeClass`);
|
||||||
|
return (
|
||||||
|
<BooleanInput
|
||||||
|
disabled={disabled || typeClassValue === 'scalar'}
|
||||||
|
name={`fields.${row.index}.array`}
|
||||||
|
dataTestId={`fields-input-array-${row.index}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
header: 'Array',
|
||||||
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: 'action',
|
id: 'action',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
@ -115,9 +172,15 @@ export const FieldsInput = ({
|
|||||||
<div className={clsx(fieldLabelStyles, 'mb-0')}>Fields</div>
|
<div className={clsx(fieldLabelStyles, 'mb-0')}>Fields</div>
|
||||||
<Button
|
<Button
|
||||||
icon={<FaPlusCircle />}
|
icon={<FaPlusCircle />}
|
||||||
disabled={!types || types.length === 0}
|
disabled={disabled || !types || types.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
append({ name: '', type: 'text', nullable: true });
|
append({
|
||||||
|
name: '',
|
||||||
|
type: 'text',
|
||||||
|
typeClass: 'scalar',
|
||||||
|
nullable: true,
|
||||||
|
array: false,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Field
|
Add Field
|
||||||
|
@ -23,7 +23,11 @@ export default {
|
|||||||
|
|
||||||
export const Basic: StoryFn<typeof LogicalModelFormInputs> = () => (
|
export const Basic: StoryFn<typeof LogicalModelFormInputs> = () => (
|
||||||
<SimpleForm schema={addLogicalModelValidationSchema} onSubmit={() => {}}>
|
<SimpleForm schema={addLogicalModelValidationSchema} onSubmit={() => {}}>
|
||||||
<LogicalModelFormInputs sourceOptions={[]} typeOptions={[]} />
|
<LogicalModelFormInputs
|
||||||
|
logicalModels={[]}
|
||||||
|
sourceOptions={[]}
|
||||||
|
typeOptions={[]}
|
||||||
|
/>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,10 +43,12 @@ export const WithDefaultValues: StoryObj<typeof LogicalModelFormInputs> = {
|
|||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
|
typeClass: 'scalar',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'first_name',
|
name: 'first_name',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
typeClass: 'scalar',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
name: 'foobar',
|
name: 'foobar',
|
||||||
@ -51,6 +57,7 @@ export const WithDefaultValues: StoryObj<typeof LogicalModelFormInputs> = {
|
|||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
>
|
>
|
||||||
<LogicalModelFormInputs
|
<LogicalModelFormInputs
|
||||||
|
logicalModels={[]}
|
||||||
sourceOptions={[{ value: 'chinook', label: 'chinook' }]}
|
sourceOptions={[{ value: 'chinook', label: 'chinook' }]}
|
||||||
typeOptions={['text', 'int']}
|
typeOptions={['text', 'int']}
|
||||||
/>
|
/>
|
||||||
@ -63,14 +70,14 @@ export const WithDefaultValues: StoryObj<typeof LogicalModelFormInputs> = {
|
|||||||
|
|
||||||
await expect(await canvas.findByTestId('name')).toHaveValue('foobar');
|
await expect(await canvas.findByTestId('name')).toHaveValue('foobar');
|
||||||
await expect(await canvas.findByTestId('fields[0].name')).toHaveValue('id');
|
await expect(await canvas.findByTestId('fields[0].name')).toHaveValue('id');
|
||||||
await expect(await canvas.findByTestId('fields[0].type')).toHaveValue(
|
await expect(await canvas.findByTestId('fields-input-type-0')).toHaveValue(
|
||||||
'int'
|
'scalar:int'
|
||||||
);
|
);
|
||||||
await expect(await canvas.findByTestId('fields[1].name')).toHaveValue(
|
await expect(await canvas.findByTestId('fields[1].name')).toHaveValue(
|
||||||
'first_name'
|
'first_name'
|
||||||
);
|
);
|
||||||
await expect(await canvas.findByTestId('fields[1].type')).toHaveValue(
|
await expect(await canvas.findByTestId('fields-input-type-1')).toHaveValue(
|
||||||
'text'
|
'scalar:text'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
GraphQLSanitizedInputField,
|
GraphQLSanitizedInputField,
|
||||||
Select,
|
Select,
|
||||||
} from '../../../../../new-components/Form';
|
} from '../../../../../new-components/Form';
|
||||||
|
import { LogicalModel } from '../../../../hasura-metadata-types';
|
||||||
import { AddLogicalModelFormData } from '../validationSchema';
|
import { AddLogicalModelFormData } from '../validationSchema';
|
||||||
import { FieldsInput } from './FieldsInput';
|
import { FieldsInput } from './FieldsInput';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export type LogicalModelFormProps = {
|
|||||||
sourceOptions: SelectItem[];
|
sourceOptions: SelectItem[];
|
||||||
typeOptions: string[];
|
typeOptions: string[];
|
||||||
disabled?: CreateBooleanMap<AddLogicalModelFormData>;
|
disabled?: CreateBooleanMap<AddLogicalModelFormData>;
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogicalModelFormInputs = (props: LogicalModelFormProps) => {
|
export const LogicalModelFormInputs = (props: LogicalModelFormProps) => {
|
||||||
@ -36,6 +38,7 @@ export const LogicalModelFormInputs = (props: LogicalModelFormProps) => {
|
|||||||
name="fields"
|
name="fields"
|
||||||
types={props.typeOptions}
|
types={props.typeOptions}
|
||||||
disabled={props.disabled?.fields}
|
disabled={props.disabled?.fields}
|
||||||
|
logicalModels={props.logicalModels}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { LogicalModel } from '../../../../../hasura-metadata-types';
|
||||||
|
import { AddLogicalModelFormData } from '../../validationSchema';
|
||||||
|
|
||||||
|
export function formFieldToLogicalModelField(
|
||||||
|
field: AddLogicalModelFormData['fields'][number]
|
||||||
|
): LogicalModel['fields'][number] {
|
||||||
|
let type: LogicalModel['fields'][number]['type'];
|
||||||
|
if (field.typeClass === 'scalar') {
|
||||||
|
type = {
|
||||||
|
scalar: field.type,
|
||||||
|
nullable: field.nullable,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (field.array) {
|
||||||
|
type = {
|
||||||
|
array: {
|
||||||
|
logical_model: field.type,
|
||||||
|
nullable: field.nullable,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
type = {
|
||||||
|
logical_model: field.type,
|
||||||
|
nullable: field.nullable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: field.name,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
}
|
@ -7,6 +7,8 @@ export const addLogicalModelValidationSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, 'Field Name is a required field'),
|
name: z.string().min(1, 'Field Name is a required field'),
|
||||||
type: z.string().min(1, 'Type is a required field'),
|
type: z.string().min(1, 'Type is a required field'),
|
||||||
|
typeClass: z.enum(['scalar', 'logical_model']),
|
||||||
|
array: z.boolean(),
|
||||||
nullable: z.boolean({ required_error: 'Nullable is a required field' }),
|
nullable: z.boolean({ required_error: 'Nullable is a required field' }),
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Tabs } from '../../../../new-components/Tabs';
|
||||||
|
import { ViewLogicalModelPage } from '../LogicalModel/ViewLogicalModelPage';
|
||||||
|
import { LogicalModelPermissionsPage } from '../LogicalModelPermissions/LogicalModelPermissionsPage';
|
||||||
|
|
||||||
|
export const LogicalModelTabs = ({
|
||||||
|
defaultValue,
|
||||||
|
source,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
defaultValue: 'logical-model' | 'logical-model-permissions';
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
data-testid="logical-model-tabs"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
value: 'logical-model',
|
||||||
|
label: `Logical Model`,
|
||||||
|
content: <ViewLogicalModelPage source={source} name={name} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'logical-model-permissions',
|
||||||
|
label: `Permissions`,
|
||||||
|
content: <LogicalModelPermissionsPage source={source} name={name} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export { LogicalModelPermissionsRoute } from './LogicalModelPermissions/LogicalModelPermissionsPage';
|
||||||
|
export { ViewLogicalModelRoute } from './LogicalModel/ViewLogicalModelPage';
|
@ -1,7 +1,20 @@
|
|||||||
|
export type ScalarFieldType = {
|
||||||
|
scalar: string;
|
||||||
|
nullable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogicalModelType = {
|
||||||
|
logical_model: string;
|
||||||
|
nullable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArrayLogicalModelType = {
|
||||||
|
array: LogicalModelType;
|
||||||
|
};
|
||||||
|
|
||||||
export type LogicalModelField = {
|
export type LogicalModelField = {
|
||||||
name: string;
|
name: string;
|
||||||
nullable: boolean;
|
type: ScalarFieldType | LogicalModelType | ArrayLogicalModelType;
|
||||||
type: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogicalModel = {
|
export type LogicalModel = {
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
ArrayLogicalModelType,
|
||||||
|
LogicalModelField,
|
||||||
|
LogicalModelType,
|
||||||
|
ScalarFieldType,
|
||||||
|
} from './logicalModel';
|
||||||
|
|
||||||
|
export const isScalarFieldType = (
|
||||||
|
fieldType: LogicalModelField['type']
|
||||||
|
): fieldType is ScalarFieldType => {
|
||||||
|
return 'scalar' in fieldType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLogicalModelType = (
|
||||||
|
fieldType: LogicalModelField['type']
|
||||||
|
): fieldType is LogicalModelType => {
|
||||||
|
return 'logical_model' in fieldType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isArrayLogicalModelType = (
|
||||||
|
fieldType: LogicalModelField['type']
|
||||||
|
): fieldType is ArrayLogicalModelType => {
|
||||||
|
return 'array' in fieldType;
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import { getWorker } from 'msw-storybook-addon';
|
||||||
|
import { MockedRequest, matchRequestUrl } from 'msw';
|
||||||
|
|
||||||
|
// https://mswjs.io/docs/extensions/life-cycle-events#asserting-request-payload
|
||||||
|
export function waitForRequest(method: string, url: string, suffix: string) {
|
||||||
|
const worker = getWorker();
|
||||||
|
let requestId = '';
|
||||||
|
|
||||||
|
return new Promise<MockedRequest>((resolve, reject) => {
|
||||||
|
worker.events.on('request:start', async req => {
|
||||||
|
const matchesMethod = req.method.toLowerCase() === method.toLowerCase();
|
||||||
|
const matchesUrl = matchRequestUrl(req.url, url).matches;
|
||||||
|
try {
|
||||||
|
// Clone to avoid "locked body stream" error
|
||||||
|
// https://stackoverflow.com/a/54115314
|
||||||
|
const body = await req.clone().json();
|
||||||
|
const matchesSuffix = body.type.endsWith(suffix);
|
||||||
|
|
||||||
|
if (matchesMethod && matchesUrl && matchesSuffix) {
|
||||||
|
requestId = req.id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.events.on('request:match', req => {
|
||||||
|
if (req.id === requestId) {
|
||||||
|
resolve(req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.events.on('request:unhandled', req => {
|
||||||
|
if (req.id === requestId) {
|
||||||
|
reject(
|
||||||
|
new Error(`The ${req.method} ${req.url.href} request was unhandled.`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user