mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
console: use new cron trigger form
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7236 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> GitOrigin-RevId: 2b32f418258d1cd6ba269de3e88fee939d1b640b
This commit is contained in:
parent
c256dcef7b
commit
a8a36bf7ee
@ -19,12 +19,12 @@ const NumberedSidebar: React.FC<NumberedSidebarProps> = ({
|
||||
return (
|
||||
<>
|
||||
{number ? <div className={sidebarNumberStyles}>{number}</div> : null}
|
||||
<label className="flex items-center block text-gray-600 font-medium">
|
||||
{title}
|
||||
{url ? <KnowMore url={url} /> : null}
|
||||
</label>
|
||||
<div className="flex items-center mb-sm">
|
||||
<div>
|
||||
<label className="flex items-center block text-gray-600 font-medium">
|
||||
{title}
|
||||
{url ? <KnowMore url={url} /> : null}
|
||||
</label>
|
||||
{description ? (
|
||||
<p className="text-sm text-gray-600">{description}</p>
|
||||
) : null}
|
||||
|
@ -62,7 +62,7 @@ const SampleContextTransforms: React.FC<SampleContextTransformsProps> = ({
|
||||
_BASE_URL
|
||||
</span>
|
||||
}
|
||||
number="1"
|
||||
number={transformationType === 'event' ? '' : '1'}
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
@ -87,40 +87,42 @@ const SampleContextTransforms: React.FC<SampleContextTransformsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Sample Session Variables"
|
||||
description={
|
||||
<span>
|
||||
Enter a sample input for your provided session variables.
|
||||
<br />
|
||||
e.g. the sample value for x-hasura-user-id
|
||||
</span>
|
||||
}
|
||||
number="2"
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Session Variables
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Value
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localSessionVars}
|
||||
setPairs={sv => {
|
||||
setLocalSessionVars(sv);
|
||||
}}
|
||||
testId="session-vars"
|
||||
{transformationType !== 'event' && (
|
||||
<div className="mb-md">
|
||||
<NumberedSidebar
|
||||
title="Sample Session Variables"
|
||||
description={
|
||||
<span>
|
||||
Enter a sample input for your provided session variables.
|
||||
<br />
|
||||
e.g. the sample value for x-hasura-user-id
|
||||
</span>
|
||||
}
|
||||
number="2"
|
||||
/>
|
||||
<div className="grid gap-3 grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Session Variables
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-600 font-medium mb-xs">
|
||||
Value
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 grid-cols-3 mb-sm">
|
||||
<KeyValueInput
|
||||
pairs={localSessionVars}
|
||||
setPairs={sv => {
|
||||
setLocalSessionVars(sv);
|
||||
}}
|
||||
testId="session-vars"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -56,6 +56,8 @@ import {
|
||||
SET_RESPONSE_TRANSFORM_STATE,
|
||||
defaultActionResponseBody,
|
||||
SetResponseTransformState,
|
||||
defaultCronTriggerRequestBody,
|
||||
defaultCronTriggerSampleInput,
|
||||
} from './stateDefaults';
|
||||
import { getSessionVarsFromLS, getEnvVarsFromLS } from './utils';
|
||||
|
||||
@ -274,6 +276,23 @@ export const getEventRequestTransformDefaultState =
|
||||
};
|
||||
};
|
||||
|
||||
export const getCronTriggerRequestTransformDefaultState =
|
||||
(): RequestTransformState => {
|
||||
return {
|
||||
...requestTransformState,
|
||||
envVars: getEnvVarsFromLS(),
|
||||
sessionVars: getSessionVarsFromLS(),
|
||||
requestQueryParams: [{ name: '', value: '' }],
|
||||
requestAddHeaders: [{ name: '', value: '' }],
|
||||
requestBody: {
|
||||
action: requestBodyActionState.transformApplicationJson,
|
||||
template: defaultCronTriggerRequestBody,
|
||||
form_template: [{ name: 'payload', value: '{{$body.payload}}' }],
|
||||
},
|
||||
requestSampleInput: defaultCronTriggerSampleInput,
|
||||
};
|
||||
};
|
||||
|
||||
export const requestTransformReducer = (
|
||||
state = requestTransformState,
|
||||
action: RequestTransformEvents
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Action as ReduxAction } from 'redux';
|
||||
import { getEventRequestSampleInput } from '@/components/Services/Events/EventTriggers/utils';
|
||||
import {
|
||||
getCronTriggerRequestSampleInput,
|
||||
getEventRequestSampleInput,
|
||||
} from '@/components/Services/Events/EventTriggers/utils';
|
||||
import { getActionRequestSampleInput } from '../../Services/Actions/Add/utils';
|
||||
import {
|
||||
defaultActionDefSdl,
|
||||
@ -224,6 +227,8 @@ export const defaultActionRequestSampleInput = getActionRequestSampleInput(
|
||||
|
||||
export const defaultEventRequestSampleInput = getEventRequestSampleInput();
|
||||
|
||||
export const defaultCronTriggerSampleInput = getCronTriggerRequestSampleInput();
|
||||
|
||||
export const defaultActionRequestBody = `{
|
||||
"users": {
|
||||
"name": {{$body.input.arg1.username}},
|
||||
@ -242,6 +247,10 @@ export const defaultEventRequestBody = `{
|
||||
}
|
||||
}`;
|
||||
|
||||
export const defaultCronTriggerRequestBody = `{
|
||||
"payload": {{$body.payload}}
|
||||
}`;
|
||||
|
||||
const defaultActionRequestJsonPayload = `{
|
||||
"users": {
|
||||
"name": "username",
|
||||
|
@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
// import DateTimePicker from 'react-datetime';
|
||||
// import { Moment } from 'moment';
|
||||
import './ReactDateTime.css';
|
||||
import { useScheduledTrigger, defaultCronExpr } from '../../CronTriggers/state';
|
||||
import AceEditor from '../../../../Common/AceEditor/BaseEditor';
|
||||
import Toggle from '../../../../Common/Toggle/Toggle';
|
||||
import CollapsibleToggle from '../../../../Common/CollapsibleToggle/CollapsibleToggle';
|
||||
import Headers from '../../../../Common/Headers/Headers';
|
||||
import RetryConf from './RetryConfEditor';
|
||||
import FrequentlyUsedCrons from './FrequentlyUsedCrons';
|
||||
import FormSection from './FormSection';
|
||||
import { heading, inputStyles } from '../../constants';
|
||||
|
||||
type Props = ReturnType<typeof useScheduledTrigger>;
|
||||
|
||||
const Form: React.FC<Props> = props => {
|
||||
const { state, setState } = props;
|
||||
|
||||
const {
|
||||
name,
|
||||
webhook,
|
||||
schedule,
|
||||
payload,
|
||||
headers,
|
||||
comment,
|
||||
includeInMetadata,
|
||||
} = state;
|
||||
|
||||
const setName = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setState.name(e.target.value);
|
||||
const setWebhookValue = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setState.webhook(e.target.value);
|
||||
const setScheduleValue = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setState.schedule(e.target.value);
|
||||
const setComment = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setState.comment(e.target.value);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FormSection
|
||||
heading="Name"
|
||||
tooltip="Name of the trigger"
|
||||
id="trigger-name"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="name"
|
||||
className={`${inputStyles} w-80`}
|
||||
value={name}
|
||||
onChange={setName}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
heading="Webhook"
|
||||
tooltip="The HTTP URL that should be triggered. You can also provide the URL from environment variables, e.g. {{MY_WEBHOOK_URL}}"
|
||||
id="trigger-webhook"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="http://httpbin.org/post"
|
||||
className={`${inputStyles} w-80`}
|
||||
value={webhook}
|
||||
onChange={setWebhookValue}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
id="trigger-schedule"
|
||||
tooltip="Schedule for your cron (events are created based on the UTC timezone)"
|
||||
heading="Cron Schedule"
|
||||
>
|
||||
<div className="mb-5 flex flex-row items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={defaultCronExpr}
|
||||
className={`${inputStyles} w-80 mr-5`}
|
||||
value={schedule}
|
||||
onChange={setScheduleValue}
|
||||
/>
|
||||
<a
|
||||
className="cursor-pointer"
|
||||
href="https://crontab.guru/#*_*_*_*_*"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Build a cron expression
|
||||
</a>
|
||||
</div>
|
||||
<FrequentlyUsedCrons setCron={setState.schedule} />
|
||||
</FormSection>
|
||||
<FormSection
|
||||
id="trigger-payload"
|
||||
tooltip="The request payload for the HTTP trigger"
|
||||
heading="Payload"
|
||||
>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
value={payload}
|
||||
onChange={setState.payload}
|
||||
height="200px"
|
||||
setOptions={{ useWorker: false }}
|
||||
/>
|
||||
</FormSection>
|
||||
<CollapsibleToggle
|
||||
title={<h2 className={heading}>Advanced</h2>}
|
||||
testId="advanced-configuration"
|
||||
>
|
||||
<FormSection
|
||||
id="trigger-headers"
|
||||
heading="Headers"
|
||||
tooltip="Configure headers for the request to the webhook"
|
||||
>
|
||||
<Headers headers={headers} setHeaders={setState.headers} />
|
||||
</FormSection>
|
||||
<FormSection
|
||||
id="trigger-retry-conf"
|
||||
heading="Retry configuration"
|
||||
tooltip="Retry configuration if the call to the webhook fails"
|
||||
>
|
||||
<RetryConf
|
||||
retryConf={state.retryConf}
|
||||
setRetryConf={setState.retryConf}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
id="trigger-include-in-metadata"
|
||||
heading="Include in Metadata"
|
||||
tooltip="If enabled, this cron trigger will be included in the metadata of GraphqL Engine i.e. it will be a part of the metadata that is exported as migrations."
|
||||
>
|
||||
<div className="flex mr-xs">
|
||||
<Toggle
|
||||
checked={includeInMetadata}
|
||||
onChange={setState.toggleIncludeInMetadata}
|
||||
className="mr-xs"
|
||||
icons={false}
|
||||
/>
|
||||
<span>Include this trigger in Hasura Metadata</span>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
id="trigger-comment"
|
||||
heading="Comment"
|
||||
tooltip="Description of your cron trigger"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="comment"
|
||||
className={`${inputStyles} mr-2.5 w-4/12`}
|
||||
value={comment || ''}
|
||||
onChange={setComment}
|
||||
/>
|
||||
</FormSection>
|
||||
</CollapsibleToggle>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
@ -1,33 +1,13 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { browserHistory } from 'react-router';
|
||||
import { Analytics, REDACT_EVERYTHING } from '@/features/Analytics';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { useScheduledTrigger, LocalScheduledTriggerState } from '../state';
|
||||
import CronTriggerFrom from '../../Common/Components/CronTriggerForm';
|
||||
import {
|
||||
getReactHelmetTitle,
|
||||
mapDispatchToPropsEmpty,
|
||||
} from '../../../../Common/utils/reactUtils';
|
||||
import { MapStateToProps } from '../../../../../types';
|
||||
import { addScheduledTrigger } from '../../ServerIO';
|
||||
import { CronTriggers } from '@/features/CronTriggers';
|
||||
|
||||
import { getReactHelmetTitle } from '../../../../Common/utils/reactUtils';
|
||||
import { EVENTS_SERVICE_HEADING, CRON_TRIGGER } from '../../constants';
|
||||
|
||||
interface Props extends InjectedProps {
|
||||
initState?: LocalScheduledTriggerState;
|
||||
}
|
||||
|
||||
const Main: React.FC<Props> = props => {
|
||||
const { dispatch, initState, readOnlyMode } = props;
|
||||
const { state, setState } = useScheduledTrigger(initState);
|
||||
|
||||
const callback = () => setState.loading('add', false);
|
||||
const onSave = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
setState.loading('add', true);
|
||||
dispatch(addScheduledTrigger(state, callback, callback));
|
||||
};
|
||||
|
||||
export const AddConnector: React.FC = () => {
|
||||
return (
|
||||
<Analytics name="AddScheduledTrigger" {...REDACT_EVERYTHING}>
|
||||
<div className="md-md">
|
||||
@ -37,42 +17,17 @@ const Main: React.FC<Props> = props => {
|
||||
EVENTS_SERVICE_HEADING
|
||||
)}
|
||||
/>
|
||||
<div className="font-bold mb-xl text-[18px] pb-0">
|
||||
Create a cron trigger
|
||||
</div>
|
||||
<CronTriggerFrom state={state} setState={setState} />
|
||||
{!readOnlyMode && (
|
||||
<div className="mr-xl">
|
||||
<Analytics
|
||||
name="events-tab-button-create-cron-trigger"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
isLoading={state.loading.add}
|
||||
loadingText="Creating..."
|
||||
onClick={onSave}
|
||||
mode="primary"
|
||||
disabled={state.loading.add}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Analytics>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-subtitle font-bold pt-md pb-md mt-0 mb-0 pl-4">
|
||||
Create a new cron trigger
|
||||
</h2>
|
||||
<CronTriggers.Form
|
||||
onSuccess={(triggerName?: string) => {
|
||||
browserHistory.push(`/events/cron/${triggerName}/modify`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
type PropsFromState = {
|
||||
readOnlyMode: boolean;
|
||||
};
|
||||
const mapStateToProps: MapStateToProps<PropsFromState> = state => ({
|
||||
readOnlyMode: state.main.readOnlyMode,
|
||||
});
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToPropsEmpty);
|
||||
type InjectedProps = ConnectedProps<typeof connector>;
|
||||
|
||||
const AddConnector = connector(Main);
|
||||
export default AddConnector;
|
||||
|
@ -112,7 +112,7 @@ const STContainer: React.FC<Props> = ({
|
||||
return (
|
||||
<Analytics name="CronTriggers" {...REDACT_EVERYTHING}>
|
||||
<div
|
||||
className={`${styles.view_stitch_schema_wrapper} ${styles.addWrapper}`}
|
||||
className={`${styles.view_stitch_schema_wrapper} ${styles.addWrapper} pt-5`}
|
||||
>
|
||||
<Helmet
|
||||
title={getReactHelmetTitle(
|
||||
|
@ -4,6 +4,8 @@ import { useAppSelector } from '@/store';
|
||||
import { useQuery, UseQueryOptions } from 'react-query';
|
||||
import { CronTriggerAPIResult, ScheduledTrigger } from '../../types';
|
||||
|
||||
export const CRON_TRIGGERS_QUERY_KEY = 'cronTrigger';
|
||||
|
||||
export function useGetCronTriggers(
|
||||
queryOptions?: Omit<
|
||||
UseQueryOptions<
|
||||
@ -30,7 +32,7 @@ export function useGetCronTriggers(
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: 'cronTrigger',
|
||||
queryKey: CRON_TRIGGERS_QUERY_KEY,
|
||||
queryFn,
|
||||
...queryOptions,
|
||||
select: data => {
|
||||
|
@ -1,87 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Analytics, REDACT_EVERYTHING } from '@/features/Analytics';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { useScheduledTrigger } from '../state';
|
||||
import { CronTriggers } from '@/features/CronTriggers';
|
||||
import { browserHistory } from 'react-router';
|
||||
import { ScheduledTrigger } from '../../types';
|
||||
import { Dispatch } from '../../../../../types';
|
||||
import { parseServerScheduledTrigger } from '../utils';
|
||||
import CronTriggerFrom from '../../Common/Components/CronTriggerForm';
|
||||
import { saveScheduledTrigger, deleteScheduledTrigger } from '../../ServerIO';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
currentTrigger?: ScheduledTrigger;
|
||||
};
|
||||
|
||||
const Modify: React.FC<Props> = props => {
|
||||
const { dispatch, currentTrigger } = props;
|
||||
const { state, setState } = useScheduledTrigger();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentTrigger) {
|
||||
const initState = parseServerScheduledTrigger(currentTrigger);
|
||||
setState.bulk(initState);
|
||||
}
|
||||
}, [currentTrigger]);
|
||||
|
||||
if (!currentTrigger) {
|
||||
return null;
|
||||
}
|
||||
const deleteFunc = () => {
|
||||
const requestCallback = () => {
|
||||
setState.loading('delete', false);
|
||||
queryClient.refetchQueries(['cronTrigger'], { active: true });
|
||||
};
|
||||
setState.loading('delete', true);
|
||||
dispatch(
|
||||
deleteScheduledTrigger(currentTrigger, requestCallback, requestCallback)
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
const requestCallback = () => {
|
||||
setState.loading('modify', false);
|
||||
};
|
||||
setState.loading('modify', true);
|
||||
dispatch(
|
||||
saveScheduledTrigger(
|
||||
state,
|
||||
currentTrigger,
|
||||
requestCallback,
|
||||
requestCallback
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const name = props.currentTrigger?.name;
|
||||
return (
|
||||
<Analytics name="ScheduledTriggerModify" {...REDACT_EVERYTHING}>
|
||||
<div className="mb-md">
|
||||
<CronTriggerFrom state={state} setState={setState} />
|
||||
<div className="flex">
|
||||
<div className="mr-md">
|
||||
<Button
|
||||
onClick={onSave}
|
||||
mode="primary"
|
||||
disabled={state.loading.modify}
|
||||
loadingText="Saving..."
|
||||
isLoading={state.loading.modify}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mr-md">
|
||||
<Button
|
||||
onClick={deleteFunc}
|
||||
mode="destructive"
|
||||
disabled={state.loading.delete}
|
||||
>
|
||||
{state.loading.delete ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CronTriggers.Form
|
||||
cronTriggerName={name}
|
||||
onSuccess={(triggerName?: string) => {
|
||||
browserHistory.push(`/events/cron/${triggerName}/modify`);
|
||||
}}
|
||||
onDeleteSuccess={() => browserHistory.push('/events/cron')}
|
||||
/>
|
||||
</div>
|
||||
</Analytics>
|
||||
);
|
||||
|
@ -28,11 +28,7 @@ const ModifyContainer: React.FC<Props> = props => {
|
||||
allTriggers={cronTriggers}
|
||||
eventsLoading={eventsLoading}
|
||||
>
|
||||
{readOnlyMode ? (
|
||||
'Cannot modify in read-only mode'
|
||||
) : (
|
||||
<Modify dispatch={dispatch} />
|
||||
)}
|
||||
{readOnlyMode ? 'Cannot modify in read-only mode' : <Modify />}
|
||||
</STContainer>
|
||||
);
|
||||
};
|
||||
|
@ -237,3 +237,16 @@ export const getEventRequestSampleInput = (
|
||||
const value = JSON.stringify(obj, null, 2);
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getCronTriggerRequestSampleInput = () => {
|
||||
const obj = {
|
||||
comment: 'comment',
|
||||
id: '06af0430-e4d8-4335-8659-c27225e8edfd',
|
||||
name: 'name',
|
||||
payload: {},
|
||||
scheduled_time: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const value = JSON.stringify(obj, null, 2);
|
||||
return value;
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import DateTimePicker from 'react-datetime';
|
||||
import 'react-datetime/css/react-datetime.css';
|
||||
import { IconTooltip } from '@/new-components/Tooltip';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Moment } from 'moment';
|
||||
|
@ -107,9 +107,7 @@ Create.play = async ({ canvasElement }) => {
|
||||
|
||||
// --------------------------------------------------
|
||||
// Step 5: Add request transform options
|
||||
await userEvent.click(
|
||||
canvas.getByText('Header, URL, and Advanced Request Options')
|
||||
);
|
||||
await userEvent.click(canvas.getByText('Advanced Settings'));
|
||||
|
||||
// Add request headers
|
||||
await userEvent.click(canvas.getByText('Add request headers'));
|
||||
@ -146,35 +144,6 @@ Create.play = async ({ canvasElement }) => {
|
||||
'HASURA_ENV_ID'
|
||||
);
|
||||
|
||||
// Add request method and url template
|
||||
await userEvent.click(
|
||||
canvas.getByRole('radio', {
|
||||
name: 'GET',
|
||||
})
|
||||
);
|
||||
|
||||
await userEvent.type(
|
||||
canvas.getByRole('textbox', {
|
||||
name: 'Request URL Template',
|
||||
}),
|
||||
'/users'
|
||||
);
|
||||
|
||||
// Add request query params
|
||||
await userEvent.click(canvas.getByText('Add query params'));
|
||||
await userEvent.type(
|
||||
canvas.getByRole('textbox', {
|
||||
name: 'query_params[0].name',
|
||||
}),
|
||||
'userId'
|
||||
);
|
||||
await userEvent.type(
|
||||
canvas.getByRole('textbox', {
|
||||
name: 'query_params[0].value',
|
||||
}),
|
||||
'12'
|
||||
);
|
||||
|
||||
// --------------------------------------------------
|
||||
// Step 6: Update retry configuration fields
|
||||
await userEvent.click(canvas.getByText('Retry Configuration'));
|
||||
|
@ -1,16 +1,24 @@
|
||||
import React from 'react';
|
||||
import { InputField, SimpleForm } from '@/new-components/Form';
|
||||
import React, { useState } from 'react';
|
||||
import { SimpleForm, InputField } from '@/new-components/Form';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { Analytics } from '@/features/Analytics';
|
||||
import { useReadOnlyMode } from '@/hooks';
|
||||
import { getConfirmation } from '@/components/Common/utils/jsUtils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RequestTransform } from '@/metadata/types';
|
||||
import { schema, Schema } from './schema';
|
||||
import {
|
||||
AdvancedSettings,
|
||||
CronPayloadInput,
|
||||
AdvancedSettings,
|
||||
CronScheduleSelector,
|
||||
IncludeInMetadataSwitch,
|
||||
RetryConfiguration,
|
||||
} from './components';
|
||||
import { getCronTriggerCreateQuery, getCronTriggerUpdateQuery } from './utils';
|
||||
import {
|
||||
getCronTriggerCreateQuery,
|
||||
getCronTriggerDeleteQuery,
|
||||
getCronTriggerUpdateQuery,
|
||||
} from './utils';
|
||||
import { useCronMetadataMigration, useDefaultValues } from './hooks';
|
||||
import { CronRequestTransformation } from './components/CronRequestTransformation';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
@ -20,29 +28,150 @@ type Props = {
|
||||
/**
|
||||
* Success callback which can be used to apply custom logic on success, for ex. closing the form
|
||||
*/
|
||||
onSuccess?: () => void;
|
||||
onSuccess?: (name?: string) => void;
|
||||
onDeleteSuccess?: () => void;
|
||||
};
|
||||
|
||||
interface FormContentProps {
|
||||
initialTransform?: RequestTransform;
|
||||
setTransform: (data: RequestTransform) => void;
|
||||
}
|
||||
|
||||
const FormContent = (props: FormContentProps) => {
|
||||
const { setTransform, initialTransform } = props;
|
||||
const { watch } = useFormContext();
|
||||
const webhookUrl = watch('webhook');
|
||||
const payload = watch('payload');
|
||||
return (
|
||||
<>
|
||||
<div className="mb-xs w-1/2">
|
||||
<InputField
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="Name..."
|
||||
tooltip="Give this cron trigger a friendly name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-xs w-1/2">
|
||||
<InputField
|
||||
name="comment"
|
||||
label="Comment / Description"
|
||||
placeholder="Comment / Description..."
|
||||
tooltip="A statement to help describe the cron trigger in brief"
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
<div className="mb-xs w-1/2">
|
||||
<InputField
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
placeholder="https://httpbin.com/post"
|
||||
tooltip="The HTTP URL that should be triggered. You can also provide the URL from environment variables, e.g. {{MY_WEBHOOK_URL}}"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-xs w-1/2">
|
||||
<CronScheduleSelector />
|
||||
</div>
|
||||
<div className="mb-xs w-1/2">
|
||||
<CronPayloadInput />
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
|
||||
<div className="my-md w-1/2">
|
||||
<AdvancedSettings />
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
<div className="mb-xs pt-xs w-1/2">
|
||||
<CronRequestTransformation
|
||||
webhookUrl={webhookUrl}
|
||||
payload={payload}
|
||||
initialValue={initialTransform}
|
||||
onChange={setTransform}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CronTriggersForm = (props: Props) => {
|
||||
const { onSuccess, cronTriggerName } = props;
|
||||
const { onSuccess, onDeleteSuccess, cronTriggerName } = props;
|
||||
const {
|
||||
requestTransform,
|
||||
data: defaultValues,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useDefaultValues({
|
||||
cronTriggerName,
|
||||
});
|
||||
const { mutation } = useCronMetadataMigration({ onSuccess });
|
||||
|
||||
const { data: readOnlyMode } = useReadOnlyMode();
|
||||
const [transform, setTransform] = useState<RequestTransform | undefined>();
|
||||
|
||||
const { mutation: deleteMutation } = useCronMetadataMigration({
|
||||
onSuccess: onDeleteSuccess,
|
||||
successMessage: 'Cron trigger deleted successfully',
|
||||
errorMessage: 'Something went wrong while deleting cron trigger',
|
||||
});
|
||||
|
||||
const { mutation: updateMutation } = useCronMetadataMigration({
|
||||
successMessage: 'Cron trigger updated successfully',
|
||||
errorMessage: 'Something went wrong while updating cron trigger',
|
||||
});
|
||||
|
||||
const { mutation: createMutation } = useCronMetadataMigration({
|
||||
successMessage: 'Cron trigger created successfully',
|
||||
errorMessage: 'Something went wrong while creating cron trigger',
|
||||
});
|
||||
|
||||
const onDelete = () => {
|
||||
const isOk = getConfirmation(
|
||||
'Are you sure you want to delete this cron trigger?',
|
||||
true,
|
||||
cronTriggerName
|
||||
);
|
||||
if (isOk && cronTriggerName) {
|
||||
deleteMutation.mutate({
|
||||
query: getCronTriggerDeleteQuery(cronTriggerName),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (values: Record<string, unknown>) => {
|
||||
if (cronTriggerName) {
|
||||
mutation.mutate({
|
||||
query: getCronTriggerUpdateQuery(values as Schema),
|
||||
});
|
||||
const isOk =
|
||||
cronTriggerName === values?.name ||
|
||||
getConfirmation(
|
||||
'Renaming a trigger deletes the current trigger and creates a new trigger with this configuration. All the events of the current trigger will be dropped. Are you sure you want to continue?',
|
||||
true,
|
||||
'RENAME'
|
||||
);
|
||||
if (isOk) {
|
||||
updateMutation.mutate(
|
||||
{
|
||||
query: getCronTriggerUpdateQuery(
|
||||
cronTriggerName,
|
||||
values as Schema,
|
||||
transform
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSuccess?.(values?.name as string);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
mutation.mutate({
|
||||
query: getCronTriggerCreateQuery(values as Schema),
|
||||
});
|
||||
createMutation.mutate(
|
||||
{
|
||||
query: getCronTriggerCreateQuery(values as Schema, transform),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSuccess?.(values?.name as string);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -61,52 +190,57 @@ const CronTriggersForm = (props: Props) => {
|
||||
options={{ defaultValues }}
|
||||
className="overflow-y-hidden p-4"
|
||||
>
|
||||
<>
|
||||
<div className="mb-md">
|
||||
<InputField
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="Name..."
|
||||
tooltip="Give this cron trigger a friendly name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<InputField
|
||||
name="comment"
|
||||
label="Comment / Description"
|
||||
placeholder="Comment / Description..."
|
||||
tooltip="A statement to help describe the cron trigger in brief"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<InputField
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
placeholder="https://httpbin.com/post"
|
||||
tooltip="The HTTP URL that should be triggered. You can also provide the URL from environment variables, e.g. {{MY_WEBHOOK_URL}}"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<CronScheduleSelector />
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<CronPayloadInput />
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<IncludeInMetadataSwitch />
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<AdvancedSettings />
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<RetryConfiguration />
|
||||
</div>
|
||||
<div className="flex items-center mb-lg">
|
||||
<Button type="submit" mode="primary" isLoading={mutation.isLoading}>
|
||||
{cronTriggerName ? 'Update Cron Trigger' : 'Add Cron Trigger'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
<FormContent
|
||||
initialTransform={requestTransform}
|
||||
setTransform={setTransform}
|
||||
/>
|
||||
<div className="flex items-center mb-lg gap-2">
|
||||
{cronTriggerName ? (
|
||||
<Analytics
|
||||
name="events-tab-button-create-cron-trigger"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={updateMutation.isLoading}
|
||||
disabled={readOnlyMode}
|
||||
>
|
||||
Update Cron Trigger
|
||||
</Button>
|
||||
</Analytics>
|
||||
) : (
|
||||
<Analytics
|
||||
name="events-tab-button-update-cron-trigger"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={createMutation.isLoading}
|
||||
disabled={readOnlyMode}
|
||||
>
|
||||
Add Cron Trigger
|
||||
</Button>
|
||||
</Analytics>
|
||||
)}
|
||||
|
||||
{cronTriggerName && (
|
||||
<Analytics
|
||||
name="events-tab-button-delete-cron-trigger"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
onClick={onDelete}
|
||||
mode="destructive"
|
||||
isLoading={deleteMutation.isLoading}
|
||||
disabled={readOnlyMode}
|
||||
>
|
||||
Delete trigger
|
||||
</Button>
|
||||
</Analytics>
|
||||
)}
|
||||
</div>
|
||||
</SimpleForm>
|
||||
);
|
||||
};
|
||||
|
@ -1,66 +1,45 @@
|
||||
import React from 'react';
|
||||
import { IconTooltip } from '@/new-components/Tooltip';
|
||||
import { InputField, Radio } from '@/new-components/Form';
|
||||
import { Collapse } from '@/new-components/deprecated';
|
||||
import { RequestHeadersSelector } from '@/new-components/RequestHeadersSelector';
|
||||
import { IconTooltip } from '@/new-components/Tooltip';
|
||||
import { Collapsible } from '@/new-components/Collapsible';
|
||||
import { IncludeInMetadataSwitch } from './IncludeInMetadataSwitch';
|
||||
import { RetryConfiguration } from './RetryConfiguration';
|
||||
|
||||
export const AdvancedSettings = () => {
|
||||
const requestMethodOptions = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse title="Header, URL, and Advanced Request Options">
|
||||
<Collapse.Content>
|
||||
<div className="relative max-w-xl">
|
||||
<div className="mb-md">
|
||||
<label className="block flex items-center text-gray-600 font-semibold mb-xs">
|
||||
Headers
|
||||
<IconTooltip message="Configure headers for the request to the webhook" />
|
||||
</label>
|
||||
<RequestHeadersSelector
|
||||
name="headers"
|
||||
addButtonText="Add request headers"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<label className="block flex items-center text-gray-600 font-semibold">
|
||||
Method
|
||||
<IconTooltip message="Configure method to transform your request to (optional). GET requests will be sent without a payload or content-type header" />
|
||||
</label>
|
||||
<Radio
|
||||
orientation="horizontal"
|
||||
name="request_method"
|
||||
options={requestMethodOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<InputField
|
||||
name="url_template"
|
||||
prependLabel="{{$base_url}}"
|
||||
label="Request URL Template"
|
||||
placeholder="URL Template (Optional)..."
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<label className="block flex items-center text-gray-600 font-semibold mb-xs">
|
||||
Query Params
|
||||
<IconTooltip message="Configure headers for the request to the webhook" />
|
||||
</label>
|
||||
<RequestHeadersSelector
|
||||
name="query_params"
|
||||
typeSelect={false}
|
||||
addButtonText="Add query params"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse.Content>
|
||||
</Collapse>
|
||||
</>
|
||||
<div className="my-md">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<h2 className="text-lg font-semibold mb-xs flex items-center mb-0">
|
||||
Advanced Settings
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<div className="mb-xs">
|
||||
<label className="block flex items-center text-gray-600 font-semibold mb-xs">
|
||||
Headers
|
||||
<IconTooltip message="Configure headers for the request to the webhook" />
|
||||
</label>
|
||||
<RequestHeadersSelector
|
||||
name="headers"
|
||||
addButtonText="Add request headers"
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
<div className="mb-xs">
|
||||
<h4
|
||||
className="text-lg font-bold pb-md mt-0 mb-0
|
||||
"
|
||||
>
|
||||
Retry Configuration
|
||||
</h4>
|
||||
<RetryConfiguration />
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
<div className="mb-xs">
|
||||
<IncludeInMetadataSwitch />
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,289 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import ConfigureTransformation from '@/components/Common/ConfigureTransformation/ConfigureTransformation';
|
||||
import {
|
||||
getCronTriggerRequestTransformDefaultState,
|
||||
requestTransformReducer,
|
||||
setEnvVars,
|
||||
setRequestAddHeaders,
|
||||
setRequestBody,
|
||||
setRequestBodyError,
|
||||
setRequestContentType,
|
||||
setRequestMethod,
|
||||
setRequestPayloadTransform,
|
||||
setRequestQueryParams,
|
||||
setRequestSampleInput,
|
||||
setRequestTransformedBody,
|
||||
setRequestTransformState,
|
||||
setRequestUrl,
|
||||
setRequestUrlError,
|
||||
setRequestUrlPreview,
|
||||
setRequestUrlTransform,
|
||||
setSessionVars,
|
||||
} from '@/components/Common/ConfigureTransformation/requestTransformState';
|
||||
import {
|
||||
RequestTransform,
|
||||
RequestTransformContentType,
|
||||
RequestTransformMethod,
|
||||
} from '@/metadata/types';
|
||||
import {
|
||||
KeyValuePair,
|
||||
RequestTransformStateBody,
|
||||
} from '@/components/Common/ConfigureTransformation/stateDefaults';
|
||||
import {
|
||||
getRequestTransformObject,
|
||||
getTransformState,
|
||||
getValidateTransformOptions,
|
||||
parseValidateApiData,
|
||||
} from '@/components/Common/ConfigureTransformation/utils';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Endpoints from '@/Endpoints';
|
||||
import requestAction from '@/utils/requestAction';
|
||||
import { useDebouncedEffect } from '@/hooks/useDebounceEffect';
|
||||
|
||||
interface CronRequestTransformationProps {
|
||||
initialValue?: RequestTransform;
|
||||
webhookUrl: string;
|
||||
payload: string;
|
||||
onChange: (data: RequestTransform) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Using the legacy request transformation component to be consistent with other part of the console.
|
||||
* Replace with the new component once the it is ready.
|
||||
*/
|
||||
|
||||
export const CronRequestTransformation = (
|
||||
props: CronRequestTransformationProps
|
||||
) => {
|
||||
const { webhookUrl, payload, onChange, initialValue } = props;
|
||||
const webhook = {
|
||||
value: webhookUrl,
|
||||
type: 'url',
|
||||
};
|
||||
|
||||
const [transformState, transformDispatch] = useReducer(
|
||||
requestTransformReducer,
|
||||
getCronTriggerRequestTransformDefaultState()
|
||||
);
|
||||
|
||||
const resetSampleInput = () => {
|
||||
const value = {
|
||||
comment: 'test',
|
||||
id: '06af0430-e4d8-4335-8659-c27225e8edfd',
|
||||
name: 'test',
|
||||
payload: JSON.parse(payload || '{}'),
|
||||
scheduled_time: '2022-12-13T10:32:00Z',
|
||||
};
|
||||
transformDispatch(setRequestSampleInput(JSON.stringify(value, null, 2)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetSampleInput();
|
||||
if (initialValue) {
|
||||
transformDispatch(
|
||||
setRequestTransformState(
|
||||
getTransformState(initialValue, transformState.requestSampleInput)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [initialValue, transformDispatch]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const envVarsOnChange = (envVars: KeyValuePair[]) => {
|
||||
transformDispatch(setEnvVars(envVars));
|
||||
};
|
||||
|
||||
const sessionVarsOnChange = (sessionVars: KeyValuePair[]) => {
|
||||
transformDispatch(setSessionVars(sessionVars));
|
||||
};
|
||||
|
||||
const requestMethodOnChange = (requestMethod: RequestTransformMethod) => {
|
||||
transformDispatch(setRequestMethod(requestMethod));
|
||||
};
|
||||
|
||||
const requestUrlOnChange = (requestUrl: string) => {
|
||||
transformDispatch(setRequestUrl(requestUrl));
|
||||
};
|
||||
|
||||
const requestUrlErrorOnChange = (requestUrlError: string) => {
|
||||
transformDispatch(setRequestUrlError(requestUrlError));
|
||||
};
|
||||
|
||||
const requestUrlPreviewOnChange = (requestUrlPreview: string) => {
|
||||
transformDispatch(setRequestUrlPreview(requestUrlPreview));
|
||||
};
|
||||
|
||||
const requestQueryParamsOnChange = (requestQueryParams: KeyValuePair[]) => {
|
||||
transformDispatch(setRequestQueryParams(requestQueryParams));
|
||||
};
|
||||
|
||||
const requestAddHeadersOnChange = (requestAddHeaders: KeyValuePair[]) => {
|
||||
transformDispatch(setRequestAddHeaders(requestAddHeaders));
|
||||
};
|
||||
|
||||
const requestBodyOnChange = (requestBody: RequestTransformStateBody) => {
|
||||
transformDispatch(setRequestBody(requestBody));
|
||||
};
|
||||
|
||||
const requestBodyErrorOnChange = (requestBodyError: string) => {
|
||||
transformDispatch(setRequestBodyError(requestBodyError));
|
||||
};
|
||||
|
||||
const requestSampleInputOnChange = (requestSampleInput: string) => {
|
||||
transformDispatch(setRequestSampleInput(requestSampleInput));
|
||||
};
|
||||
|
||||
const requestTransformedBodyOnChange = (requestTransformedBody: string) => {
|
||||
transformDispatch(setRequestTransformedBody(requestTransformedBody));
|
||||
};
|
||||
|
||||
const requestContentTypeOnChange = (
|
||||
requestContentType: RequestTransformContentType
|
||||
) => {
|
||||
transformDispatch(setRequestContentType(requestContentType));
|
||||
};
|
||||
|
||||
const requestUrlTransformOnChange = (data: boolean) => {
|
||||
transformDispatch(setRequestUrlTransform(data));
|
||||
};
|
||||
|
||||
const requestPayloadTransformOnChange = (data: boolean) => {
|
||||
transformDispatch(setRequestPayloadTransform(data));
|
||||
};
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestUrlErrorOnChange('');
|
||||
requestUrlPreviewOnChange('');
|
||||
const onResponse = (data: Record<string, any>) => {
|
||||
parseValidateApiData(
|
||||
data,
|
||||
requestUrlErrorOnChange,
|
||||
requestUrlPreviewOnChange
|
||||
);
|
||||
};
|
||||
const options = getValidateTransformOptions({
|
||||
version: transformState.version,
|
||||
inputPayloadString: transformState.requestSampleInput,
|
||||
webhookUrl: webhook.value,
|
||||
envVarsFromContext: transformState.envVars,
|
||||
sessionVarsFromContext: transformState.sessionVars,
|
||||
requestUrl: transformState.requestUrl,
|
||||
queryParams: transformState.requestQueryParams,
|
||||
isEnvVar: webhook.type === 'env',
|
||||
});
|
||||
if (!webhook.value) {
|
||||
requestUrlErrorOnChange(
|
||||
'Please configure your webhook handler to generate request url transform'
|
||||
);
|
||||
} else if (
|
||||
transformState.requestSampleInput &&
|
||||
transformState.requestBody
|
||||
) {
|
||||
(
|
||||
dispatch(
|
||||
requestAction(
|
||||
Endpoints.metadata,
|
||||
options,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
)
|
||||
) as unknown as Promise<Record<string, unknown>>
|
||||
).then(onResponse, onResponse); // parseValidateApiData will parse both success and error
|
||||
}
|
||||
},
|
||||
|
||||
500,
|
||||
[
|
||||
transformState.requestSampleInput,
|
||||
webhookUrl,
|
||||
transformState.requestUrl,
|
||||
transformState.requestQueryParams,
|
||||
transformState.envVars,
|
||||
transformState.sessionVars,
|
||||
]
|
||||
);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
requestBodyErrorOnChange('');
|
||||
requestTransformedBodyOnChange('');
|
||||
const onResponse = (data: Record<string, any>) => {
|
||||
parseValidateApiData(
|
||||
data,
|
||||
requestBodyErrorOnChange,
|
||||
undefined,
|
||||
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.requestSampleInput &&
|
||||
transformState.requestBody
|
||||
) {
|
||||
(
|
||||
dispatch(
|
||||
requestAction(
|
||||
Endpoints.metadata,
|
||||
options,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
)
|
||||
) as unknown as Promise<Record<string, unknown>>
|
||||
).then(onResponse, onResponse); // parseValidateApiData will parse both success and error
|
||||
}
|
||||
},
|
||||
500,
|
||||
[
|
||||
transformState.requestSampleInput,
|
||||
transformState.requestBody,
|
||||
webhookUrl,
|
||||
transformState.envVars,
|
||||
transformState.sessionVars,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const data = getRequestTransformObject(transformState);
|
||||
if (data) {
|
||||
onChange(data);
|
||||
}
|
||||
}, [transformState]);
|
||||
|
||||
return (
|
||||
<ConfigureTransformation
|
||||
transformationType="event"
|
||||
requestTransfromState={transformState}
|
||||
resetSampleInput={resetSampleInput}
|
||||
envVarsOnChange={envVarsOnChange}
|
||||
sessionVarsOnChange={sessionVarsOnChange}
|
||||
requestMethodOnChange={requestMethodOnChange}
|
||||
requestUrlOnChange={requestUrlOnChange}
|
||||
requestQueryParamsOnChange={requestQueryParamsOnChange}
|
||||
requestAddHeadersOnChange={requestAddHeadersOnChange}
|
||||
requestBodyOnChange={requestBodyOnChange}
|
||||
requestSampleInputOnChange={requestSampleInputOnChange}
|
||||
requestContentTypeOnChange={requestContentTypeOnChange}
|
||||
requestUrlTransformOnChange={requestUrlTransformOnChange}
|
||||
requestPayloadTransformOnChange={requestPayloadTransformOnChange}
|
||||
/>
|
||||
);
|
||||
};
|
@ -14,7 +14,7 @@ export const CronScheduleSelector = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block flex items-center text-gray-600 font-semibold mb-xs">
|
||||
<div className="block flex items-center text-gray-600 font-semibold">
|
||||
<label htmlFor="schedule">Cron Schedule</label>
|
||||
<IconTooltip message="Schedule for your cron (events are created based on the UTC timezone)" />
|
||||
<a
|
||||
@ -26,10 +26,12 @@ export const CronScheduleSelector = () => {
|
||||
(Build a cron expression)
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative w-full max-w-xl mb-xs">
|
||||
<div className="relative w-full">
|
||||
<InputField name="schedule" type="text" placeholder={defaultCronExpr} />
|
||||
<div className="-mt-xs mb-md">
|
||||
<FrequentlyUsedCrons setCron={setCron} />
|
||||
</div>
|
||||
</div>
|
||||
<FrequentlyUsedCrons setCron={setCron} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Collapse } from '@/new-components/deprecated';
|
||||
import { IconTooltip } from '@/new-components/Tooltip';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
@ -11,59 +10,52 @@ export const RetryConfiguration = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse
|
||||
title="Retry Configuration"
|
||||
tooltip="Retry configuration if the call to the webhook fails"
|
||||
>
|
||||
<Collapse.Content>
|
||||
<div className="space-y-sm relative max-w-xl">
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Number of retries</label>
|
||||
<IconTooltip message="Number of retries that Hasura makes to the webhook in case of failure" />
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
{/* TODO: This is a horizontal/inline input field, currently we do not have it in common so this component implements its own,
|
||||
we should replace this in future with the common component */}
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="num_retries"
|
||||
{...register('num_retries')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Retry interval in seconds</label>
|
||||
<IconTooltip message="Interval (in seconds) between each retry" />
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="retry_interval_seconds"
|
||||
{...register('retry_interval_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Timeout in seconds</label>
|
||||
<IconTooltip message="Request timeout for the webhook" />
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="timeout_seconds"
|
||||
{...register('timeout_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-sm relative max-w-xl">
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Number of retries</label>
|
||||
<IconTooltip message="Number of retries that Hasura makes to the webhook in case of failure" />
|
||||
</div>
|
||||
</Collapse.Content>
|
||||
</Collapse>
|
||||
<div className="col-span-6">
|
||||
{/* TODO: This is a horizontal/inline input field, currently we do not have it in common so this component implements its own,
|
||||
we should replace this in future with the common component */}
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="num_retries"
|
||||
{...register('num_retries')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Retry interval in seconds</label>
|
||||
<IconTooltip message="Interval (in seconds) between each retry" />
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="retry_interval_seconds"
|
||||
{...register('retry_interval_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6 flex items-center">
|
||||
<label className="block">Timeout in seconds</label>
|
||||
<IconTooltip message="Request timeout for the webhook" />
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<input
|
||||
type="number"
|
||||
className={inputStyes}
|
||||
aria-label="timeout_seconds"
|
||||
{...register('timeout_seconds')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,29 +1,43 @@
|
||||
import { useMetadataMigration } from '@/features/MetadataAPI';
|
||||
import { useFireNotification } from '@/new-components/Notifications';
|
||||
import { APIError } from '@/hooks/error';
|
||||
import { CRON_TRIGGERS_QUERY_KEY } from '@/components/Services/Events/CronTriggers/Hooks/useGetCronTriggers';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { ALL_CRON_TRIGGERS_QUERY_KEY } from './useGetAllCronTriggers';
|
||||
|
||||
interface Props {
|
||||
onSuccess?: () => void;
|
||||
triggerName?: string;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export const useCronMetadataMigration = (props: Props) => {
|
||||
const { onSuccess } = props;
|
||||
const { onSuccess, successMessage, errorMessage } = props;
|
||||
const { fireNotification } = useFireNotification();
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMetadataMigration({
|
||||
onError: (error: APIError) => {
|
||||
fireNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: error?.message ?? 'Unable to create Cron Trigger',
|
||||
message: `${errorMessage}: ${error.message}`,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
fireNotification({
|
||||
type: 'success',
|
||||
title: 'Success!',
|
||||
message: 'Cron Trigger created successfully',
|
||||
message: successMessage,
|
||||
});
|
||||
|
||||
/* this query is performed in legacy section, remove it will completely be refactored */
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [CRON_TRIGGERS_QUERY_KEY],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ALL_CRON_TRIGGERS_QUERY_KEY],
|
||||
});
|
||||
onSuccess?.();
|
||||
},
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { RequestTransform } from '@/metadata/types';
|
||||
import { Schema } from '../schema';
|
||||
import {
|
||||
emptyDefaultValues,
|
||||
serverHeadersToKeyValueArray,
|
||||
serverQueryParamsToKeyValueArray,
|
||||
stringifyNumberValue,
|
||||
} from './utils';
|
||||
import { useGetAllCronTriggers } from './useGetAllCronTriggers';
|
||||
@ -32,7 +32,9 @@ export const useDefaultValues = (props: Props) => {
|
||||
return { data: emptyDefaultValues, isLoading, isError: true };
|
||||
}
|
||||
|
||||
const existingCronTriggerValues: Schema = {
|
||||
const existingCronTriggerValues: Schema & {
|
||||
requestTransform?: RequestTransform;
|
||||
} = {
|
||||
name: currentTrigger.name,
|
||||
webhook: currentTrigger.webhook,
|
||||
schedule: currentTrigger.schedule,
|
||||
@ -52,15 +54,19 @@ export const useDefaultValues = (props: Props) => {
|
||||
),
|
||||
include_in_metadata: currentTrigger.include_in_metadata,
|
||||
comment: currentTrigger.comment ?? '',
|
||||
url_template: currentTrigger.request_transform?.url ?? '',
|
||||
request_method: currentTrigger.request_transform?.method ?? null,
|
||||
query_params: serverQueryParamsToKeyValueArray(
|
||||
currentTrigger.request_transform?.query_params
|
||||
),
|
||||
};
|
||||
return { data: existingCronTriggerValues, isLoading, isError };
|
||||
return {
|
||||
data: existingCronTriggerValues,
|
||||
isLoading,
|
||||
isError,
|
||||
requestTransform: currentTrigger?.request_transform,
|
||||
};
|
||||
}
|
||||
|
||||
// return error and loading states with empty default values
|
||||
return { data: emptyDefaultValues, isLoading, isError };
|
||||
return {
|
||||
data: emptyDefaultValues,
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ interface GetCronTriggersResponse {
|
||||
cron_triggers: CronTrigger[];
|
||||
}
|
||||
|
||||
export const ALL_CRON_TRIGGERS_QUERY_KEY = 'allCronTriggers';
|
||||
export const useGetAllCronTriggers = () => {
|
||||
const headers = useAppSelector(state => state.tables.dataHeaders);
|
||||
|
||||
@ -17,7 +18,7 @@ export const useGetAllCronTriggers = () => {
|
||||
args: {},
|
||||
};
|
||||
|
||||
return useQuery(['allCronTriggers'], () =>
|
||||
return useQuery([ALL_CRON_TRIGGERS_QUERY_KEY], () =>
|
||||
Api.post<GetCronTriggersResponse>({
|
||||
headers,
|
||||
body,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Nullable } from '@/components/Common/utils/tsUtils';
|
||||
import { ServerHeader } from '@/metadata/types';
|
||||
import { Schema } from '../schema';
|
||||
|
||||
@ -24,18 +23,6 @@ export const serverHeadersToKeyValueArray = (
|
||||
return [];
|
||||
};
|
||||
|
||||
export const serverQueryParamsToKeyValueArray = (
|
||||
serverObject: Nullable<Record<string, string>>
|
||||
): Schema['query_params'] => {
|
||||
if (serverObject) {
|
||||
return Object.entries(serverObject).map(([key, value]) => ({
|
||||
name: key,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const stringifyNumberValue = (
|
||||
value: number | undefined,
|
||||
defaultValue: string
|
||||
@ -57,7 +44,4 @@ export const emptyDefaultValues: Schema = {
|
||||
timeout_seconds: '60',
|
||||
include_in_metadata: true,
|
||||
comment: '',
|
||||
url_template: '',
|
||||
request_method: null,
|
||||
query_params: [],
|
||||
};
|
||||
|
@ -15,9 +15,6 @@ export const schema = z.object({
|
||||
timeout_seconds: z.string(),
|
||||
include_in_metadata: z.boolean(),
|
||||
comment: z.string(),
|
||||
url_template: z.string(),
|
||||
request_method: z.enum(['POST', 'GET', 'PUT', 'DELETE', 'PATCH']).nullable(),
|
||||
query_params: requestHeadersSelectorSchema,
|
||||
});
|
||||
|
||||
export type Schema = z.infer<typeof schema>;
|
||||
|
@ -1,60 +1,18 @@
|
||||
import { isEmpty, isJsonString } from '@/components/Common/utils/jsUtils';
|
||||
import { isJsonString } from '@/components/Common/utils/jsUtils';
|
||||
import { CronTrigger, RequestTransform } from '@/metadata/types';
|
||||
import { Schema } from './schema';
|
||||
|
||||
type RequestTransformBlockProps = {
|
||||
url_template: Schema['url_template'];
|
||||
request_method: Schema['request_method'];
|
||||
query_params: Schema['query_params'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get request transform / rest connectors field of the cron trigger object, this field is added optionally
|
||||
* if at least one of the transform fields are present
|
||||
*/
|
||||
const getRequestTransformBlock = (
|
||||
props: RequestTransformBlockProps
|
||||
): RequestTransform | undefined => {
|
||||
const { url_template, request_method, query_params } = props;
|
||||
|
||||
// add transform block if atleast one of the field is present
|
||||
if (isEmpty(url_template) && isEmpty(query_params) && isEmpty(request_method))
|
||||
return;
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
template_engine: 'Kriti',
|
||||
method: request_method,
|
||||
url: url_template,
|
||||
query_params: query_params.reduce(
|
||||
(allParams, param) => ({
|
||||
...allParams,
|
||||
[param.name]: param.value,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
// in case of GET requests, do not send request body or content-type header to the webhook
|
||||
// https://github.com/hasura/graphql-engine/issues/7937
|
||||
...(request_method === 'GET' && {
|
||||
request_headers: {
|
||||
remove_headers: ['content-type'],
|
||||
},
|
||||
body: {
|
||||
action: 'remove',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms form data to `create_cron_trigger` api payload
|
||||
* @param values React hook form schema containing all form fields
|
||||
* @returns api payload for `create_cron_trigger`
|
||||
*/
|
||||
const transformFormData = (values: Schema) => {
|
||||
const { url_template, request_method, query_params } = values;
|
||||
|
||||
const apiPayload: CronTrigger = {
|
||||
const transformFormData = (
|
||||
values: Schema,
|
||||
replace: boolean,
|
||||
requestTransform?: RequestTransform
|
||||
) => {
|
||||
const apiPayload: CronTrigger & { replace?: true } = {
|
||||
name: values.name,
|
||||
webhook: values.webhook,
|
||||
schedule: values.schedule,
|
||||
@ -73,18 +31,19 @@ const transformFormData = (values: Schema) => {
|
||||
},
|
||||
include_in_metadata: values.include_in_metadata,
|
||||
comment: values.comment,
|
||||
request_transform: getRequestTransformBlock({
|
||||
url_template,
|
||||
request_method,
|
||||
query_params,
|
||||
}),
|
||||
request_transform: requestTransform,
|
||||
...(replace && { replace: true }),
|
||||
};
|
||||
|
||||
return apiPayload;
|
||||
};
|
||||
|
||||
export const getCronTriggerCreateQuery = (values: Schema) => {
|
||||
const args = transformFormData(values);
|
||||
export const getCronTriggerCreateQuery = (
|
||||
values: Schema,
|
||||
requestTransform?: RequestTransform,
|
||||
replace = false
|
||||
) => {
|
||||
const args = transformFormData(values, replace, requestTransform);
|
||||
return {
|
||||
type: 'create_cron_trigger' as const,
|
||||
args,
|
||||
@ -98,10 +57,17 @@ export const getCronTriggerDeleteQuery = (name: string) => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const getCronTriggerUpdateQuery = (values: Schema) => ({
|
||||
export const getCronTriggerUpdateQuery = (
|
||||
name: string,
|
||||
values: Schema,
|
||||
requestTransform?: RequestTransform
|
||||
) => ({
|
||||
type: 'bulk' as const,
|
||||
args: [
|
||||
getCronTriggerDeleteQuery(values.name),
|
||||
getCronTriggerCreateQuery(values),
|
||||
],
|
||||
args:
|
||||
name === values.name
|
||||
? [getCronTriggerCreateQuery(values, requestTransform, true)]
|
||||
: [
|
||||
getCronTriggerDeleteQuery(name),
|
||||
getCronTriggerCreateQuery(values, requestTransform),
|
||||
],
|
||||
});
|
||||
|
@ -33,6 +33,8 @@ The following fields are required to define a cron trigger:
|
||||
timezone. You can use tools like [crontab guru](https://crontab.guru/#*_*_*_*_*) to help build a cron expression.
|
||||
- **Payload**: The JSON payload which will be sent to the webhook.
|
||||
|
||||
You can also define a **comment** that helps you identify the cron trigger.
|
||||
|
||||
For example, we can create a cron trigger called `eod_reports`, to trigger the webhook `https://mywebhook.com/eod` with
|
||||
the cron schedule `0 22 * * 1-5`, which means "At 22:00 on every day-of-week from Monday through Friday" (you can check
|
||||
this [here](https://crontab.guru/#0_22_*_*_1-5)).
|
||||
@ -104,7 +106,6 @@ If you like, you can also define the following values:
|
||||
- **Retry configuration**: In case the call to the webhook fails.
|
||||
- **Include in metadata**: When set to true, the cron trigger will be included in the metadata and can be exported along
|
||||
with it.
|
||||
- **Comment**: Custom description of the cron trigger.
|
||||
|
||||
<Tabs groupId="user-preference" className="api-tabs">
|
||||
<TabItem value="console" label="Console">
|
||||
@ -191,5 +192,5 @@ webhooks without needing any middleware or modifications to the upstream code.
|
||||
REST Connectors modify the cron trigger's HTTP request to adapt to your webhook's expected format by adding suitable
|
||||
transforms.
|
||||
|
||||
Currently, rest connectors for cron triggers can only be configured through the CLI and the
|
||||
[Metadata API](/api-reference/metadata-api/scheduled-triggers.mdx#metadata-create-cron-trigger).
|
||||
Rest Connectors for cron triggers can be configured in the same way as rest connectors for event triggers. You can read more about it
|
||||
in the [REST Connectors](/docs/latest/event-triggers/rest-connectors/) section.
|
||||
|
BIN
docs/static/img/event-triggers/advanced-cron.png
vendored
BIN
docs/static/img/event-triggers/advanced-cron.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 46 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 52 KiB |
Loading…
Reference in New Issue
Block a user