console: new "add remote schema" page (with GQL customization)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4428
GitOrigin-RevId: d8f82ffe86fd59abb43afa195b40c2fdafabb847
This commit is contained in:
Vijay Prasanna 2022-05-10 17:01:30 +05:30 committed by hasura-bot
parent f7b4d32941
commit db07dcf60d
19 changed files with 4508 additions and 3442 deletions

View File

@ -17,6 +17,7 @@
- console: add remote database relationships for views
- console: bug fixes for RS-to-RS relationships
- console: allow users to remove prefix / suffix / root field namespace from a remote schema
- console: new "add remote schema" page (with GQL customization)
- cli: avoid exporting hasura-specific schemas during hasura init (#8352)
- cli: fix performance regression in `migrate status` command (fix #8398)

View File

@ -21566,6 +21566,15 @@
"@types/lodash": "*"
}
},
"@types/lodash.pickby": {
"version": "4.6.7",
"resolved": "https://registry.npmjs.org/@types/lodash.pickby/-/lodash.pickby-4.6.7.tgz",
"integrity": "sha512-4ebXRusuLflfscbD0PUX4eVknDHD9Yf+uMtBIvA/hrnTqeAzbuHuDjvnYriLjUrI9YrhCPVKUf4wkRSXJQ6gig==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@ -35839,6 +35848,11 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lodash.pickby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
"integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8="
},
"lodash.restparam": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",

View File

@ -88,6 +88,7 @@
"less": "3.11.1",
"lodash.get": "4.4.2",
"lodash.merge": "4.6.2",
"lodash.pickby": "^4.6.0",
"moment": "^2.26.0",
"piping": "0.3.2",
"prop-types": "15.7.2",
@ -170,6 +171,7 @@
"@types/jwt-decode": "2.2.1",
"@types/lodash": "^4.14.159",
"@types/lodash.merge": "^4.6.6",
"@types/lodash.pickby": "^4.6.7",
"@types/mini-css-extract-plugin": "0.9.1",
"@types/optimize-css-assets-webpack-plugin": "5.0.1",
"@types/react": "17.0.39",

View File

