mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: fix data types issues in native queries forms
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9198 GitOrigin-RevId: 07932129503b4941c21e99bf11d5eac32ea6fa11
This commit is contained in:
parent
605f0281dc
commit
1d91f633c3
@ -34,8 +34,8 @@ const fillAndSubmitForm = async ({
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
userEvent.click(c.getByText('Add Parameter'));
|
await userEvent.click(c.getByText('Add Parameter'));
|
||||||
userEvent.click(c.getByText('Save'));
|
await userEvent.click(c.getByText('Save'));
|
||||||
|
|
||||||
const errorMessages = [
|
const errorMessages = [
|
||||||
'Native Query Name is required',
|
'Native Query Name is required',
|
||||||
@ -49,35 +49,35 @@ const fillAndSubmitForm = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove param added for error testing
|
// remove param added for error testing
|
||||||
userEvent.click(c.getByText('Remove'));
|
await userEvent.click(c.getByText('Remove'));
|
||||||
|
|
||||||
userEvent.type(
|
await userEvent.type(
|
||||||
c.getByPlaceholderText('Name that exposes this model in GraphQL API'),
|
c.getByPlaceholderText('Name that exposes this model in GraphQL API'),
|
||||||
'my_native_query'
|
'my_native_query'
|
||||||
);
|
);
|
||||||
userEvent.type(
|
await userEvent.type(
|
||||||
c.getByPlaceholderText('A description of this logical model'),
|
c.getByPlaceholderText('A description of this logical model'),
|
||||||
'a description'
|
'a description'
|
||||||
);
|
);
|
||||||
|
|
||||||
//select postgres from the database dropdown
|
//select postgres from the database dropdown
|
||||||
userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
await c.findByLabelText('Database', undefined, { timeout: 3000 }),
|
await c.findByLabelText('Database', undefined, { timeout: 3000 }),
|
||||||
await c.findByRole('option', { name: 'postgres' })
|
await c.findByRole('option', { name: 'postgres' })
|
||||||
);
|
);
|
||||||
|
|
||||||
userEvent.click(c.getByText('Add Parameter'));
|
await userEvent.click(await c.findByText('Add Parameter'));
|
||||||
|
|
||||||
userEvent.type(c.getByPlaceholderText('Parameter Name'), 'param1');
|
await userEvent.type(c.getByPlaceholderText('Parameter Name'), 'param1');
|
||||||
userEvent.type(c.getByPlaceholderText('Default Value'), 'default');
|
await userEvent.type(c.getByPlaceholderText('Default Value'), 'default');
|
||||||
userEvent.click(c.getByTestId('required-switch'));
|
await userEvent.click(c.getByTestId('required-switch'));
|
||||||
|
|
||||||
userEvent.selectOptions(
|
await userEvent.selectOptions(
|
||||||
await c.findByLabelText('Query Return Type', undefined, { timeout: 3000 }),
|
await c.findByLabelText('Query Return Type', undefined, { timeout: 3000 }),
|
||||||
await c.findByRole('option', { name: 'hello_world' })
|
await c.findByRole('option', { name: 'hello_world' })
|
||||||
);
|
);
|
||||||
|
|
||||||
userEvent.click(c.getByText('Save'));
|
await userEvent.click(c.getByText('Save'));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HappyPath: ComponentStory<typeof AddNativeQuery> = args => {
|
export const HappyPath: ComponentStory<typeof AddNativeQuery> = args => {
|
||||||
|
@ -8,17 +8,21 @@ import {
|
|||||||
useConsoleForm,
|
useConsoleForm,
|
||||||
} from '../../../../new-components/Form';
|
} from '../../../../new-components/Form';
|
||||||
// import { FormDebugWindow } from '../../../../new-components/Form/dev-components/FormDebugWindow';
|
// import { FormDebugWindow } from '../../../../new-components/Form/dev-components/FormDebugWindow';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { Driver, drivers } from '../../../../dataSources';
|
||||||
|
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||||
import { hasuraToast } from '../../../../new-components/Toasts';
|
import { hasuraToast } from '../../../../new-components/Toasts';
|
||||||
|
import { Feature } from '../../../DataSource';
|
||||||
import { useMetadata } from '../../../hasura-metadata-api';
|
import { useMetadata } from '../../../hasura-metadata-api';
|
||||||
|
import { useSupportedDataTypes } from '../../hooks/useSupportedDataTypes';
|
||||||
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
|
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
|
||||||
|
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
|
||||||
import { ArgumentsField } from './components/ArgumentsField';
|
import { ArgumentsField } from './components/ArgumentsField';
|
||||||
import { PageWrapper } from './components/PageWrapper';
|
import { PageWrapper } from './components/PageWrapper';
|
||||||
import { SqlEditorField } from './components/SqlEditorField';
|
import { SqlEditorField } from './components/SqlEditorField';
|
||||||
import { schema } from './schema';
|
import { schema } from './schema';
|
||||||
import { NativeQueryForm } from './types';
|
import { NativeQueryForm } from './types';
|
||||||
import { transformFormOutputToMetadata } from './utils';
|
import { transformFormOutputToMetadata } from './utils';
|
||||||
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
|
|
||||||
import { Driver, drivers } from '../../../../dataSources';
|
|
||||||
|
|
||||||
type AddNativeQueryProps = {
|
type AddNativeQueryProps = {
|
||||||
defaultFormValues?: Partial<NativeQueryForm>;
|
defaultFormValues?: Partial<NativeQueryForm>;
|
||||||
@ -38,7 +42,11 @@ export const AddNativeQuery = ({
|
|||||||
options: { defaultValues: defaultFormValues },
|
options: { defaultValues: defaultFormValues },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: sources, isLoading: isSourcesLoading } = useMetadata(s => {
|
const {
|
||||||
|
data: sources,
|
||||||
|
isLoading: isSourcesLoading,
|
||||||
|
error: sourcesError,
|
||||||
|
} = useMetadata(s => {
|
||||||
return s.metadata.sources.filter(s => drivers.includes(s.kind as Driver));
|
return s.metadata.sources.filter(s => drivers.includes(s.kind as Driver));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,6 +102,32 @@ export const AddNativeQuery = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the data source types
|
||||||
|
*/
|
||||||
|
const {
|
||||||
|
data: typeOptions = [],
|
||||||
|
error: typeOptionError,
|
||||||
|
isLoading: isIntrospectionLoading,
|
||||||
|
} = useSupportedDataTypes({
|
||||||
|
dataSourceName: selectedSource,
|
||||||
|
select: values => {
|
||||||
|
if (values === Feature.NotImplemented) return [];
|
||||||
|
return Object.values(values).flat();
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
enabled: !!selectedSource,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sourcesError || typeOptionError)
|
||||||
|
return (
|
||||||
|
<IndicatorCard status="negative" headline="Internal Error">
|
||||||
|
<div>{sourcesError}</div>
|
||||||
|
<div> {typeOptionError?.message}</div>
|
||||||
|
</IndicatorCard>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper pathname={pathname} push={push}>
|
<PageWrapper pathname={pathname} push={push}>
|
||||||
<Form onSubmit={handleFormSubmit}>
|
<Form onSubmit={handleFormSubmit}>
|
||||||
@ -123,7 +157,14 @@ export const AddNativeQuery = ({
|
|||||||
placeholder="Select a database..."
|
placeholder="Select a database..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ArgumentsField />
|
{isIntrospectionLoading ? (
|
||||||
|
<div>
|
||||||
|
<Skeleton />
|
||||||
|
<Skeleton />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ArgumentsField types={typeOptions} />
|
||||||
|
)}
|
||||||
<SqlEditorField />
|
<SqlEditorField />
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
{/* Logical Model Dropdown */}
|
{/* Logical Model Dropdown */}
|
||||||
|
@ -5,23 +5,22 @@ import {
|
|||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { FaPlusCircle } from 'react-icons/fa';
|
import { FaPlusCircle } from 'react-icons/fa';
|
||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../../new-components/Button';
|
||||||
import {
|
import {
|
||||||
FieldWrapper,
|
|
||||||
GraphQLSanitizedInputField,
|
GraphQLSanitizedInputField,
|
||||||
InputField,
|
InputField,
|
||||||
Select,
|
Select,
|
||||||
fieldLabelStyles,
|
fieldLabelStyles,
|
||||||
} from '../../../../../new-components/Form';
|
} from '../../../../../new-components/Form';
|
||||||
import { Switch } from '../../../../../new-components/Switch';
|
import { BooleanInput } from '../../components/BooleanInput';
|
||||||
import { useCardedTableFromReactTableWithRef } from '../../components/CardedTableFromReactTable';
|
import { useCardedTableFromReactTableWithRef } from '../../components/CardedTableFromReactTable';
|
||||||
import { NativeQueryArgumentNormalized, NativeQueryForm } from '../types';
|
import { NativeQueryArgumentNormalized, NativeQueryForm } from '../types';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<NativeQueryArgumentNormalized>();
|
const columnHelper = createColumnHelper<NativeQueryArgumentNormalized>();
|
||||||
|
|
||||||
export const ArgumentsField = () => {
|
export const ArgumentsField = ({ types }: { types: string[] }) => {
|
||||||
const { control } = useFormContext<NativeQueryForm>();
|
const { control } = useFormContext<NativeQueryForm>();
|
||||||
|
|
||||||
const { append, remove, fields } = useFieldArray({
|
const { append, remove, fields } = useFieldArray({
|
||||||
@ -53,10 +52,7 @@ export const ArgumentsField = () => {
|
|||||||
// saving prop for future upgrade
|
// saving prop for future upgrade
|
||||||
//menuPortalTarget={tableRef.current}
|
//menuPortalTarget={tableRef.current}
|
||||||
name={`arguments.${row.index}.type`}
|
name={`arguments.${row.index}.type`}
|
||||||
options={[
|
options={types.map(t => ({ label: t, value: t }))}
|
||||||
{ value: 'string', label: 'string' },
|
|
||||||
{ value: 'int', label: 'int' },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
@ -75,22 +71,7 @@ export const ArgumentsField = () => {
|
|||||||
columnHelper.accessor('required', {
|
columnHelper.accessor('required', {
|
||||||
id: 'required',
|
id: 'required',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Controller
|
<BooleanInput name={`arguments.${row.index}.required`} />
|
||||||
name={`arguments.${row.index}.required`}
|
|
||||||
render={({ field: { value, onChange }, fieldState }) => (
|
|
||||||
<FieldWrapper
|
|
||||||
id={`arguments.${row.index}.required`}
|
|
||||||
noErrorPlaceholder
|
|
||||||
error={fieldState.error}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
data-testid="required-switch"
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
/>
|
|
||||||
</FieldWrapper>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
header: 'Required',
|
header: 'Required',
|
||||||
}),
|
}),
|
||||||
@ -106,7 +87,7 @@ export const ArgumentsField = () => {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[control, remove]
|
[remove, types]
|
||||||
);
|
);
|
||||||
|
|
||||||
const argumentsTable = useReactTable({
|
const argumentsTable = useReactTable({
|
||||||
|
@ -47,6 +47,8 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedDataSource = watch('dataSourceName');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for the data sources
|
* Options for the data sources
|
||||||
*/
|
*/
|
||||||
@ -71,13 +73,13 @@ export const LogicalModelWidget = (props: AddLogicalModelDialogProps) => {
|
|||||||
error: typeOptionError,
|
error: typeOptionError,
|
||||||
isLoading: isIntrospectionLoading,
|
isLoading: isIntrospectionLoading,
|
||||||
} = useSupportedDataTypes({
|
} = useSupportedDataTypes({
|
||||||
dataSourceName: watch('dataSourceName'),
|
dataSourceName: selectedDataSource,
|
||||||
select: values => {
|
select: values => {
|
||||||
if (values === Feature.NotImplemented) return [];
|
if (values === Feature.NotImplemented) return [];
|
||||||
return Object.values(values).flat();
|
return Object.values(values).flat();
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
enabled: !!watch('dataSourceName'),
|
enabled: !!selectedDataSource,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { BooleanInput } from './BooleanInput';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { TypeInput } from './TypeInput';
|
|
||||||
import { FiTrash2 } from 'react-icons/fi';
|
import { FiTrash2 } from 'react-icons/fi';
|
||||||
import { CardedTable } from '../../../../../new-components/CardedTable';
|
|
||||||
import { InputField } from '../../../../../new-components/Form';
|
|
||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../../new-components/Button';
|
||||||
|
import { CardedTable } from '../../../../../new-components/CardedTable';
|
||||||
|
import { InputField, Select } from '../../../../../new-components/Form';
|
||||||
|
import { BooleanInput } from '../../components/BooleanInput';
|
||||||
|
|
||||||
export const FieldsInput = ({
|
export const FieldsInput = ({
|
||||||
name,
|
name,
|
||||||
@ -52,7 +51,7 @@ export const FieldsInput = ({
|
|||||||
/>
|
/>
|
||||||
</CardedTable.TableBodyCell>
|
</CardedTable.TableBodyCell>
|
||||||
<CardedTable.TableBodyCell>
|
<CardedTable.TableBodyCell>
|
||||||
<TypeInput
|
<Select
|
||||||
name={`${name}[${index}].type`}
|
name={`${name}[${index}].type`}
|
||||||
label=""
|
label=""
|
||||||
options={types.map(t => ({ label: t, value: t }))}
|
options={types.map(t => ({ label: t, value: t }))}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { Controller } from 'react-hook-form';
|
|
||||||
import {
|
|
||||||
FieldWrapper,
|
|
||||||
FieldWrapperPassThroughProps,
|
|
||||||
MultiSelectItem,
|
|
||||||
} from '../../../../../new-components/Form';
|
|
||||||
|
|
||||||
type TypeInputProps = FieldWrapperPassThroughProps & {
|
|
||||||
name: string;
|
|
||||||
options: MultiSelectItem[];
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TypeInput = ({
|
|
||||||
name,
|
|
||||||
options,
|
|
||||||
disabled,
|
|
||||||
...wrapperProps
|
|
||||||
}: TypeInputProps) => {
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name={name}
|
|
||||||
render={({ field: { value }, fieldState }) => (
|
|
||||||
<FieldWrapper id={name} {...wrapperProps} error={fieldState.error}>
|
|
||||||
<select
|
|
||||||
data-testid={name}
|
|
||||||
className={
|
|
||||||
'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-gray-500'
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{options.map(({ label, value }) => (
|
|
||||||
<option key={`${name}-${value}`} value={value}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</FieldWrapper>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
|||||||
import { CardedTable } from '../../../../../new-components/CardedTable';
|
import { CardedTable } from '../../../../../new-components/CardedTable';
|
||||||
import { InputField, Select } from '../../../../../new-components/Form';
|
import { InputField, Select } from '../../../../../new-components/Form';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { BooleanInput } from '../../LogicalModelWidget/parts/BooleanInput';
|
import { BooleanInput } from '../../components/BooleanInput';
|
||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../../new-components/Button';
|
||||||
import { FiTrash2 } from 'react-icons/fi';
|
import { FiTrash2 } from 'react-icons/fi';
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
FieldWrapper,
|
FieldWrapper,
|
||||||
FieldWrapperPassThroughProps,
|
FieldWrapperPassThroughProps,
|
||||||
} from '../../../../../new-components/Form';
|
} from '../../../../new-components/Form';
|
||||||
import { Switch } from '../../../../../new-components/Switch';
|
import { Switch } from '../../../../new-components/Switch';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
type BooleanInputProps = FieldWrapperPassThroughProps & {
|
type BooleanInputProps = FieldWrapperPassThroughProps & {
|
||||||
@ -21,6 +21,7 @@ export const BooleanInput = ({
|
|||||||
render={({ field: { onChange, value }, fieldState }) => (
|
render={({ field: { onChange, value }, fieldState }) => (
|
||||||
<FieldWrapper id={name} {...wrapperProps} error={fieldState.error}>
|
<FieldWrapper id={name} {...wrapperProps} error={fieldState.error}>
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="required-switch"
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
Loading…
Reference in New Issue
Block a user