console: fix headers for mutation input validation

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9898
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
GitOrigin-RevId: 4e1a36b498c65a6ad9bcbe98a1dd9e87f288ac06
This commit is contained in:
Varun Choudhary 2023-07-26 12:07:26 +05:30 committed by hasura-bot
parent f0441a3d61
commit 059c3e3876
8 changed files with 160 additions and 12 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -77,8 +77,8 @@ describe('Create a insert type table permission with input validation', () => {
cy.findByText('Forward client headers to webhook').click(); cy.findByText('Forward client headers to webhook').click();
cy.get('[name="definition.timeout"]').clear().type('40'); cy.get('[name="definition.timeout"]').clear().type('40');
cy.findByRole('button', { name: 'Add Additional Headers' }).click(); cy.findByRole('button', { name: 'Add Additional Headers' }).click();
cy.findByPlaceholderText('Key...').type('x-hasura-user-id'); cy.findByPlaceholderText('Key').type('x-hasura-user-id');
cy.findByPlaceholderText('Value...').type('1234'); cy.findByPlaceholderText('Value or {{Environment_Variable}}').type('1234');
// -------------------- // --------------------
cy.log('**--- Click to Save Permission'); cy.log('**--- Click to Save Permission');
@ -174,8 +174,8 @@ describe('Create a update type table permission with input validation', () => {
cy.findByText('Forward client headers to webhook').click(); cy.findByText('Forward client headers to webhook').click();
cy.get('[name="definition.timeout"]').clear().type('40'); cy.get('[name="definition.timeout"]').clear().type('40');
cy.findByRole('button', { name: 'Add Additional Headers' }).click(); cy.findByRole('button', { name: 'Add Additional Headers' }).click();
cy.findByPlaceholderText('Key...').type('x-hasura-user-id'); cy.findByPlaceholderText('Key').type('x-hasura-user-id');
cy.findByPlaceholderText('Value...').type('1234'); cy.findByPlaceholderText('Value or {{Environment_Variable}}').type('1234');
// -------------------- // --------------------
cy.log('**--- Click to Save Permission'); cy.log('**--- Click to Save Permission');
@ -271,8 +271,8 @@ describe('Create a delete type table permission with input validation', () => {
cy.findByText('Forward client headers to webhook').click(); cy.findByText('Forward client headers to webhook').click();
cy.get('[name="definition.timeout"]').clear().type('40'); cy.get('[name="definition.timeout"]').clear().type('40');
cy.findByRole('button', { name: 'Add Additional Headers' }).click(); cy.findByRole('button', { name: 'Add Additional Headers' }).click();
cy.findByPlaceholderText('Key...').type('x-hasura-user-id'); cy.findByPlaceholderText('Key').type('x-hasura-user-id');
cy.findByPlaceholderText('Value...').type('1234'); cy.findByPlaceholderText('Value or {{Environment_Variable}}').type('1234');
// -------------------- // --------------------
cy.log('**--- Click to Save Permission'); cy.log('**--- Click to Save Permission');

View File

@ -46,6 +46,9 @@ Base.play = async ({ canvasElement }: any) => {
// -------------------------------------------------- // --------------------------------------------------
// Step 6: add headers // Step 6: add headers
await userEvent.click(canvas.getByText('Add Additional Headers')); await userEvent.click(canvas.getByText('Add Additional Headers'));
await userEvent.type(canvas.getByPlaceholderText('Key...'), 'x-hasura-name'); await userEvent.type(canvas.getByPlaceholderText('Key'), 'x-hasura-name');
await userEvent.type(canvas.getByPlaceholderText('Value...'), 'hasura_user'); await userEvent.type(
canvas.getByPlaceholderText('Value or {{Environment_Variable}}'),
'hasura_user'
);
}; };

View File

@ -4,10 +4,10 @@ import { IconTooltip } from '../../../../../new-components/Tooltip';
import { Switch } from '../../../../../new-components/Switch'; import { Switch } from '../../../../../new-components/Switch';
import { InputField } from '../../../../../new-components/Form'; import { InputField } from '../../../../../new-components/Form';
import { FaShieldAlt } from 'react-icons/fa'; import { FaShieldAlt } from 'react-icons/fa';
import { RequestHeadersSelector } from '../../../../../new-components/RequestHeadersSelector';
import { BooleanCheckbox } from './BooleanCheckbox'; import { BooleanCheckbox } from './BooleanCheckbox';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Badge } from '../../../../../new-components/Badge'; import { Badge } from '../../../../../new-components/Badge';
import { RequestHeaders } from '../../../../../new-components/RequestHeader';
export const inputValidationSchema = z.object({ export const inputValidationSchema = z.object({
type: z.enum(['http']), type: z.enum(['http']),
@ -70,7 +70,10 @@ export const InputValidation = () => {
<InputField <InputField
learnMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl" learnMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl"
tooltipIcon={ tooltipIcon={
<FaShieldAlt className="h-4 text-muted cursor-pointer" /> <div className="flex items-center">
<p className="text-red-600 pr-xs">*</p>
<FaShieldAlt className="h-4 text-muted cursor-pointer" />
</div>
} }
name="definition.url" name="definition.url"
label="Webhook URL" label="Webhook URL"
@ -79,7 +82,7 @@ export const InputValidation = () => {
description="Note: Provide an URL or use an env var to template the handler URL if you have different URLs for multiple environments." description="Note: Provide an URL or use an env var to template the handler URL if you have different URLs for multiple environments."
/> />
<div className="mb-xs"> <div className="mb-xs">
<label className="flex items-center text-gray-600 font-semibold mb-xs"> <label className="flex items-center text-gray-600 font-semibold">
Header Header
<IconTooltip message="Configure headers for the request to the webhook" /> <IconTooltip message="Configure headers for the request to the webhook" />
</label> </label>
@ -89,7 +92,7 @@ export const InputValidation = () => {
text="Forward client headers to webhook" text="Forward client headers to webhook"
/> />
</div> </div>
<RequestHeadersSelector <RequestHeaders
name="definition.headers" name="definition.headers"
addButtonText="Add Additional Headers" addButtonText="Add Additional Headers"
/> />

View File

@ -0,0 +1,77 @@
import React, { ReactText } from 'react';
import { useFieldArray } from 'react-hook-form';
import { FaPlusCircle, FaShieldAlt } from 'react-icons/fa';
import { Button } from '../Button';
import { RequestHeadersSchema } from './schema';
import { KeyValue } from './components/KeyValue';
import { IconTooltip } from '../Tooltip';
export interface RequestHeadersProps {
name: string;
addButtonText?: ReactText;
}
export const RequestHeaders = (props: RequestHeadersProps) => {
const { name, addButtonText = 'Add' } = props;
const { fields, append, remove } = useFieldArray<
Record<string, RequestHeadersSchema>
>({
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 flex">
Value
<IconTooltip
message={
<div>
Value can be either static string or a template which can
reference environment variables.
<p>Example:</p>
<p>Static string: "abc"</p>
<p>
Template with environment variables:
&#123;&#123;ACTION_BASE_URL&#125;&#125;/payment" or
&#123;&#123;FULL_ACTION_URL&#125;&#125;
</p>
</div>
}
icon={<FaShieldAlt className="h-4 text-muted cursor-pointer" />}
/>
</label>
</div>
{fields.map((field, index) => (
<KeyValue
key={field.id}
fieldId={field.id}
fieldName={name}
rowIndex={index}
removeRow={remove}
/>
))}
</>
) : null}
<Button
data-testid="add-header"
icon={<FaPlusCircle />}
onClick={() => {
append({
name: '',
value: '',
});
}}
size="sm"
>
{addButtonText}
</Button>
</div>
);
};

View File

@ -0,0 +1,53 @@
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 KeyValue = (props: Props) => {
const { fieldId, fieldName, rowIndex, removeRow } = props;
const keyLabel = `${fieldName}[${rowIndex}].name`;
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}
data-test={`header-test${rowIndex}-key`}
/>
<div className="flex rounded">
<input
{...register(valueLabel)}
aria-label={valueLabel}
data-test={`header-test${rowIndex}-value`}
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 or {{Environment_Variable}}"
/>
<div className="col-span-1 flex items-center justify-center pl-1.5">
<RiCloseCircleFill
onClick={() => {
removeRow(rowIndex);
}}
className="cursor-pointer"
data-test={`delete-header-${rowIndex}`}
/>
</div>
</div>
</div>
);
};

View File

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

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const requestHeadersSchema = z.array(
z.object({
name: z.string(),
value: z.string(),
})
);
export type RequestHeadersSchema = z.infer<typeof requestHeadersSchema>;