@ -1,52 +1,75 @@
import React from 'react';
import React, { useEffect } from 'react';
import Common from '../Common/Common';
import { addRemoteSchema, RESET } from './addRemoteSchemaReducer';
import Helmet from 'react-helmet';
import Button from '../../../Common/Button/Button';
import { RemoteSchema } from '@/features/RemoteSchema';
import { appPrefix, pageTitle } from '../constants';
import {
availableFeatureFlagIds,
FeatureFlagToast,
useIsFeatureFlagEnabled,
} from '@/features/FeatureFlags';
import { exportMetadata } from '@/metadata/actions';
import _push from '../../Data/push';
import { pageTitle } from '../constants';
const Add = ({ isRequesting, dispatch, ...props }) => {
const styles = require('../RemoteSchema.scss');
class Add extends React.Component {
componentWillUnmount() {
this.props.dispatch({ type: RESET });
useEffect(() => {
return () => {
dispatch({ type: RESET });
};
}, []);
const { isLoading, enabled } = useIsFeatureFlagEnabled(
availableFeatureFlagIds.addRemoteSchemaId
);
if (isLoading) {
return 'Loading...';
}
render() {
const styles = require('../RemoteSchema.scss');
const { isRequesting, dispatch } = this.props;
if (enabled) {
return (
<div className={styles.addWrapper}>
<Helmet title={`Add ${pageTitle} - ${pageTitle}s | Hasura`} />
<div className={styles.heading_text}>Add a new remote schema</div>
<form
onSubmit={e => {
e.preventDefault();
dispatch(addRemoteSchema());
}}
>
<Common isNew {...this.props} />
<div className={styles.commonBtn}>
<Button
type="submit"
color="yellow"
size="sm"
// disabled={isRequesting} // TODO
data-test="add-remote-schema-submit"
>
{isRequesting ? 'Adding...' : 'Add Remote Schema'}
</Button>
{/*
<button className={styles.default_button}>Cancel</button>
*/}
</div>
</form>
</div>
<RemoteSchema.Create
onSuccess={remoteSchemaName => {
// This only exists right now because the sidebar is reading from redux state
dispatch(exportMetadata()).then(() => {
dispatch(_push(`${appPrefix}/manage/${remoteSchemaName}/details`));
});
}}
/>
);
}
}
return (
<div className={styles.addWrapper}>
<Helmet title={`Add ${pageTitle} - ${pageTitle}s | Hasura`} />
<div className={styles.heading_text}>Add a new remote schema</div>
<form
onSubmit={e => {
e.preventDefault();
dispatch(addRemoteSchema());
}}
>
<Common isNew {...props} dispatch={dispatch} />
<div className={styles.commonBtn}>
<Button
type="submit"
color="yellow"
size="sm"
data-test="add-remote-schema-submit"
>
{isRequesting ? 'Adding...' : 'Add Remote Schema'}
</Button>
</div>
</form>
<FeatureFlagToast flagId={availableFeatureFlagIds.addRemoteSchemaId} />
</div>
);
};
const mapStateToProps = state => {
return {

View File

@ -1,9 +1,11 @@
import { FeatureFlagDefinition } from './types';
const relationshipTabTablesId = '0bea35ff-d3e9-45e9-af1b-59923bf82fa9';
const addRemoteSchemaId = 'bf57c2ba-cab2-11ec-9d64-0242ac120002';
export const availableFeatureFlagIds = {
relationshipTabTablesId,
addRemoteSchemaId,
};
export const availableFeatureFlags: FeatureFlagDefinition[] = [
@ -18,4 +20,14 @@ export const availableFeatureFlags: FeatureFlagDefinition[] = [
// defaultValue: false,
// discussionUrl: '',
// },
{
id: addRemoteSchemaId,
title: 'New create remote schema page',
description:
'Try out the new Add Remote Schema page that supports GraphQL customization',
section: 'remote schemas',
status: 'alpha',
defaultValue: false,
discussionUrl: '',
},
];

View File

@ -35,6 +35,7 @@ export const allowedMetadataTypesArr = [
'create_remote_schema_remote_relationship',
'update_remote_schema_remote_relationship',
'delete_remote_schema_remote_relationship',
'add_remote_schema',
'bulk',
] as const;

View File

@ -0,0 +1,93 @@
import React, { useReducer } from 'react';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { ComponentMeta, Story } from '@storybook/react';
import { RemoteSchema } from '@/features/RemoteSchema';
import { ReduxDecorator } from '@/storybook/decorators/redux-decorator';
import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { handlers } from './mocks/handlers.mock';
export default {
title: 'RemoteSchema/components/Create',
component: RemoteSchema.Create,
decorators: [ReactQueryDecorator(), ReduxDecorator({})],
parameters: {
msw: handlers(),
},
} as ComponentMeta<typeof RemoteSchema.Create>;
export const Playground: Story = () => {
const [formSuccess, toggle] = useReducer(s => !s, false);
return (
<>
<RemoteSchema.Create onSuccess={() => toggle()} />
{formSuccess ? (
<p data-testid="form-result">Form saved succesfully!</p>
) : null}
</>
);
};
Playground.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
userEvent.click(await canvas.findByTestId('submit'));
// expect error messages
expect(
await canvas.findByText('Remote Schema name is a required field!')
).toBeInTheDocument();
expect(await canvas.findByText('Invalid url')).toBeInTheDocument();
// Fill up the fields
// const nameInput = await canvas.findByLabelText('Name');
userEvent.type(await canvas.findByTestId('name'), 'test');
userEvent.type(await canvas.findByTestId('url'), 'http://example.com');
userEvent.type(await canvas.findByTestId('timeout_seconds'), '180');
userEvent.click(await canvas.findByTestId('forward_client_headers'));
userEvent.click(await canvas.findByTestId('open_customization'));
userEvent.type(
await canvas.findByTestId('customization.root_fields_namespace'),
'root_field_namespace_example'
);
userEvent.type(
await canvas.findByTestId('customization.type_prefix'),
'type_prefix_example_'
);
userEvent.type(
await canvas.findByTestId('customization.type_suffix'),
'_type_suffix_example'
);
userEvent.type(
await canvas.findByTestId('customization.query_root.parent_type'),
'query_root_parent_type_example_'
);
userEvent.type(
await canvas.findByTestId('customization.query_root.prefix'),
'query_root_prefix_example_'
);
userEvent.type(
await canvas.findByTestId('customization.query_root.suffix'),
'_query_root_suffix_example'
);
userEvent.type(
await canvas.findByTestId('customization.mutation_root.parent_type'),
'mutation_root_parent_type_example_'
);
userEvent.type(
await canvas.findByTestId('customization.mutation_root.prefix'),
'mutation_root_prefix_example_'
);
userEvent.type(
await canvas.findByTestId('customization.mutation_root.suffix'),
'_mutation_root_suffix_example'
);
userEvent.click(await canvas.findByTestId('submit'));
expect(
await canvas.findByText('Form saved succesfully!')
).toBeInTheDocument();
};

View File

@ -0,0 +1,416 @@
import { useMetadataMigration } from '@/features/MetadataAPI';
import { Button } from '@/new-components/Button';
import { FieldError } from 'react-hook-form';
import { Form, InputField } from '@/new-components/Form';
import { fireNotification } from '@/new-components/Notifications';
import { ToolTip } from '@/new-components/Tooltip';
import get from 'lodash.get';
import { APIError } from '@/hooks/error';
import React, { useState } from 'react';
import { FaExclamationCircle, FaPlusCircle } from 'react-icons/fa';
import { Headers } from '../Headers';
import { schema, Schema } from './schema';
import { transformFormData } from './utils';
type Props = {
onSuccess?: (remoteSchemaName?: string) => void;
};
export const Create = ({ onSuccess }: Props) => {
const [remoteSchemaName, setRemoteSchemaName] = useState('');
const mutation = useMetadataMigration(
{
onError: (error: APIError) => {
fireNotification({
type: 'error',
title: 'Error',
message: error?.message ?? 'Unable to create Remote Schema',
});
},
onSuccess: () => {
fireNotification({
type: 'success',
title: 'Success!',
message: 'Remote Schema created successfully',
});
if (onSuccess) onSuccess(remoteSchemaName);
},
},
true
);
const onSubmit = (values: Schema) => {
const args = transformFormData(values);
setRemoteSchemaName(values.name);
mutation.mutate({
source: '',
query: { type: 'add_remote_schema', args },
migrationName: 'createRemoteSchema',
});
};
const defaultValues: Schema = {
name: '',
url: '',
headers: [],
forward_client_headers: false,
timeout_seconds: '',
comment: '',
customization: {
root_fields_namespace: '',
type_prefix: '',
type_suffix: '',
query_root: {
parent_type: '',
prefix: '',
suffix: '',
},
mutation_root: {
parent_type: '',
prefix: '',
suffix: '',
},
},
};
const [openCustomizationWidget, setOpenCustomizationWidget] = useState(false);
return (
<Form
schema={schema}
onSubmit={onSubmit}
options={{ defaultValues }}
className="overflow-y-hidden p-4"
>
{options => {
const queryRootError = get(
options.formState.errors,
'customization.query_root'
) as FieldError | undefined;
const mutationRootError = get(
options.formState.errors,
'customization.mutation_root'
) as FieldError | undefined;
return (
<div className="max-w-6xl">
<h1 className="text-xl leading-6 font-semibold mb-lg">
Add Remote Schema
</h1>
<div className="mb-md w-6/12">
<InputField
name="name"
label="Remote Schema Name"
placeholder="Name..."
tooltip="give this GraphQL schema a friendly name"
/>
</div>
<div className="mb-md w-6/12">
<InputField
name="comment"
label="Comment / Description"
placeholder="Comment / Description..."
tooltip="A statement to help describe the remote schema in brief"
/>
</div>
<div className="mb-md w-6/12">
<InputField
name="url"
label="GraphQL Service URL"
placeholder="https://myservice.com/graphql"
description="Note: Specifying the server URL via an environmental variable is
recommended if you have different URLs for multiple
environments."
tooltip="Remote GraphQL servers URL. E.g. https://my-domain/v1/graphql"
/>
</div>
<div className="mb-lg w-4/12">
<label className="block flex items-center text-gray-600 font-semibold mb-xs">
GraphQL Server Timeout
<ToolTip message="Configure timeout for your remote GraphQL server. Defaults to 60 seconds." />
</label>
<div className="relative w-full">
<input
type="text"
className="block w-full shadow-sm rounded pr-10 border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="60"
{...options.register('timeout_seconds')}
data-testid="timeout_seconds"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex text-gray-400 items-center pointer-events-none">
Seconds
</div>
</div>
</div>
<div className="mb-lg w-8/12">
<h2 className="text-lg font-semibold text-gray-600 ">Headers</h2>
<div className="items-center mr-sm mb-sm my-sm flex">
<input
{...options.register('forward_client_headers')}
type="checkbox"
className="mr-sm border-gray-400 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-400"
value="true"
data-testid="forward_client_headers"
/>
<label className="pl-3 flex items-center">
Forward all headers from client
<ToolTip message="Custom headers to be sent to the remote GraphQL server" />
</label>
</div>
<Headers name="headers" />
</div>
<div className="mb-lg w-8/12">
<h2 className="text-lg font-semibold flex items-center">
GraphQL Customizations
</h2>
<p className="text-sm text-gray-600 mb-sm">
Individual Types and Fields will be editable after saving.
<br />
<a href="https://spec.graphql.org/June2018/#example-e2969">
Read more
</a>{' '}
about Type and Field naming conventions in the official GraphQL
spec
</p>
{openCustomizationWidget ? (
<div className="w-full rounded border bg-white border-gray-300 p-4">
<Button
type="button"
size="sm"
onClick={() => setOpenCustomizationWidget(false)}
>
Close
</Button>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Root Field Namespace
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="namespace_"
{...options.register(
'customization.root_fields_namespace'
)}
data-testid="customization.root_fields_namespace"
/>
</div>
</div>
<h2 className="text-lg font-semibold mb-xs items-center flex">
Types
<ToolTip message="add a prefix / suffix to all types of the remote schema" />
</h2>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Prefix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="prefix_"
{...options.register('customization.type_prefix')}
data-testid="customization.type_prefix"
/>
</div>
</div>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Suffix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="_suffix"
{...options.register('customization.type_suffix')}
data-testid="customization.type_suffix"
/>
</div>
</div>
<h2 className="text-lg font-semibold mb-xs flex items-center">
Fields
<ToolTip message="add a prefix / suffix to the fields of the query / mutation root fields" />
</h2>
<h3 className="font-semibold mb-xs text-gray-600 text-lg">
Query root
</h3>
{queryRootError?.message && (
<div
role="alert"
aria-label={queryRootError.message}
className="mt-xs text-red-600 flex items-center"
>
<FaExclamationCircle className="fill-current h-4 mr-xs" />
{queryRootError.message}
</div>
)}
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Type Name
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="Query/query_root"
{...options.register(
'customization.query_root.parent_type'
)}
data-testid="customization.query_root.parent_type"
/>
</div>
</div>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Prefix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="prefix_"
{...options.register('customization.query_root.prefix')}
data-testid="customization.query_root.prefix"
/>
</div>
</div>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Suffix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="_suffix"
{...options.register('customization.query_root.suffix')}
data-testid="customization.query_root.suffix"
/>
</div>
</div>
<h3 className="font-semibold mb-xs text-gray-600 text-lg">
Mutation root
</h3>
{mutationRootError?.message && (
<div
role="alert"
aria-label={mutationRootError.message}
className="mt-xs text-red-600 flex items-center"
>
<FaExclamationCircle className="fill-current h-4 mr-xs" />
{mutationRootError.message}
</div>
)}
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Type Name
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="Mutation/mutation_root"
{...options.register(
'customization.mutation_root.parent_type'
)}
data-testid="customization.mutation_root.parent_type"
/>
</div>
</div>
<div className="grid gap-3 grid-cols-12 mb-md">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Prefix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="prefix_"
{...options.register(
'customization.mutation_root.prefix'
)}
data-testid="customization.mutation_root.prefix"
/>
</div>
</div>
<div className="grid gap-3 grid-cols-12">
<div className="flex items-center col-span-4">
<label className="block text-gray-600 font-medium">
Suffix
</label>
</div>
<div className="col-span-8">
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="_suffix"
{...options.register(
'customization.mutation_root.suffix'
)}
data-testid="customization.mutation_root.suffix"
/>
</div>
</div>
</div>
) : (
<Button
icon={<FaPlusCircle />}
type="button"
size="sm"
onClick={() => setOpenCustomizationWidget(true)}
data-testid="open_customization"
>
Add GQL Customization
</Button>
)}
</div>
<div className="flex items-center mb-lg">
<Button
type="submit"
data-testid="submit"
mode="primary"
isLoading={mutation.isLoading}
>
Add Remote Schema
</Button>
</div>
</div>
);
}}
</Form>
);
};

View File

@ -0,0 +1 @@
export { Create } from './Create';

View File

@ -0,0 +1,7 @@
import { rest } from 'msw';
export const handlers = () => [
rest.post('http://localhost:8080/v1/metadata', (req, res, ctx) => {
return res(ctx.json({ message: 'success' }));
}),
];

View File

@ -0,0 +1,51 @@
import { z } from 'zod';
import { headersSchema } from '../Headers';
export const schema = z.object({
name: z.string().min(1, 'Remote Schema name is a required field!'),
url: z.string().url().min(1, 'URL is a required field!'),
headers: headersSchema,
forward_client_headers: z.preprocess(val => {
if (val === 'true') return true;
return false;
}, z.boolean()),
timeout_seconds: z.string().optional(),
customization: z.object({
root_fields_namespace: z.string(),
type_prefix: z.string(),
type_suffix: z.string(),
query_root: z
.object({
parent_type: z.string(),
prefix: z.string(),
suffix: z.string(),
})
.refine(
data => {
if ((data.prefix || data.suffix) && !data.parent_type) return false;
return true;
},
{
message: 'Query type name cannot be empty!',
}
),
mutation_root: z
.object({
parent_type: z.string(),
prefix: z.string(),
suffix: z.string(),
})
.refine(
data => {
if ((data.prefix || data.suffix) && !data.parent_type) return false;
return true;
},
{
message: 'Mutation type name cannot be empty!',
}
),
}),
comment: z.string().optional(),
});
export type Schema = z.infer<typeof schema>;

View File

@ -0,0 +1,84 @@
import pickBy from 'lodash.pickby';
import { Schema } from './schema';
export const transformFormData = (values: Schema) => {
const {
name,
url,
headers,
forward_client_headers,
comment,
timeout_seconds,
customization: {
root_fields_namespace,
type_prefix,
type_suffix,
query_root,
mutation_root,
},
} = values;
const customization: Record<string, any> = {};
/* if root field namespace is present */
if (root_fields_namespace)
customization.root_fields_namespace = root_fields_namespace;
/* if type prefix & suffix are present */
if (type_prefix || type_suffix)
customization.type_names = pickBy(
{
prefix: type_prefix,
suffix: type_suffix,
},
value => value.length > 0
);
/* if Query root customization is present */
if (query_root.parent_type && (query_root.prefix || query_root.suffix)) {
customization.field_names = [
...(customization.field_names ?? []),
pickBy(
{
parent_type: query_root.parent_type,
prefix: query_root.prefix,
suffix: query_root.suffix,
},
value => value.length > 0
),
];
}
/* if Mutation root customization is present */
if (
mutation_root.parent_type &&
(mutation_root.prefix || mutation_root.suffix)
) {
customization.field_names = [
...(customization.field_names ?? []),
pickBy(
{
parent_type: mutation_root.parent_type,
prefix: mutation_root.prefix,
suffix: mutation_root.suffix,
},
value => value.length > 0
),
];
}
const definition: Record<string, any> = {
url,
forward_client_headers,
comment,
headers: headers.map(header => {
if (header.type === 'from_env')
return { name: header.name, value_from_env: header.value };
return { name: header.name, value: header.value };
}),
timeout_seconds: timeout_seconds ? parseInt(timeout_seconds, 10) : 60,
customization,
};
return { name, definition };
};

View File

@ -0,0 +1,83 @@
import { Button } from '@/new-components/Button';
import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { FaPlusCircle } from 'react-icons/fa';
import { RiCloseCircleFill } from 'react-icons/ri';
import { THeaders } from './schema';
export const Headers = ({ name }: { name: string }) => {
const { register } = useFormContext();
const { fields, append, remove } = useFieldArray<Record<string, THeaders>>({
name,
});
return (
<div>
{fields.length ? (
<div className="grid gap-3 grid-cols-2 mb-sm">
<label
htmlFor="table_name"
className="block text-gray-600 font-medium mb-xs"
>
Key
</label>
<label
htmlFor="table_name"
className="block text-gray-600 font-medium mb-xs"
>
Value
</label>
</div>
) : null}
{fields.map((field, index) => {
return (
<div className="grid gap-4 grid-cols-2 mb-sm" key={field.id}>
<input
type="text"
className="w-full block h-10 shadow-sm rounded border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="Key..."
{...register(`${name}[${index}].name`)}
/>
<div className="flex shadow-sm rounded">
<select
{...register(`${name}[${index}].type`)}
className="inline-flex rounded-l border border-r-0 border-gray-300 bg-white hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
>
<option value="from_value">Value</option>
<option value="from_env">Env Var</option>
</select>
<input
{...register(`${name}[${index}].value`)}
type="text"
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-r border-gray-300 hover:border-gray-400 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
placeholder="user-id"
/>
<div className="col-span-1 flex items-center justify-center pl-1.5">
<RiCloseCircleFill
onClick={() => {
remove(index);
}}
className="cursor-pointer"
/>
</div>
</div>
</div>
);
})}
<Button
data-testid={`${name}_add_new_row`}
icon={<FaPlusCircle />}
onClick={() => {
append({ name: '', value: '', type: 'from_value' });
}}
size="sm"
>
Add additonal header
</Button>
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './Headers';
export { headersSchema } from './schema';

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const headersSchema = z.array(
z.object({
name: z.string(),
value: z.string(),
type: z.literal('from_env').or(z.literal('from_value')),
})
);
export type THeaders = z.infer<typeof headersSchema>;

View File

@ -0,0 +1,5 @@
import { Create } from './components/Create';
export const RemoteSchema = {
Create,
};

View File

@ -25,7 +25,7 @@ const buttonModesStyles: Record<ButtonModes, string> = {
destructive:
'text-red-600 bg-gray-50 from-transparent to-white border-gray-300 hover:border-gray-400 disabled:border-gray-300 focus:from-bg-gray-50 focus:to-bg-gray-50',
primary:
'text-gray-600 bg-primary from-primary to-primary-light border-primary-dark hover:border-primary-darker focus:from-primary focus:to-primary disabled:border-primary-dark',
'text-gray-600 from-primary to-primary-light border-primary-dark hover:border-primary-darker focus:from-primary focus:to-primary disabled:border-primary-dark',
};
const sharedButtonStyle =

View File

@ -2,7 +2,6 @@ import React, { ReactElement } from 'react';
import clsx from 'clsx';
import get from 'lodash.get';
import { FieldError, useFormContext } from 'react-hook-form';
import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper';
export type InputFieldProps = FieldWrapperPassThroughProps & {
@ -33,7 +32,6 @@ export const InputField = ({
} = useFormContext();
const maybeError = get(errors, name) as FieldError | undefined;
return (
<FieldWrapper id={name} {...wrapperProps} error={maybeError}>
<div
@ -58,8 +56,8 @@ export const InputField = ({
className={clsx(
'block w-full max-w-xl h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400 placeholder-gray-500',
maybeError
? 'border-red-600 hover:border-red-700 placeholder-red-600 '
: 'border-gray-300 placeholder-gray-600',
? 'border-red-600 hover:border-red-700'
: 'border-gray-300',
disabled
? 'cursor-not-allowed bg-gray-100 border-gray-100'
: 'hover:border-gray-400',
@ -71,6 +69,7 @@ export const InputField = ({
placeholder={placeholder}
{...register(name)}
disabled={disabled}
data-testid={name}
/>
{iconPosition === 'end' && icon ? (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">

File diff suppressed because it is too large Load Diff