mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: move request headers component to new components and add stories
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4956 GitOrigin-RevId: 7d9530889067389684330c886276502b398ef813
This commit is contained in:
parent
c1380e1daf
commit
214ec9aa5d
@ -8,7 +8,7 @@ import get from 'lodash.get';
|
|||||||
import { APIError } from '@/hooks/error';
|
import { APIError } from '@/hooks/error';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FaExclamationCircle, FaPlusCircle } from 'react-icons/fa';
|
import { FaExclamationCircle, FaPlusCircle } from 'react-icons/fa';
|
||||||
import { Headers } from '../Headers';
|
import { RequestHeadersSelector } from '@/new-components/RequestHeadersSelector';
|
||||||
import { schema, Schema } from './schema';
|
import { schema, Schema } from './schema';
|
||||||
import { transformFormData } from './utils';
|
import { transformFormData } from './utils';
|
||||||
import { GraphQLServiceUrl } from './GraphQLServiceUrl';
|
import { GraphQLServiceUrl } from './GraphQLServiceUrl';
|
||||||
@ -157,7 +157,7 @@ export const Create = ({ onSuccess }: Props) => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Headers name="headers" />
|
<RequestHeadersSelector name="headers" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-lg w-8/12">
|
<div className="mb-lg w-8/12">
|
||||||
<h2 className="text-lg font-semibold flex items-center">
|
<h2 className="text-lg font-semibold flex items-center">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { headersSchema } from '../Headers';
|
import { requestHeadersSelectorSchema } from '@/new-components/RequestHeadersSelector';
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
name: z.string().min(1, 'Remote Schema name is a required field!'),
|
name: z.string().min(1, 'Remote Schema name is a required field!'),
|
||||||
@ -7,7 +7,7 @@ export const schema = z.object({
|
|||||||
value: z.string(),
|
value: z.string(),
|
||||||
type: z.literal('from_url').or(z.literal('from_env')),
|
type: z.literal('from_url').or(z.literal('from_env')),
|
||||||
}),
|
}),
|
||||||
headers: headersSchema,
|
headers: requestHeadersSelectorSchema,
|
||||||
forward_client_headers: z.preprocess(val => {
|
forward_client_headers: z.preprocess(val => {
|
||||||
if (val === 'true') return true;
|
if (val === 'true') return true;
|
||||||
return false;
|
return false;
|
||||||
|
@ -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<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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './Headers';
|
|
||||||
export { headersSchema } from './schema';
|
|
@ -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<typeof RequestHeadersSelector>;
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
headers: requestHeadersSelectorSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props extends RequestHeadersSelectorProps {
|
||||||
|
onSubmit: jest.Mock<void, [Record<string, unknown>]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<Props> = args => {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
options={{
|
||||||
|
defaultValues: {
|
||||||
|
headers: [
|
||||||
|
{ name: 'x-hasura-role', value: 'admin', type: 'from_value' },
|
||||||
|
{ name: 'x-hasura-user', value: 'HASURA_USER', type: 'from_env' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={args.onSubmit}
|
||||||
|
schema={schema}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RequestHeadersSelector name={args.name} />
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<typeof schema> = {
|
||||||
|
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 }])
|
||||||
|
);
|
||||||
|
};
|
@ -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<string, RequestHeadersSelectorSchema>
|
||||||
|
>({
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
const thereIsAtLeastOneField = fields.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{thereIsAtLeastOneField ? (
|
||||||
|
<>
|
||||||
|
<div className="grid gap-3 grid-cols-2 mb-sm">
|
||||||
|
<label className="block text-gray-600 font-medium mb-xs">Key</label>
|
||||||
|
<label className="block text-gray-600 font-medium mb-xs">
|
||||||
|
Value
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<KeyValueHeader
|
||||||
|
fieldId={field.id}
|
||||||
|
fieldName={name}
|
||||||
|
rowIndex={index}
|
||||||
|
removeRow={remove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<FaPlusCircle />}
|
||||||
|
onClick={() => {
|
||||||
|
append({ name: '', value: '', type: 'from_value' });
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Add additional header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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 (
|
||||||
|
<div className="grid gap-4 grid-cols-2 mb-sm" key={fieldId}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={`w-full block h-10 shadow-sm rounded ${borderStyle} ${ringStyle}`}
|
||||||
|
placeholder="Key..."
|
||||||
|
{...register(keyLabel)}
|
||||||
|
aria-label={keyLabel}
|
||||||
|
/>
|
||||||
|
<div className="flex rounded">
|
||||||
|
<select
|
||||||
|
{...register(typeLabel)}
|
||||||
|
aria-label={typeLabel}
|
||||||
|
className={`inline-flex h-10 shadow-sm rounded-l border border-r-0 bg-white ${borderStyle} ${ringStyle}`}
|
||||||
|
>
|
||||||
|
<option value="from_value">Value</option>
|
||||||
|
<option value="from_env">Env Var</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
{...register(valueLabel)}
|
||||||
|
aria-label={valueLabel}
|
||||||
|
type="text"
|
||||||
|
className={`flex-1 min-w-0 h-10 shadow-sm block w-full px-3 py-2 rounded-r ${borderStyle} ${ringStyle}`}
|
||||||
|
placeholder="Value..."
|
||||||
|
/>
|
||||||
|
<div className="col-span-1 flex items-center justify-center pl-1.5">
|
||||||
|
<RiCloseCircleFill
|
||||||
|
onClick={() => {
|
||||||
|
removeRow(rowIndex);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './RequestHeadersSelector';
|
||||||
|
export { requestHeadersSelectorSchema } from './schema';
|
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const headersSchema = z.array(
|
export const requestHeadersSelectorSchema = z.array(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
@ -8,4 +8,6 @@ export const headersSchema = z.array(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export type THeaders = z.infer<typeof headersSchema>;
|
export type RequestHeadersSelectorSchema = z.infer<
|
||||||
|
typeof requestHeadersSelectorSchema
|
||||||
|
>;
|
Loading…
Reference in New Issue
Block a user