diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js index 69763632baa..fbaa985e18a 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js @@ -37,9 +37,11 @@ import { TableEditItemContainer } from './TableEditItem/TableEditItemContainer'; import { TableInsertItemContainer } from './TableInsertItem/TableInsertItemContainer'; import { ModifyTableContainer } from './TableModify/ModifyTableContainer'; import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage'; - 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 { UpdateNativeQueryRoute, @@ -95,6 +97,7 @@ const makeDataRouter = ( + { isLoading={isLoading} onEditClick={model => { push?.( - `/data/native-queries/logical-models/${model.source.name}/${model.name}/permissions` + `/data/native-queries/logical-models/${model.source.name}/${model.name}` ); }} onRemoveClick={handleRemoveLogicalModel} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LandingPage/components/ListLogicalModels.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LandingPage/components/ListLogicalModels.tsx index 994efa63764..c8203800463 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LandingPage/components/ListLogicalModels.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LandingPage/components/ListLogicalModels.tsx @@ -8,6 +8,8 @@ import Skeleton from 'react-loading-skeleton'; import { Button } from '../../../../../new-components/Button'; import { CardedTableFromReactTable } from '../../components/CardedTableFromReactTable'; import { LogicalModelWithSource } from '../../types'; +import { CgDetailsMore } from 'react-icons/cg'; +import { FaTrash } from 'react-icons/fa'; const columnHelper = createColumnHelper(); @@ -39,11 +41,15 @@ export const ListLogicalModels = ({ header: 'Actions', cell: ({ cell, row }) => (
-
- ), - }, - ]} - /> + > + {isLoading ? ( +
+ +
+ ) : !logicalModel ? ( +
+ + Logical model with name {name} and driver {source} not found + +
+ ) : ( + { + create({ + logicalModelName: logicalModel?.name, + permission, + }); + }} + onDelete={async permission => { + remove({ + logicalModelName: logicalModel?.name, + permission, + }); + }} + isCreating={isCreating} + isRemoving={isRemoving} + comparators={comparators} + logicalModelName={logicalModel?.name} + logicalModels={logicalModels} + /> + )} + ); }; @@ -103,7 +92,11 @@ export const LogicalModelPermissionsRoute = withRouter<{ itemSourceName={params.source} itemName={params.name} > - + ); }); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.stories.tsx index 700eb41ede6..d711c7cefd7 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.stories.tsx @@ -1,36 +1,27 @@ import { StoryObj, Meta } from '@storybook/react'; import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; import { LogicalModelWidget } from './LogicalModelWidget'; -import { ReduxDecorator } from '../../../../storybook/decorators/redux-decorator'; import { fireEvent, userEvent, within } from '@storybook/testing-library'; import { handlers } from './mocks/handlers'; import { expect } from '@storybook/jest'; import { defaultEmptyValues } from './validationSchema'; +import { waitForRequest } from '../../../../storybook/utils/waitForRequest'; export default { component: LogicalModelWidget, - decorators: [ - ReactQueryDecorator(), - ReduxDecorator({ - tables: { - dataHeaders: { - 'x-hasura-admin-secret': 'myadminsecretkey', - } as any, - }, - }), - ], + decorators: [ReactQueryDecorator()], } as Meta; export const DefaultView: StoryObj = { - render: () => , - parameters: { msw: handlers['200'], }, }; export const DialogVariant: StoryObj = { - render: () => , + args: { + asDialog: true, + }, parameters: { msw: handlers['200'], @@ -39,17 +30,15 @@ export const DialogVariant: StoryObj = { export const PreselectedAndDisabledInputs: StoryObj = { - render: () => ( - - ), + args: { + defaultValues: { + ...defaultEmptyValues, + dataSourceName: 'chinook', + }, + disabled: { + dataSourceName: true, + }, + }, parameters: { msw: handlers['200'], @@ -57,8 +46,6 @@ export const PreselectedAndDisabledInputs: StoryObj = }; export const BasicUserFlow: StoryObj = { - render: () => , - name: '🧪 Basic user flow', parameters: { @@ -79,14 +66,17 @@ export const BasicUserFlow: StoryObj = { await userEvent.type(canvas.getByTestId('fields[0].name'), 'id'); await userEvent.selectOptions( - canvas.getByTestId('fields[0].type'), - 'integer' + canvas.getByTestId('fields-input-type-0'), + 'scalar:integer' ); fireEvent.click(canvas.getByText('Add Field')); 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')); @@ -97,7 +87,6 @@ export const BasicUserFlow: StoryObj = { }; export const NetworkErrorOnSubmit: StoryObj = { - render: () => , name: '🧪 Network Error On Submit', parameters: { @@ -118,14 +107,17 @@ export const NetworkErrorOnSubmit: StoryObj = { await userEvent.type(canvas.getByTestId('fields[0].name'), 'id'); await userEvent.selectOptions( - canvas.getByTestId('fields[0].type'), - 'integer' + canvas.getByTestId('fields-input-type-0'), + 'scalar:integer' ); fireEvent.click(canvas.getByText('Add Field')); 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')); @@ -134,3 +126,134 @@ export const NetworkErrorOnSubmit: StoryObj = { ).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 = { + 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 = { + 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, + }, + }, + }, + ]); + }, +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.tsx index b9edea9433b..6e21fb1a5a2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/LogicalModelWidget.tsx @@ -19,13 +19,20 @@ import { AddLogicalModelFormData, addLogicalModelValidationSchema, } 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'; export type AddLogicalModelDialogProps = { defaultValues?: AddLogicalModelFormData; onCancel?: () => void; onSubmit?: () => void; - disabled?: CreateBooleanMap; + disabled?: CreateBooleanMap< + AddLogicalModelFormData & { + callToAction?: 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) => { trackLogicalModel({ - data, + data: { + dataSourceName: data.dataSourceName, + name: data.name, + fields: data.fields.map(formFieldToLogicalModelField), + }, onSuccess: () => { hasuraToast({ type: 'success', @@ -116,18 +133,38 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => { return isMetadataLoading || isIntrospectionLoading ? ( ) : ( -
- -
- -
- + <> + {isEditMode && ( + + The current release does not support editing Logical Models. This + feature will be available in a future release. You can still{' '} + + edit permissions + {' '} + or edit logical models directly by modifying the Metadata. + + )} +
+ +
+ +
+ + ); return ( @@ -160,6 +197,7 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => { sourceOptions={sourceOptions} typeOptions={typeOptions} disabled={props.disabled} + logicalModels={logicalModels} /> )} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/components/FieldsInput.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/components/FieldsInput.tsx index 2b719b3f044..ea99ba8ce69 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/components/FieldsInput.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Data/LogicalModels/LogicalModelWidget/components/FieldsInput.tsx @@ -10,7 +10,6 @@ import { FaPlusCircle } from 'react-icons/fa'; import { Button } from '../../../../../new-components/Button'; import { GraphQLSanitizedInputField, - Select, fieldLabelStyles, } from '../../../../../new-components/Form'; import { BooleanInput } from '../../components/BooleanInput'; @@ -19,6 +18,7 @@ import { AddLogicalModelField, AddLogicalModelFormData, } from '../validationSchema'; +import { LogicalModel } from '../../../../hasura-metadata-types'; const columnHelper = createColumnHelper(); @@ -26,12 +26,15 @@ export const FieldsInput = ({ name, types, disabled, + logicalModels, }: { name: string; types: string[]; disabled?: boolean; + logicalModels: LogicalModel[]; }) => { - const { control } = useFormContext(); + const { control, setValue, watch } = + useFormContext(); const { append, remove, fields } = useFieldArray({ control, @@ -58,18 +61,58 @@ export const FieldsInput = ({ }), columnHelper.accessor('type', { id: 'type', - cell: ({ row }) => ( - { + 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} + > + + + {logicalModels.map(l => ( + + ))} + + + {types.map(t => ( + + ))} + + + ); + }, header: 'Type', }), - columnHelper.accessor('nullable', { id: 'nullable', cell: ({ row }) => ( @@ -80,6 +123,20 @@ export const FieldsInput = ({ ), header: 'Nullable', }), + columnHelper.accessor('array', { + id: 'array', + cell: ({ row }) => { + const typeClassValue = watch(`fields.${row.index}.typeClass`); + return ( + + ); + }, + header: 'Array', + }), columnHelper.display({ id: 'action', header: 'Actions', @@ -115,9 +172,15 @@ export const FieldsInput = ({
Fields