console: unify webhook handler UX for action, remote schemas and events

[GS-397]: https://hasurahq.atlassian.net/browse/GS-397?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7796
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sean Park-Ross <94021366+seanparkross@users.noreply.github.com>
GitOrigin-RevId: 620aea50e7d1b45835a5996246f46017b2ba5904
This commit is contained in:
Varun Choudhary 2023-02-16 15:42:51 +05:30 committed by hasura-bot
parent ac475fa02e
commit fc40739f62
22 changed files with 299 additions and 214 deletions

View File

@ -78,24 +78,24 @@ X-Hasura-Role: admin
### Args syntax {#metadata-pg-create-event-trigger-syntax} ### Args syntax {#metadata-pg-create-event-trigger-syntax}
| Key | Required | Schema | Description | | Key | Required | Schema | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | |------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| name | true | [TriggerName](/api-reference/syntax-defs.mdx#triggername) | Name of the Event Trigger | | name | true | [TriggerName](/api-reference/syntax-defs.mdx#triggername) | Name of the Event Trigger |
| table | true | [QualifiedTable](/api-reference/syntax-defs.mdx#qualifiedtable) | Object with table name and schema | | table | true | [QualifiedTable](/api-reference/syntax-defs.mdx#qualifiedtable) | Object with table name and schema |
| source | false | [SourceName](/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the table (default: `default`) | | source | false | [SourceName](/api-reference/syntax-defs.mdx#sourcename) | Name of the source database of the table (default: `default`) |
| webhook | false | String | Full url of webhook (\*) | | webhook | false | [WebhookURL](/api-reference/syntax-defs.mdx#webhookurl) | Event Trigger webhook URL |
| webhook_from_env | false | String | Environment variable name of webhook (must exist at boot time) (\*) | | webhook_from_env | false | String | Environment variable name of webhook (Deprecated in favour of [WebhookURL](/api-reference/syntax-defs.mdx#webhookurl)) |
| insert | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for insert operation | | insert | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for insert operation |
| update | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for update operation | | update | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for update operation |
| delete | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for delete operation | | delete | false | [OperationSpec](/api-reference/syntax-defs.mdx#operationspec) | Specification for delete operation |
| headers | false | [ [HeaderFromValue](/api-reference/syntax-defs.mdx#headerfromvalue) \| [HeaderFromEnv](/api-reference/syntax-defs.mdx#headerfromenv) ] | List of headers to be sent with the webhook | | headers | false | [ [HeaderFromValue](/api-reference/syntax-defs.mdx#headerfromvalue) \| [HeaderFromEnv](/api-reference/syntax-defs.mdx#headerfromenv) ] | List of headers to be sent with the webhook |
| retry_conf | false | [RetryConf](/api-reference/syntax-defs.mdx#retryconf) | Retry configuration if event delivery fails | | retry_conf | false | [RetryConf](/api-reference/syntax-defs.mdx#retryconf) | Retry configuration if event delivery fails |
| replace | false | Boolean | If set to true, the Event Trigger is replaced with the new definition | | replace | false | Boolean | If set to true, the Event Trigger is replaced with the new definition |
| enable_manual | false | Boolean | If set to true, the Event Trigger can be invoked manually | | enable_manual | false | Boolean | If set to true, the Event Trigger can be invoked manually |
| request_transform | false | [RequestTransformation](/api-reference/syntax-defs.mdx#requesttransformation) | Attaches a Request Transformation to the Event Trigger. | | request_transform | false | [RequestTransformation](/api-reference/syntax-defs.mdx#requesttransformation) | Attaches a Request Transformation to the Event Trigger. |
| response_transform | false | [ResponseTransformation](/api-reference/syntax-defs.mdx#responsetransformation) | Attaches a Request Transformation to the Event Trigger. | | response_transform | false | [ResponseTransformation](/api-reference/syntax-defs.mdx#responsetransformation) | Attaches a Request Transformation to the Event Trigger. |
| cleanup_config | false | [AutoEventTriggerCleanupConfig](/api-reference/syntax-defs.mdx#autoeventtriggercleanupconfig) | Cleanup config for the auto cleanup process (EE/Cloud only). | | cleanup_config | false | [AutoEventTriggerCleanupConfig](/api-reference/syntax-defs.mdx#autoeventtriggercleanupconfig) | Cleanup config for the auto cleanup process (EE/Cloud only). |
| trigger_on_replication | false | Boolean | Specification for enabling/disabling the Event Trigger during logical replication | | trigger_on_replication | false | Boolean | Specification for enabling/disabling the Event Trigger during logical replication |
(\*) Either `webhook` or `webhook_from_env` are required. (\*) Either `webhook` or `webhook_from_env` are required.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -5,21 +5,8 @@ import {
} from '../Common/stateDefaults'; } from '../Common/stateDefaults';
import { DefaultState } from './types'; import { DefaultState } from './types';
let defaultHandler = '';
if (typeof navigator !== 'undefined') {
const { appVersion } = navigator;
const isLinux =
appVersion.toLowerCase().includes('linux') ||
appVersion.toLowerCase().includes('x11');
if (isLinux) {
defaultHandler = 'http://localhost:3000';
} else {
defaultHandler = 'http://host.docker.internal:3000';
}
}
const state: DefaultState = { const state: DefaultState = {
handler: defaultHandler, handler: '',
actionDefinition: { actionDefinition: {
sdl: defaultActionDefSdl, sdl: defaultActionDefSdl,
error: null, error: null,

View File

@ -2,6 +2,9 @@ import React, { useEffect, useState } from 'react';
import { useDebouncedEffect } from '@/hooks/useDebounceEffect'; import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
import { Analytics, REDACT_EVERYTHING } from '@/features/Analytics'; import { Analytics, REDACT_EVERYTHING } from '@/features/Analytics';
import { inputStyles } from '../../constants'; import { inputStyles } from '../../constants';
import { FaShieldAlt } from 'react-icons/fa';
import { IconTooltip } from '@/new-components/Tooltip';
import { LearnMoreLink } from '@/new-components/LearnMoreLink';
const editorLabel = 'Webhook (HTTP/S) Handler'; const editorLabel = 'Webhook (HTTP/S) Handler';
@ -32,26 +35,30 @@ const HandlerEditor: React.FC<HandlerEditorProps> = ({
return ( return (
<Analytics name="ActionEditor" {...REDACT_EVERYTHING}> <Analytics name="ActionEditor" {...REDACT_EVERYTHING}>
<div className="mb-lg w-4/12"> <div className="mb-lg w-6/12">
<h2 className="text-lg font-semibold mb-xs flex items-center"> <h2 className="text-lg font-semibold mb-xs flex items-center">
{editorLabel} {editorLabel}
<span className="text-red-700 ml-xs mr-sm">*</span> <span className="text-red-700 ml-xs">*</span>
<IconTooltip
message="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
icon={<FaShieldAlt className="h-4 text-muted cursor-pointer" />}
/>
<LearnMoreLink href="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl" />
</h2> </h2>
<p className="text-sm text-gray-600 mb-sm">
Note: Provide an URL or use an env var to template the handler URL if
you have different URLs for multiple environments.
</p>
<input <input
disabled={disabled} disabled={disabled}
type="text" type="text"
name="handler" name="handler"
value={localValue} value={localValue}
onChange={e => setLocalValue(e.target.value)} onChange={e => setLocalValue(e.target.value)}
placeholder="http://custom-logic.com/api" placeholder="http://custom-logic.com/api or {{ACTION_BASE_URL}}/handler"
className={inputStyles} className={inputStyles}
data-test="action-create-handler-input" data-test="action-create-handler-input"
/> />
<p className="text-sm text-gray-600">
Note: You can use an env var to template the handler URL if you have
different URLs for multiple environments.
<br /> e.g. {'{{ACTION_BASE_URL}}/handler'}
</p>
</div> </div>
</Analytics> </Analytics>
); );

View File

@ -61,6 +61,7 @@ import {
RetryConf, RetryConf,
EventTriggerAutoCleanup, EventTriggerAutoCleanup,
} from '../../types'; } from '../../types';
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
interface Props extends InjectedProps {} interface Props extends InjectedProps {}
@ -229,49 +230,53 @@ const Add: React.FC<Props> = props => {
transformState.sessionVars, transformState.sessionVars,
]); ]);
useEffect(() => { useDebouncedEffect(
requestBodyErrorOnChange(''); () => {
requestTransformedBodyOnChange(''); requestBodyErrorOnChange('');
const onResponse = (data: Record<string, any>) => { requestTransformedBodyOnChange('');
parseValidateApiData( const onResponse = (data: Record<string, any>) => {
data, parseValidateApiData(
requestBodyErrorOnChange, data,
undefined, requestBodyErrorOnChange,
requestTransformedBodyOnChange
);
};
const options = getValidateTransformOptions({
version: transformState.version,
inputPayloadString: transformState.requestSampleInput,
webhookUrl: webhook.value,
envVarsFromContext: transformState.envVars,
sessionVarsFromContext: transformState.sessionVars,
transformerBody: transformState.requestBody,
isEnvVar: webhook.type === 'env',
});
if (!webhook.value) {
requestBodyErrorOnChange(
'Please configure your webhook handler to generate request body transform'
);
} else if (transformState.requestBody && webhook.value) {
dispatch(
requestAction(
Endpoints.metadata,
options,
undefined, undefined,
undefined, requestTransformedBodyOnChange
true, );
true };
) const options = getValidateTransformOptions({
).then(onResponse, onResponse); // parseValidateApiData will parse both success and error version: transformState.version,
} inputPayloadString: transformState.requestSampleInput,
}, [ webhookUrl: webhook.value,
transformState.requestSampleInput, envVarsFromContext: transformState.envVars,
transformState.requestBody, sessionVarsFromContext: transformState.sessionVars,
webhook, transformerBody: transformState.requestBody,
transformState.envVars, isEnvVar: webhook.type === 'env',
transformState.sessionVars, });
]); if (!webhook.value) {
requestBodyErrorOnChange(
'Please configure your webhook handler to generate request body transform'
);
} else if (transformState.requestBody && webhook.value) {
dispatch(
requestAction(
Endpoints.metadata,
options,
undefined,
undefined,
true,
true
)
).then(onResponse, onResponse); // parseValidateApiData will parse both success and error
}
},
1000,
[
transformState.requestSampleInput,
transformState.requestBody,
webhook,
transformState.envVars,
transformState.sessionVars,
]
);
const createBtnText = 'Create Event Trigger'; const createBtnText = 'Create Event Trigger';

View File

@ -18,9 +18,9 @@ import {
} from '../../types'; } from '../../types';
import ColumnList from '../Common/ColumnList'; import ColumnList from '../Common/ColumnList';
import FormLabel from './FormLabel'; import FormLabel from './FormLabel';
import DebouncedDropdownInput from '../Common/DropdownWrapper';
import { inputStyles, heading } from '../../constants'; import { inputStyles, heading } from '../../constants';
import { AutoCleanupForm } from '../Common/AutoCleanupForm'; import { AutoCleanupForm } from '../Common/AutoCleanupForm';
import { FaShieldAlt } from 'react-icons/fa';
type CreateETFormProps = { type CreateETFormProps = {
state: LocalEventTriggerState; state: LocalEventTriggerState;
@ -62,7 +62,6 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
handleDatabaseChange, handleDatabaseChange,
handleSchemaChange, handleSchemaChange,
handleTableChange, handleTableChange,
handleWebhookTypeChange,
handleWebhookValueChange, handleWebhookValueChange,
handleOperationsChange, handleOperationsChange,
handleOperationsColumnsChange, handleOperationsColumnsChange,
@ -166,36 +165,31 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
<FormLabel <FormLabel
title="Webhook (HTTP/S) Handler" title="Webhook (HTTP/S) Handler"
tooltip={tooltip.webhookUrlDescription} tooltip={tooltip.webhookUrlDescription}
tooltipIcon={
<FaShieldAlt className="h-4 text-muted cursor-pointer" />
}
knowMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl"
/> />
<div> <div>
<div className="w-72"> <div className="w-1/2">
<DebouncedDropdownInput <p className="text-sm text-gray-600 mb-sm">
dropdownOptions={[ Note: Provide an URL or use an env var to template the handler URL
{ display_text: 'URL', value: 'static' }, if you have different URLs for multiple environments.
{ display_text: 'From env var', value: 'env' }, </p>
]} <input
title={webhook.type === 'static' ? 'URL' : 'From env var'} type="text"
dataKey={webhook.type === 'static' ? 'static' : 'env'} name="handler"
onButtonChange={handleWebhookTypeChange} onChange={e => handleWebhookValueChange(e.target.value)}
onHandlerValChange={handleWebhookValueChange}
required required
bsClass="w-72" value={webhook.value}
handlerVal={webhook.value}
id="webhook-url" id="webhook-url"
inputPlaceHolder={ placeholder="http://httpbin.org/post or {{MY_WEBHOOK_URL}}/handler"
webhook.type === 'static' data-test="webhook"
? 'http://httpbin.org/post' className={`w-82 ${inputStyles}`}
: 'MY_WEBHOOK_URL'
}
testId="webhook"
/> />
</div> </div>
</div> </div>
<br /> <br />
<small>
Note: Specifying the webhook URL via an environmental variable is
recommended if you have different URLs for multiple environments.
</small>
</div> </div>
<hr className="my-md" /> <hr className="my-md" />
{isProConsole(window.__env) && ( {isProConsole(window.__env) && (

View File

@ -1,17 +1,26 @@
import React from 'react'; import React from 'react';
import { IconTooltip } from '@/new-components/Tooltip'; import { IconTooltip } from '@/new-components/Tooltip';
import { LearnMoreLink } from '@/new-components/LearnMoreLink';
type FormLabelProps = { type FormLabelProps = {
title: string; title: string;
tooltip: string; tooltip: string;
tooltipIcon?: React.ReactElement;
knowMoreLink?: string;
}; };
const FormLabel: React.FC<FormLabelProps> = ({ title, tooltip }) => { const FormLabel: React.FC<FormLabelProps> = ({
title,
tooltip,
tooltipIcon,
knowMoreLink,
}) => {
return ( return (
<> <>
<h2 className="text-lg font-semibold mb-xs flex items-center"> <h2 className="text-lg font-semibold mb-xs flex items-center">
{title} {title}
<IconTooltip message={tooltip} /> <IconTooltip message={tooltip} icon={tooltipIcon} />
{knowMoreLink ? <LearnMoreLink href={knowMoreLink} /> : null}
</h2> </h2>
</> </>
); );

View File

@ -12,7 +12,7 @@ export const manualOperationsDescription =
'Trigger manually from table data browser in console'; 'Trigger manually from table data browser in console';
export const webhookUrlDescription = export const webhookUrlDescription =
'POST endpoint which will be triggered with payload on configured events'; 'Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url';
export const advancedOperationDescription = export const advancedOperationDescription =
'For update triggers, webhook will be triggered only when selected columns are modified'; 'For update triggers, webhook will be triggered only when selected columns are modified';

View File

@ -65,6 +65,7 @@ import {
getEventTriggerByName, getEventTriggerByName,
} from '../../../../../metadata/selector'; } from '../../../../../metadata/selector';
import { AutoCleanupForm } from '../Common/AutoCleanupForm'; import { AutoCleanupForm } from '../Common/AutoCleanupForm';
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
interface Props extends InjectedProps {} interface Props extends InjectedProps {}
@ -291,25 +292,29 @@ const Modify: React.FC<Props> = props => {
transformState.sessionVars, transformState.sessionVars,
]); ]);
useEffect(() => { useDebouncedEffect(
if ( () => {
transformState.requestBody && if (
state.webhook?.value && transformState.requestBody &&
!transformState.requestTransformedBody state.webhook?.value &&
) { !transformState.requestTransformedBody
requestBodyErrorOnChange(''); ) {
dispatch( requestBodyErrorOnChange('');
requestAction( dispatch(
Endpoints.metadata, requestAction(
reqBodyoptions, Endpoints.metadata,
undefined, reqBodyoptions,
undefined, undefined,
true, undefined,
true true,
) true
).then(onRequestBodyResponse, onRequestBodyResponse); )
} ).then(onRequestBodyResponse, onRequestBodyResponse);
}, [transformState.requestTransformedBody]); }
},
1000,
[transformState.requestTransformedBody]
);
const saveWrapper = const saveWrapper =
(property?: EventTriggerProperty) => (property?: EventTriggerProperty) =>

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { FaShieldAlt } from 'react-icons/fa';
import Editor from '../../../../Common/Layout/ExpandableEditor/Editor'; import Editor from '../../../../Common/Layout/ExpandableEditor/Editor';
import { inputStyles } from '../../constants'; import { inputStyles } from '../../constants';
import { EventTrigger, URLConf, VoidCallback } from '../../types'; import { EventTrigger, URLConf, VoidCallback } from '../../types';
import { parseServerWebhook } from '../../utils'; import { parseServerWebhook } from '../../utils';
import FormLabel from '../Add/FormLabel';
import DebouncedDropdownInput from '../Common/DropdownWrapper'; import DebouncedDropdownInput from '../Common/DropdownWrapper';
type WebhookEditorProps = { type WebhookEditorProps = {
@ -14,7 +16,6 @@ type WebhookEditorProps = {
const WebhookEditor = (props: WebhookEditorProps) => { const WebhookEditor = (props: WebhookEditorProps) => {
const { currentTrigger, webhook, setWebhook, save } = props; const { currentTrigger, webhook, setWebhook, save } = props;
const existingWebhook = parseServerWebhook( const existingWebhook = parseServerWebhook(
currentTrigger.configuration.webhook, currentTrigger.configuration.webhook,
currentTrigger.configuration.webhook_from_env currentTrigger.configuration.webhook_from_env
@ -50,37 +51,62 @@ const WebhookEditor = (props: WebhookEditorProps) => {
); );
const expanded = () => ( const expanded = () => (
<div className="pb-sm pt-sm max-w-80"> <div className="w-1/2">
<DebouncedDropdownInput <p className="text-sm text-gray-600 mb-sm">
dropdownOptions={[ Note: Provide an URL or use an env var to template the handler URL if
{ display_text: 'URL', value: 'static' }, you have different URLs for multiple environments.
{ display_text: 'From env var', value: 'env' }, </p>
]} {existingWebhook?.type === 'env' ? (
title={webhook.type === 'env' ? 'From env var' : 'URL'} <DebouncedDropdownInput
dataKey={webhook.type === 'env' ? 'env' : 'static'} dropdownOptions={[
onButtonChange={handleWebhookTypeChange} { display_text: 'URL', value: 'static' },
onHandlerValChange={handleWebhookValueChange} { display_text: 'From env var', value: 'env' },
required ]}
bsClass={`${inputStyles} w-72`} title={webhook.type === 'env' ? 'From env var' : 'URL'}
handlerVal={webhook.value} dataKey={webhook.type === 'env' ? 'env' : 'static'}
id="webhook-url" onButtonChange={handleWebhookTypeChange}
inputPlaceHolder={ onHandlerValChange={handleWebhookValueChange}
webhook.type === 'env' ? 'MY_WEBHOOK_URL' : 'http://httpbin.org/post' required
} bsClass={`w-82`}
testId="webhook" handlerVal={webhook.value}
/> id="webhook-url"
inputPlaceHolder={
webhook.type === 'env'
? 'MY_WEBHOOK_URL'
: 'http://httpbin.org/post'
}
testId="webhook"
/>
) : (
<input
type="text"
name="handler"
onChange={e => handleWebhookValueChange(e.target.value)}
required
className={`${inputStyles} w-82`}
value={
webhook.type === 'static' ? webhook.value : `{{${webhook.value}}}`
}
id="webhook-url"
placeholder="http://httpbin.org/post or {{MY_WEBHOOK_URL}}/handler"
data-test="webhook"
/>
)}
<br /> <br />
<small>
Note: Specifying the webhook URL via an environmental variable is
recommended if you have different URLs for multiple environments.
</small>
</div> </div>
); );
return ( return (
<div className="w-full border-b border-solid border-gray-300 mb-md"> <div className="w-full border-b border-solid border-gray-300 mb-md">
<div className="mb-md"> <div className="mb-md">
<h4 className="text-lg font-bold mb-md">Webhook (HTTP/S) Handler</h4> <FormLabel
title="Webhook (HTTP/S) Handler"
tooltip="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
tooltipIcon={
<FaShieldAlt className="h-4 text-muted cursor-pointer" />
}
knowMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl"
/>
<Editor <Editor
editorCollapsed={collapsed} editorCollapsed={collapsed}
editorExpanded={expanded} editorExpanded={expanded}

View File

@ -12,6 +12,8 @@ import CommonHeader from '../../../Common/Layout/ReusableHeader/Header';
import GraphQLCustomizationEdit from './GraphQLCustomization/GraphQLCustomizationEdit'; import GraphQLCustomizationEdit from './GraphQLCustomization/GraphQLCustomizationEdit';
import { focusYellowRing, inputStyles, subHeading } from '../constants'; import { focusYellowRing, inputStyles, subHeading } from '../constants';
import { FaShieldAlt } from 'react-icons/fa';
import { LearnMoreLink } from '@/new-components/LearnMoreLink';
class Common extends React.Component { class Common extends React.Component {
getPlaceHolderText(valType) { getPlaceHolderText(valType) {
@ -48,6 +50,7 @@ class Common extends React.Component {
forwardClientHeaders, forwardClientHeaders,
comment, comment,
customization, customization,
isEnvVarEnabled,
} = this.props; } = this.props;
const { isModify } = this.props.editState; const { isModify } = this.props.editState;
@ -58,8 +61,9 @@ class Common extends React.Component {
const tooltips = { const tooltips = {
graphqlurl: ( graphqlurl: (
<IconTooltip <IconTooltip
message="Remote GraphQL servers URL. E.g. https://my-domain/v1/graphql" message="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
side="right" side="right"
icon={<FaShieldAlt className="h-4 text-muted cursor-pointer" />}
/> />
), ),
clientHeaderForward: ( clientHeaderForward: (
@ -142,42 +146,59 @@ class Common extends React.Component {
<hr className="my-md" /> <hr className="my-md" />
<div className={`${subHeading} flex items-center`}> <div className={`${subHeading} flex items-center`}>
GraphQL server URL *{tooltips.graphqlurl} GraphQL server URL *{tooltips.graphqlurl}
<LearnMoreLink href="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl" />
</div> </div>
<div className={'w-80'}> <p className="text-sm text-gray-600 mb-sm w-1/2">
<DropdownButton Note: Provide an URL or use an env var to template the handler URL if
dropdownOptions={[ you have different URLs for multiple environments.
{ display_text: 'URL', value: 'manualUrl' }, </p>
{ display_text: 'From env var', value: 'envName' }, <div className={'w-1/2'}>
]} {isEnvVarEnabled ? (
title={ <DropdownButton
(manualUrl !== null && 'URL') || dropdownOptions={[
(envName !== null && 'From env var') || { display_text: 'URL', value: 'manualUrl' },
'Value' { display_text: 'From env var', value: 'envName' },
} ]}
dataKey={ title={
(manualUrl !== null && 'manualUrl') || (manualUrl !== null && 'URL') ||
(envName !== null && 'envName') (envName !== null && 'From env var') ||
} 'Value'
onButtonChange={this.toggleUrlParam.bind(this)} }
onInputChange={this.handleInputChange.bind(this)} dataKey={
required={urlRequired} (manualUrl !== null && 'manualUrl') ||
bsClass="w-80" (envName !== null && 'envName')
inputVal={manualUrl || envName} }
disabled={isDisabled} onButtonChange={this.toggleUrlParam.bind(this)}
id="graphql-server-url" onInputChange={this.handleInputChange.bind(this)}
inputPlaceHolder={ required={urlRequired}
(manualUrl !== null && bsClass="w-80"
'https://my-graphql-service.com/graphql') || inputVal={envName || manualUrl}
(envName !== null && 'MY_GRAPHQL_ENDPOINT') disabled={isDisabled}
} id="graphql-server-url"
testId="remote-schema-graphql-url" inputPlaceHolder={
/> (manualUrl !== null &&
'https://my-graphql-service.com/graphql') ||
(envName !== null && 'MY_GRAPHQL_ENDPOINT')
}
testId="remote-schema-graphql-url"
/>
) : (
<input
type="text"
name="handler"
onChange={this.handleInputChange.bind(this)}
required={urlRequired}
className={`w-3/4 ${inputStyles}`}
data-key={'manualUrl'}
value={manualUrl}
disabled={isDisabled}
id="graphql-server-url"
placeholder="https://my-graphql-service.com/graphql or {{MY_WEBHOOK_URL}}/graphql"
data-test="remote-schema-graphql-url"
/>
)}
</div> </div>
<br /> <br />
<small>
Note: Specifying the server URL via an environmental variable is
recommended if you have different URLs for multiple environments.
</small>
<div className={`${subHeading} pt-md`}> <div className={`${subHeading} pt-md`}>
Headers for the remote GraphQL server Headers for the remote GraphQL server
</div> </div>

View File

@ -199,6 +199,12 @@ class Edit extends React.Component {
{ redactText: true } { redactText: true }
); );
const isEnvVarEnabled = () => {
return this.props.allRemoteSchemas.some(
rs => rs.name === remoteSchemaName && rs.definition.url_from_env
);
};
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -227,7 +233,7 @@ class Edit extends React.Component {
this.editClicked(); this.editClicked();
}} }}
> >
<Common {...this.props} /> <Common {...this.props} isEnvVarEnabled={isEnvVarEnabled()} />
{generateMigrateBtns()} {generateMigrateBtns()}
</form> </form>
</Analytics> </Analytics>

View File

@ -11,6 +11,7 @@ import {
ScheduleEventPayloadInput, ScheduleEventPayloadInput,
} from './components'; } from './components';
import { ScheduledTime } from './components/ScheduledTime'; import { ScheduledTime } from './components/ScheduledTime';
import { FaShieldAlt } from 'react-icons/fa';
type Props = { type Props = {
/** /**
@ -51,10 +52,15 @@ const OneOffScheduledEventForm = (props: Props) => {
</div> </div>
<div className="mb-md"> <div className="mb-md">
<InputField <InputField
learnMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl"
tooltipIcon={
<FaShieldAlt className="h-4 text-muted cursor-pointer" />
}
name="webhook" name="webhook"
label="Webhook URL" label="Webhook URL"
placeholder="https://httpbin.com/post" placeholder="https://httpbin.com/post or {{MY_WEBHOOK_URL}}/handler"
tooltip="The HTTP URL that should be triggered." tooltip="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
description="Note: Provide an URL or use an env var to template the handler URL if you have different URLs for multiple environments."
/> />
</div> </div>
<div className="mb-md"> <div className="mb-md">

View File

@ -19,6 +19,7 @@ import {
} from './utils'; } from './utils';
import { useCronMetadataMigration, useDefaultValues } from './hooks'; import { useCronMetadataMigration, useDefaultValues } from './hooks';
import { CronRequestTransformation } from './components/CronRequestTransformation'; import { CronRequestTransformation } from './components/CronRequestTransformation';
import { FaQuestionCircle, FaShieldAlt } from 'react-icons/fa';
type Props = { type Props = {
/** /**
@ -63,10 +64,15 @@ const FormContent = (props: FormContentProps) => {
<hr className="my-md" /> <hr className="my-md" />
<div className="mb-xs w-1/2"> <div className="mb-xs w-1/2">
<InputField <InputField
learnMoreLink="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl"
tooltipIcon={
<FaShieldAlt className="h-4 text-muted cursor-pointer" />
}
name="webhook" name="webhook"
label="Webhook URL" label="Webhook URL"
placeholder="https://httpbin.com/post" placeholder="https://httpbin.com/post or {{MY_WEBHOOK_URL}}/handler"
tooltip="The HTTP URL that should be triggered. You can also provide the URL from environment variables, e.g. {{MY_WEBHOOK_URL}}" tooltip="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
description="Note: Provide an URL or use an env var to template the handler URL if you have different URLs for multiple environments."
/> />
</div> </div>
<div className="mb-xs w-1/2"> <div className="mb-xs w-1/2">

View File

@ -16,7 +16,9 @@ export const CronScheduleSelector = () => {
return ( return (
<> <>
<div className="block flex items-center text-gray-600 font-semibold"> <div className="block flex items-center text-gray-600 font-semibold">
<label htmlFor="schedule">Cron Schedule</label> <label htmlFor="schedule" className="font-semibold">
Cron Schedule
</label>
<IconTooltip message="Schedule for your cron (events are created based on the UTC timezone)" /> <IconTooltip message="Schedule for your cron (events are created based on the UTC timezone)" />
<LearnMoreLink <LearnMoreLink
href="https://crontab.guru/#*_*_*_*_*" href="https://crontab.guru/#*_*_*_*_*"

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { IconTooltip } from '@/new-components/Tooltip'; import { IconTooltip } from '@/new-components/Tooltip';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { FaShieldAlt } from 'react-icons/fa';
import { LearnMoreLink } from '@/new-components/LearnMoreLink';
export const GraphQLServiceUrl = () => { export const GraphQLServiceUrl = () => {
const { watch, register } = useFormContext(); const { watch, register } = useFormContext();
@ -9,28 +11,21 @@ export const GraphQLServiceUrl = () => {
<div className="mb-md w-6/12"> <div className="mb-md w-6/12">
<label className="block flex items-center text-gray-600 font-semibold mb-xs"> <label className="block flex items-center text-gray-600 font-semibold mb-xs">
GraphQL Service URL GraphQL Service URL
<IconTooltip message="Remote GraphQL servers URL. E.g. https://my-domain/v1/graphql" /> <IconTooltip
message="Environment variables and secrets are available using the {{VARIABLE}} tag. Environment variable templating is available for this field. Example: https://{{ENV_VAR}}/endpoint_url"
icon={<FaShieldAlt className="h-4 text-muted cursor-pointer" />}
/>
<LearnMoreLink href="https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl" />
</label> </label>
<p className="text-sm text-gray-600 mb-sm"> <p className="text-sm text-gray-600 mb-sm">
Note: Specifying the server URL via an environmental variable is Note: Provide an URL or use an env var to template the handler URL if
recommended if you have different URLs for multiple environments. you have different URLs for multiple environments.
</p> </p>
<div className="flex shadow-sm rounded"> <div className="flex shadow-sm rounded">
<select
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"
{...register('url.type')}
>
<option value="from_url">URL</option>
<option value="from_env">Env Var</option>
</select>
<input <input
type="text" 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" 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={ placeholder="https://myservice.com/graphql or {{MY_WEBHOOK_URL}}/graphql"
url?.type === 'from_url'
? 'https://myservice.com/graphql'
: 'MY_GRAPHQL_ENDPOINT'
}
{...register('url.value')} {...register('url.value')}
data-testid="url" data-testid="url"
/> />

View File

@ -39,6 +39,10 @@ export type FieldWrapperPassThroughProps = {
* Render line breaks in the description * Render line breaks in the description
*/ */
renderDescriptionLineBreaks?: boolean; renderDescriptionLineBreaks?: boolean;
/**
* tooltip icon other then ?
*/
tooltipIcon?: React.ReactElement;
} & DiscriminatedTypes< } & DiscriminatedTypes<
{ {
/** /**
@ -110,6 +114,7 @@ export const FieldWrapper = (props: FieldWrapperProps) => {
className, className,
size = 'full', size = 'full',
error, error,
tooltipIcon,
children, children,
description, description,
tooltip, tooltip,
@ -158,7 +163,9 @@ export const FieldWrapper = (props: FieldWrapperProps) => {
{label} {label}
{loading ? <Skeleton className="absolute inset-0" /> : null} {loading ? <Skeleton className="absolute inset-0" /> : null}
</span> </span>
{!loading && tooltip ? <IconTooltip message={tooltip} /> : null} {!loading && tooltip ? (
<IconTooltip message={tooltip} icon={tooltipIcon} />
) : null}
{!loading && !!learnMoreLink && ( {!loading && !!learnMoreLink && (
<LearnMoreLink href={learnMoreLink} /> <LearnMoreLink href={learnMoreLink} />
)} )}

View File

@ -18,7 +18,7 @@ export const LearnMoreLink: React.VFC<LearnMoreLinkProps> = props => {
href={href} href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`ml-xs italic text-sm text-secondary ${className}`} className={`ml-xs italic text-sm font-thin text-secondary ${className}`}
> >
{text} {text}
</a> </a>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { ReactElement, ReactNode } from 'react';
import { Tooltip, TooltipProps } from '@/new-components/Tooltip'; import { Tooltip, TooltipProps } from '@/new-components/Tooltip';
import { FaQuestionCircle } from 'react-icons/fa'; import { FaQuestionCircle } from 'react-icons/fa';
@ -11,6 +11,10 @@ export type IconTooltipProps = {
* The tooltip icon classes * The tooltip icon classes
*/ */
className?: string; className?: string;
/**
* tooltip icon other then ?
*/
icon?: ReactNode;
} & Pick<TooltipProps, 'side'> & } & Pick<TooltipProps, 'side'> &
Pick<TooltipProps, 'defaultOpen'>; Pick<TooltipProps, 'defaultOpen'>;
@ -19,6 +23,7 @@ export const IconTooltip: React.VFC<IconTooltipProps> = ({
className, className,
side = 'right', side = 'right',
defaultOpen = false, defaultOpen = false,
icon,
}) => ( }) => (
<Tooltip <Tooltip
tooltipContentChildren={message} tooltipContentChildren={message}
@ -26,8 +31,12 @@ export const IconTooltip: React.VFC<IconTooltipProps> = ({
defaultOpen={defaultOpen} defaultOpen={defaultOpen}
className="flex items-center" className="flex items-center"
> >
<FaQuestionCircle {!icon ? (
className={`h-4 text-muted cursor-pointer ${className}`} <FaQuestionCircle
/> className={`h-4 text-muted cursor-pointer ${className}`}
/>
) : (
icon
)}
</Tooltip> </Tooltip>
); );