mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): implement webhook basic details page (#3820)
This commit is contained in:
parent
b91b0f3835
commit
6c3a5a6899
11 changed files with 460 additions and 69 deletions
|
@ -8,6 +8,10 @@ export enum ConnectorsTabs {
|
||||||
Social = 'social',
|
Social = 'social',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WebhookDetailsTabs {
|
||||||
|
Settings = 'settings',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SignInExperiencePage {
|
export enum SignInExperiencePage {
|
||||||
BrandingTab = 'branding',
|
BrandingTab = 'branding',
|
||||||
SignUpAndSignInTab = 'sign-up-and-sign-in',
|
SignUpAndSignInTab = 'sign-up-and-sign-in',
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ConnectorsTabs,
|
ConnectorsTabs,
|
||||||
UserDetailsTabs,
|
UserDetailsTabs,
|
||||||
RoleDetailsTabs,
|
RoleDetailsTabs,
|
||||||
|
WebhookDetailsTabs,
|
||||||
} from '@/consts';
|
} from '@/consts';
|
||||||
import { isHookFeatureEnabled } from '@/consts/webhooks';
|
import { isHookFeatureEnabled } from '@/consts/webhooks';
|
||||||
import ApiResourceDetails from '@/pages/ApiResourceDetails';
|
import ApiResourceDetails from '@/pages/ApiResourceDetails';
|
||||||
|
@ -38,6 +39,8 @@ import UserLogs from '@/pages/UserDetails/UserLogs';
|
||||||
import UserRoles from '@/pages/UserDetails/UserRoles';
|
import UserRoles from '@/pages/UserDetails/UserRoles';
|
||||||
import UserSettings from '@/pages/UserDetails/UserSettings';
|
import UserSettings from '@/pages/UserDetails/UserSettings';
|
||||||
import Users from '@/pages/Users';
|
import Users from '@/pages/Users';
|
||||||
|
import WebhookDetails from '@/pages/WebhookDetails';
|
||||||
|
import WebhookSettings from '@/pages/WebhookDetails/WebhookSettings';
|
||||||
import Webhooks from '@/pages/Webhooks';
|
import Webhooks from '@/pages/Webhooks';
|
||||||
|
|
||||||
import type { AppContentOutletContext } from '../AppContent/types';
|
import type { AppContentOutletContext } from '../AppContent/types';
|
||||||
|
@ -90,6 +93,10 @@ function ConsoleContent() {
|
||||||
<Route path="webhooks">
|
<Route path="webhooks">
|
||||||
<Route index element={<Webhooks />} />
|
<Route index element={<Webhooks />} />
|
||||||
<Route path="create" element={<Webhooks />} />
|
<Route path="create" element={<Webhooks />} />
|
||||||
|
<Route path=":id" element={<WebhookDetails />}>
|
||||||
|
<Route index element={<Navigate replace to={WebhookDetailsTabs.Settings} />} />
|
||||||
|
<Route path={WebhookDetailsTabs.Settings} element={<WebhookSettings />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
<Route path="users">
|
<Route path="users">
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { type Hook } from '@logto/schemas';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
|
import DetailsForm from '@/components/DetailsForm';
|
||||||
|
import FormCard from '@/components/FormCard';
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
import BasicWebhookForm from '@/pages/Webhooks/components/BasicWebhookForm';
|
||||||
|
|
||||||
|
import { type WebhookDetailsFormType, type WebhookDetailsOutletContext } from '../types';
|
||||||
|
import { webhookDetailsParser } from '../utils';
|
||||||
|
|
||||||
|
function WebhookSettings() {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const { hook, isDeleting, onHookUpdated } = useOutletContext<WebhookDetailsOutletContext>();
|
||||||
|
|
||||||
|
const webhookFormData = webhookDetailsParser.toLocalForm(hook);
|
||||||
|
const formMethods = useForm<WebhookDetailsFormType>({ defaultValues: webhookFormData });
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = formMethods;
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (formData) => {
|
||||||
|
const updatedHook = await api
|
||||||
|
.patch(`api/hooks/${hook.id}`, { json: webhookDetailsParser.toRemoteModel(formData) })
|
||||||
|
.json<Hook>();
|
||||||
|
reset(webhookDetailsParser.toLocalForm(updatedHook));
|
||||||
|
onHookUpdated(updatedHook);
|
||||||
|
toast.success(t('general.saved'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailsForm
|
||||||
|
isDirty={isDirty}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onDiscard={reset}
|
||||||
|
>
|
||||||
|
<FormCard
|
||||||
|
title="webhook_details.settings.settings"
|
||||||
|
description="webhook_details.settings.settings_description"
|
||||||
|
>
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
<BasicWebhookForm />
|
||||||
|
</FormProvider>
|
||||||
|
</FormCard>
|
||||||
|
</DetailsForm>
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebhookSettings;
|
50
packages/console/src/pages/WebhookDetails/index.module.scss
Normal file
50
packages/console/src/pages/WebhookDetails/index.module.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: _.unit(6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.webhookIcon {
|
||||||
|
margin-left: _.unit(2);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-left: _.unit(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: _.unit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: var(--font-title-1);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font: var(--font-label-2);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verticalBar {
|
||||||
|
@include _.vertical-bar;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
198
packages/console/src/pages/WebhookDetails/index.tsx
Normal file
198
packages/console/src/pages/WebhookDetails/index.tsx
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import { type Hook } from '@logto/schemas';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Delete from '@/assets/images/delete.svg';
|
||||||
|
import Forbidden from '@/assets/images/forbidden.svg';
|
||||||
|
import More from '@/assets/images/more.svg';
|
||||||
|
import Shield from '@/assets/images/shield.svg';
|
||||||
|
import WebhookDark from '@/assets/images/webhook-dark.svg';
|
||||||
|
import Webhook from '@/assets/images/webhook.svg';
|
||||||
|
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import ConfirmModal from '@/components/ConfirmModal';
|
||||||
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
|
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
|
||||||
|
import DetailsPage from '@/components/DetailsPage';
|
||||||
|
import DynamicT from '@/components/DynamicT';
|
||||||
|
import PageMeta from '@/components/PageMeta';
|
||||||
|
import Status from '@/components/Status';
|
||||||
|
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||||
|
import { WebhookDetailsTabs } from '@/consts';
|
||||||
|
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||||
|
import useTheme from '@/hooks/use-theme';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
import { type WebhookDetailsOutletContext } from './types';
|
||||||
|
|
||||||
|
function WebhookDetails() {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data, error, mutate } = useSWR<Hook, RequestError>(id && `api/hooks/${id}`);
|
||||||
|
const isLoading = !data && !error;
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const WebhookIcon = theme === 'light' ? Webhook : WebhookDark;
|
||||||
|
|
||||||
|
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isDisableFormOpen, setIsDisableFormOpen] = useState(false);
|
||||||
|
const [isUpdatingEnableState, setIsUpdatingEnableState] = useState(false);
|
||||||
|
const [isEnabled, setIsEnabled] = useState(data?.enabled ?? true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDeleteFormOpen(false);
|
||||||
|
setIsDisableFormOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
const onDelete = async () => {
|
||||||
|
if (!data || isDeleting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`api/hooks/${data.id}`);
|
||||||
|
toast.success(t('webhook_details.deleted', { name: data.name }));
|
||||||
|
navigate('/webhooks');
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleEnableState = async () => {
|
||||||
|
if (!data || isUpdatingEnableState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdatingEnableState(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { enabled } = await api
|
||||||
|
.patch(`api/hooks/${data.id}`, { json: { enabled: !isEnabled } })
|
||||||
|
.json<Hook>();
|
||||||
|
setIsEnabled(enabled);
|
||||||
|
setIsDisableFormOpen(false);
|
||||||
|
toast.success(
|
||||||
|
t(enabled ? 'webhook_details.webhook_reactivated' : 'webhook_details.webhook_disabled')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsUpdatingEnableState(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailsPage
|
||||||
|
backLink="/webhooks"
|
||||||
|
backLinkTitle="webhook_details.back_to_webhooks"
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
onRetry={mutate}
|
||||||
|
>
|
||||||
|
<PageMeta titleKey="webhook_details.page_title" />
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<Card className={styles.header}>
|
||||||
|
<WebhookIcon className={styles.webhookIcon} />
|
||||||
|
<div className={styles.metadata}>
|
||||||
|
<div className={styles.title}>{data.name}</div>
|
||||||
|
<div>
|
||||||
|
{isEnabled ? (
|
||||||
|
<div>Success Rate (WIP)</div>
|
||||||
|
) : (
|
||||||
|
<Status status="disabled" variant="outlined">
|
||||||
|
<DynamicT forKey="webhook_details.not_in_use" />
|
||||||
|
</Status>
|
||||||
|
)}
|
||||||
|
<div className={styles.verticalBar} />
|
||||||
|
<div className={styles.text}>ID</div>
|
||||||
|
<CopyToClipboard size="small" value={data.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ActionMenu
|
||||||
|
buttonProps={{ icon: <More className={styles.icon} />, size: 'large' }}
|
||||||
|
title={t('general.more_options')}
|
||||||
|
>
|
||||||
|
<ActionMenuItem
|
||||||
|
icon={isEnabled ? <Forbidden /> : <Shield />}
|
||||||
|
iconClassName={styles.icon}
|
||||||
|
onClick={async () => {
|
||||||
|
if (isEnabled) {
|
||||||
|
setIsDisableFormOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onToggleEnableState();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicT
|
||||||
|
forKey={
|
||||||
|
isEnabled
|
||||||
|
? 'webhook_details.disable_webhook'
|
||||||
|
: 'webhook_details.reactivate_webhook'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ActionMenuItem>
|
||||||
|
<ActionMenuItem
|
||||||
|
icon={<Delete />}
|
||||||
|
type="danger"
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteFormOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicT forKey="webhook_details.delete_webhook" />
|
||||||
|
</ActionMenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
<DeleteConfirmModal
|
||||||
|
isOpen={isDeleteFormOpen}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsDeleteFormOpen(true);
|
||||||
|
}}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
>
|
||||||
|
<div>{t('webhook_details.deletion_reminder')}</div>
|
||||||
|
</DeleteConfirmModal>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDisableFormOpen}
|
||||||
|
isLoading={isUpdatingEnableState}
|
||||||
|
confirmButtonText="webhook_details.disable_webhook"
|
||||||
|
onCancel={async () => {
|
||||||
|
setIsDisableFormOpen(false);
|
||||||
|
}}
|
||||||
|
onConfirm={onToggleEnableState}
|
||||||
|
>
|
||||||
|
<DynamicT forKey="webhook_details.disable_reminder" />
|
||||||
|
</ConfirmModal>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<TabNav>
|
||||||
|
<TabNavItem href={`/webhooks/${data.id}/${WebhookDetailsTabs.Settings}`}>
|
||||||
|
<DynamicT forKey="webhook_details.settings_tab" />
|
||||||
|
</TabNavItem>
|
||||||
|
</TabNav>
|
||||||
|
<Outlet
|
||||||
|
context={
|
||||||
|
{
|
||||||
|
hook: data,
|
||||||
|
isDeleting,
|
||||||
|
onHookUpdated: (hook) => {
|
||||||
|
void mutate(hook);
|
||||||
|
},
|
||||||
|
} satisfies WebhookDetailsOutletContext
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DetailsPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebhookDetails;
|
11
packages/console/src/pages/WebhookDetails/types.ts
Normal file
11
packages/console/src/pages/WebhookDetails/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { type HookConfig, type Hook } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type BasicWebhookFormType } from '../Webhooks/types';
|
||||||
|
|
||||||
|
export type WebhookDetailsOutletContext = {
|
||||||
|
hook: Hook;
|
||||||
|
isDeleting: boolean;
|
||||||
|
onHookUpdated: (hook?: Hook) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebhookDetailsFormType = BasicWebhookFormType & { headers: HookConfig['headers'] };
|
34
packages/console/src/pages/WebhookDetails/utils.ts
Normal file
34
packages/console/src/pages/WebhookDetails/utils.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { type Hook } from '@logto/schemas';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
import { type WebhookDetailsFormType } from './types';
|
||||||
|
|
||||||
|
export const webhookDetailsParser = {
|
||||||
|
toLocalForm: (data: Hook): WebhookDetailsFormType => {
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
events,
|
||||||
|
name,
|
||||||
|
config: { url, headers },
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: conditional(events.length > 0 && events) ?? (event ? [event] : []),
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
toRemoteModel: (formData: WebhookDetailsFormType): Partial<Hook> => {
|
||||||
|
const { name, events, url, headers } = formData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
events,
|
||||||
|
config: {
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { HookEvent } from '@logto/schemas';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { CheckboxGroup } from '@/components/Checkbox';
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import TextInput from '@/components/TextInput';
|
||||||
|
import { hookEventLabel } from '@/consts/webhooks';
|
||||||
|
import { uriValidator } from '@/utils/validator';
|
||||||
|
|
||||||
|
import { type BasicWebhookFormType } from '../../types';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export const hookEventOptions = Object.values(HookEvent).map((event) => ({
|
||||||
|
title: hookEventLabel[event],
|
||||||
|
value: event,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function BasicWebhookForm() {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<BasicWebhookFormType>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField title="webhooks.create_form.events">
|
||||||
|
<div className={styles.formFieldDescription}>
|
||||||
|
{t('webhooks.create_form.events_description')}
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="events"
|
||||||
|
control={control}
|
||||||
|
defaultValue={[]}
|
||||||
|
rules={{
|
||||||
|
validate: (value) =>
|
||||||
|
value.length === 0 ? t('webhooks.create_form.missing_event_error') : true,
|
||||||
|
}}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CheckboxGroup options={hookEventOptions} value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.events && <div className={styles.errorMessage}>{errors.events.message}</div>}
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired title="webhooks.create_form.name">
|
||||||
|
<TextInput
|
||||||
|
{...register('name', { required: true })}
|
||||||
|
placeholder={t('webhooks.create_form.name_placeholder')}
|
||||||
|
error={Boolean(errors.name)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField
|
||||||
|
isRequired
|
||||||
|
title="webhooks.create_form.endpoint_url"
|
||||||
|
tip={t('webhooks.create_form.endpoint_url_tip')}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
{...register('url', {
|
||||||
|
required: true,
|
||||||
|
validate: (value) => {
|
||||||
|
if (!uriValidator(value)) {
|
||||||
|
return t('errors.invalid_uri_format');
|
||||||
|
}
|
||||||
|
return value.startsWith('https://') || t('webhooks.create_form.https_format_error');
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
placeholder={t('webhooks.create_form.endpoint_url_placeholder')}
|
||||||
|
error={errors.url?.type === 'required' ? true : errors.url?.message}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BasicWebhookForm;
|
|
@ -1,27 +1,17 @@
|
||||||
import { type Hook, type CreateHook, HookEvent, type HookConfig } from '@logto/schemas';
|
import { type Hook, type CreateHook, type HookEvent, type HookConfig } from '@logto/schemas';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { CheckboxGroup } from '@/components/Checkbox';
|
|
||||||
import FormField from '@/components/FormField';
|
|
||||||
import ModalLayout from '@/components/ModalLayout';
|
import ModalLayout from '@/components/ModalLayout';
|
||||||
import TextInput from '@/components/TextInput';
|
|
||||||
import { hookEventLabel } from '@/consts/webhooks';
|
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import { uriValidator } from '@/utils/validator';
|
|
||||||
|
|
||||||
import * as styles from './CreateForm.module.scss';
|
import { type BasicWebhookFormType } from '../../types';
|
||||||
|
import BasicWebhookForm from '../BasicWebhookForm';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: (createdHook?: Hook) => void;
|
onClose: (createdHook?: Hook) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormData = Pick<CreateHook, 'name'> & {
|
|
||||||
events: HookEvent[];
|
|
||||||
url: HookConfig['url'];
|
|
||||||
};
|
|
||||||
|
|
||||||
type CreateHookPayload = Pick<CreateHook, 'name'> & {
|
type CreateHookPayload = Pick<CreateHook, 'name'> & {
|
||||||
events: HookEvent[];
|
events: HookEvent[];
|
||||||
config: {
|
config: {
|
||||||
|
@ -29,20 +19,12 @@ type CreateHookPayload = Pick<CreateHook, 'name'> & {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const hookEventOptions = Object.values(HookEvent).map((event) => ({
|
|
||||||
title: hookEventLabel[event],
|
|
||||||
value: event,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function CreateForm({ onClose }: Props) {
|
function CreateForm({ onClose }: Props) {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const formMethods = useForm<BasicWebhookFormType>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
formState: { isSubmitting },
|
||||||
control,
|
} = formMethods;
|
||||||
formState: { isSubmitting, errors },
|
|
||||||
} = useForm<FormData>();
|
|
||||||
|
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
|
@ -76,50 +58,9 @@ function CreateForm({ onClose }: Props) {
|
||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<FormField title="webhooks.create_form.events">
|
<FormProvider {...formMethods}>
|
||||||
<div className={styles.formFieldDescription}>
|
<BasicWebhookForm />
|
||||||
{t('webhooks.create_form.events_description')}
|
</FormProvider>
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="events"
|
|
||||||
control={control}
|
|
||||||
defaultValue={[]}
|
|
||||||
rules={{
|
|
||||||
validate: (value) =>
|
|
||||||
value.length === 0 ? t('webhooks.create_form.missing_event_error') : true,
|
|
||||||
}}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CheckboxGroup options={hookEventOptions} value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.events && <div className={styles.errorMessage}>{errors.events.message}</div>}
|
|
||||||
</FormField>
|
|
||||||
<FormField isRequired title="webhooks.create_form.name">
|
|
||||||
<TextInput
|
|
||||||
{...register('name', { required: true })}
|
|
||||||
placeholder={t('webhooks.create_form.name_placeholder')}
|
|
||||||
error={Boolean(errors.name)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField
|
|
||||||
isRequired
|
|
||||||
title="webhooks.create_form.endpoint_url"
|
|
||||||
tip={t('webhooks.create_form.endpoint_url_tip')}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
{...register('url', {
|
|
||||||
required: true,
|
|
||||||
validate: (value) => {
|
|
||||||
if (!uriValidator(value)) {
|
|
||||||
return t('errors.invalid_uri_format');
|
|
||||||
}
|
|
||||||
return value.startsWith('https://') || t('webhooks.create_form.https_format_error');
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
placeholder={t('webhooks.create_form.endpoint_url_placeholder')}
|
|
||||||
error={errors.url?.type === 'required' ? true : errors.url?.message}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</ModalLayout>
|
</ModalLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
7
packages/console/src/pages/Webhooks/types.ts
Normal file
7
packages/console/src/pages/Webhooks/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas';
|
||||||
|
|
||||||
|
export type BasicWebhookFormType = {
|
||||||
|
name: Hook['name'];
|
||||||
|
events: HookEvent[];
|
||||||
|
url: HookConfig['url'];
|
||||||
|
};
|
Loading…
Reference in a new issue