diff --git a/console/src/features/RemoteSchema/components/Create/Create.tsx b/console/src/features/RemoteSchema/components/Create/Create.tsx index cfa79348108..ba36eb64d32 100644 --- a/console/src/features/RemoteSchema/components/Create/Create.tsx +++ b/console/src/features/RemoteSchema/components/Create/Create.tsx @@ -8,7 +8,7 @@ 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 { RequestHeadersSelector } from '@/new-components/RequestHeadersSelector'; import { schema, Schema } from './schema'; import { transformFormData } from './utils'; import { GraphQLServiceUrl } from './GraphQLServiceUrl'; @@ -157,7 +157,7 @@ export const Create = ({ onSuccess }: Props) => { - +

diff --git a/console/src/features/RemoteSchema/components/Create/schema.ts b/console/src/features/RemoteSchema/components/Create/schema.ts index ee2292de892..a3c2acb58dd 100644 --- a/console/src/features/RemoteSchema/components/Create/schema.ts +++ b/console/src/features/RemoteSchema/components/Create/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { headersSchema } from '../Headers'; +import { requestHeadersSelectorSchema } from '@/new-components/RequestHeadersSelector'; export const schema = z.object({ name: z.string().min(1, 'Remote Schema name is a required field!'), @@ -7,7 +7,7 @@ export const schema = z.object({ value: z.string(), type: z.literal('from_url').or(z.literal('from_env')), }), - headers: headersSchema, + headers: requestHeadersSelectorSchema, forward_client_headers: z.preprocess(val => { if (val === 'true') return true; return false; diff --git a/console/src/features/RemoteSchema/components/Headers/Headers.tsx b/console/src/features/RemoteSchema/components/Headers/Headers.tsx deleted file mode 100644 index 9a71aed61bd..00000000000 --- a/console/src/features/RemoteSchema/components/Headers/Headers.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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>({ - name, - }); - - return ( -
- {fields.length ? ( -
- - - -
- ) : null} - - {fields.map((field, index) => { - return ( -
- - -
- - -
- { - remove(index); - }} - className="cursor-pointer" - /> -
-
-
- ); - })} - -
- ); -}; diff --git a/console/src/features/RemoteSchema/components/Headers/index.ts b/console/src/features/RemoteSchema/components/Headers/index.ts deleted file mode 100644 index 4e28e4e38d8..00000000000 --- a/console/src/features/RemoteSchema/components/Headers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Headers'; -export { headersSchema } from './schema'; diff --git a/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.stories.tsx b/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.stories.tsx new file mode 100644 index 00000000000..906d3285181 --- /dev/null +++ b/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.stories.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { ComponentMeta, Story } from '@storybook/react'; +import { expect } from '@storybook/jest'; +import { userEvent, within, waitFor } from '@storybook/testing-library'; +import { z } from 'zod'; +import { + RequestHeadersSelector, + RequestHeadersSelectorProps, + requestHeadersSelectorSchema, +} from '.'; +import { Button } from '../Button'; +import { Form } from '../Form'; + +export default { + title: 'components/Request Headers Selector', + component: RequestHeadersSelector, + argTypes: { + onSubmit: { action: true }, + }, +} as ComponentMeta; + +const schema = z.object({ + headers: requestHeadersSelectorSchema, +}); + +interface Props extends RequestHeadersSelectorProps { + onSubmit: jest.Mock]>; +} + +export const Primary: Story = args => { + return ( +
+ {() => { + return ( + <> + + + + ); + }} + + ); +}; + +Primary.args = { + name: 'headers', +}; + +Primary.play = async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + const addNewRowButton = canvas.getByText('Add additional header'); + + // Add a third header + await userEvent.click(addNewRowButton); + await userEvent.type( + canvas.getByRole('textbox', { + name: 'headers[2].name', + }), + 'x-hasura-name' + ); + await userEvent.type( + canvas.getByRole('textbox', { + name: 'headers[2].value', + }), + 'hasura_user' + ); + + // Add a fourth header + await userEvent.click(addNewRowButton); + await userEvent.type( + canvas.getByRole('textbox', { + name: 'headers[3].name', + }), + 'x-hasura-id' + ); + await userEvent.selectOptions( + canvas.getByRole('combobox', { + name: 'headers[3].type', + }), + 'from_env' + ); + await userEvent.type( + canvas.getByRole('textbox', { + name: 'headers[3].value', + }), + 'HASURA_ENV_ID' + ); + + await userEvent.click(canvas.getByText('Submit')); + + const submittedHeaders: z.infer = { + headers: [ + { name: 'x-hasura-role', type: 'from_value', value: 'admin' }, + { + name: 'x-hasura-user', + type: 'from_env', + value: 'HASURA_USER', + }, + { + name: 'x-hasura-name', + type: 'from_value', + value: 'hasura_user', + }, + { + name: 'x-hasura-id', + type: 'from_env', + value: 'HASURA_ENV_ID', + }, + ], + }; + + await waitFor(() => expect(args.onSubmit).toHaveBeenCalledTimes(1)); + const firstOnSubmitCallParams = args.onSubmit.mock.calls[0]; + expect(firstOnSubmitCallParams).toMatchObject( + expect.objectContaining([{ ...submittedHeaders }]) + ); +}; diff --git a/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.tsx b/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.tsx new file mode 100644 index 00000000000..326f315237f --- /dev/null +++ b/console/src/new-components/RequestHeadersSelector/RequestHeadersSelector.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useFieldArray } from 'react-hook-form'; +import { FaPlusCircle } from 'react-icons/fa'; +import { Button } from '@/new-components/Button'; +import { RequestHeadersSelectorSchema } from './schema'; +import { KeyValueHeader } from './components/KeyValueHeader'; + +export interface RequestHeadersSelectorProps { + name: string; +} + +export const RequestHeadersSelector = (props: RequestHeadersSelectorProps) => { + const { name } = props; + const { fields, append, remove } = useFieldArray< + Record + >({ + name, + }); + const thereIsAtLeastOneField = fields.length > 0; + + return ( +
+ {thereIsAtLeastOneField ? ( + <> +
+ + +
+ + {fields.map((field, index) => ( + + ))} + + ) : null} + + +
+ ); +}; diff --git a/console/src/new-components/RequestHeadersSelector/components/KeyValueHeader.tsx b/console/src/new-components/RequestHeadersSelector/components/KeyValueHeader.tsx new file mode 100644 index 00000000000..acea29ada36 --- /dev/null +++ b/console/src/new-components/RequestHeadersSelector/components/KeyValueHeader.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { RiCloseCircleFill } from 'react-icons/ri'; +import { useFormContext } from 'react-hook-form'; + +interface Props { + fieldId: string; + fieldName: string; + rowIndex: number; + removeRow: (index?: number | number[]) => void; +} + +const borderStyle = + 'border-gray-300 hover:border-gray-400 focus:border-yellow-400'; +const ringStyle = 'focus:ring-2 focus:ring-yellow-200'; + +export const KeyValueHeader = (props: Props) => { + const { fieldId, fieldName, rowIndex, removeRow } = props; + const keyLabel = `${fieldName}[${rowIndex}].name`; + const typeLabel = `${fieldName}[${rowIndex}].type`; + const valueLabel = `${fieldName}[${rowIndex}].value`; + const { register } = useFormContext(); + + return ( +
+ +
+ + +
+ { + removeRow(rowIndex); + }} + className="cursor-pointer" + /> +
+
+
+ ); +}; diff --git a/console/src/new-components/RequestHeadersSelector/index.ts b/console/src/new-components/RequestHeadersSelector/index.ts new file mode 100644 index 00000000000..9b88a171e1b --- /dev/null +++ b/console/src/new-components/RequestHeadersSelector/index.ts @@ -0,0 +1,2 @@ +export * from './RequestHeadersSelector'; +export { requestHeadersSelectorSchema } from './schema'; diff --git a/console/src/features/RemoteSchema/components/Headers/schema.ts b/console/src/new-components/RequestHeadersSelector/schema.ts similarity index 51% rename from console/src/features/RemoteSchema/components/Headers/schema.ts rename to console/src/new-components/RequestHeadersSelector/schema.ts index f41a4b2242d..8f63a88bc23 100644 --- a/console/src/features/RemoteSchema/components/Headers/schema.ts +++ b/console/src/new-components/RequestHeadersSelector/schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const headersSchema = z.array( +export const requestHeadersSelectorSchema = z.array( z.object({ name: z.string(), value: z.string(), @@ -8,4 +8,6 @@ export const headersSchema = z.array( }) ); -export type THeaders = z.infer; +export type RequestHeadersSelectorSchema = z.infer< + typeof requestHeadersSelectorSchema +>;