mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
feat(console): support editing webhook custom headers in the console (#3823)
This commit is contained in:
parent
6c3a5a6899
commit
124e0bca0d
20 changed files with 217 additions and 5 deletions
|
@ -0,0 +1,25 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.field {
|
||||
margin-bottom: _.unit(3);
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
gap: _.unit(2);
|
||||
align-items: center;
|
||||
|
||||
.keyInput {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.valueInput {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-error);
|
||||
margin-top: _.unit(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CirclePlus from '@/assets/images/circle-plus.svg';
|
||||
import Minus from '@/assets/images/minus.svg';
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import { type WebhookDetailsFormType } from '@/pages/WebhookDetails/types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function CustomHeaderField() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: {
|
||||
errors: { headers: headerErrors },
|
||||
submitCount,
|
||||
},
|
||||
} = useFormContext<WebhookDetailsFormType>();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
control,
|
||||
name: 'headers',
|
||||
});
|
||||
|
||||
const keyValidator = (key: string, index: number) => {
|
||||
const headers = getValues('headers');
|
||||
if (!headers) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (headers.filter(({ key: _key }) => _key === key).length > 1) {
|
||||
return t('webhook_details.settings.key_duplicated_error');
|
||||
}
|
||||
|
||||
const correspondValue = getValues(`headers.${index}.value`);
|
||||
if (correspondValue) {
|
||||
return Boolean(key) || t('webhook_details.settings.key_missing_error');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const revalidate = () => {
|
||||
for (const [index, _] of fields.entries()) {
|
||||
void trigger(`headers.${index}.key`);
|
||||
if (submitCount > 0) {
|
||||
void trigger(`headers.${index}.value`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField
|
||||
title="webhook_details.settings.custom_headers"
|
||||
tip={t('webhook_details.settings.custom_headers_tip')}
|
||||
>
|
||||
{fields.map((header, index) => {
|
||||
return (
|
||||
<div key={header.id} className={styles.field}>
|
||||
<div className={styles.input}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
placeholder="Key"
|
||||
error={Boolean(headerErrors?.[index]?.key)}
|
||||
{...register(`headers.${index}.key`, {
|
||||
validate: (key) => keyValidator(key, index),
|
||||
onChange: revalidate,
|
||||
})}
|
||||
/>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
placeholder="Value"
|
||||
error={Boolean(headerErrors?.[index]?.value)}
|
||||
{...register(`headers.${index}.value`, {
|
||||
validate: (value) =>
|
||||
getValues(`headers.${index}.key`)
|
||||
? Boolean(value) || t('webhook_details.settings.value_missing_error')
|
||||
: true,
|
||||
onChange: revalidate,
|
||||
})}
|
||||
/>
|
||||
{fields.length > 1 && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{headerErrors?.[index]?.key?.message && (
|
||||
<div className={styles.error}>{headerErrors[index]?.key?.message}</div>
|
||||
)}
|
||||
{headerErrors?.[index]?.value?.message && (
|
||||
<div className={styles.error}>{headerErrors[index]?.value?.message}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
title="general.add_another"
|
||||
icon={<CirclePlus />}
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomHeaderField;
|
|
@ -13,12 +13,16 @@ import BasicWebhookForm from '@/pages/Webhooks/components/BasicWebhookForm';
|
|||
import { type WebhookDetailsFormType, type WebhookDetailsOutletContext } from '../types';
|
||||
import { webhookDetailsParser } from '../utils';
|
||||
|
||||
import CustomHeaderField from './components/CustomHeaderField';
|
||||
|
||||
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 formMethods = useForm<WebhookDetailsFormType>({
|
||||
defaultValues: webhookFormData,
|
||||
});
|
||||
const api = useApi();
|
||||
|
||||
const {
|
||||
|
@ -50,6 +54,7 @@ function WebhookSettings() {
|
|||
>
|
||||
<FormProvider {...formMethods}>
|
||||
<BasicWebhookForm />
|
||||
<CustomHeaderField />
|
||||
</FormProvider>
|
||||
</FormCard>
|
||||
</DetailsForm>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type HookConfig, type Hook } from '@logto/schemas';
|
||||
import { type Hook } from '@logto/schemas';
|
||||
|
||||
import { type BasicWebhookFormType } from '../Webhooks/types';
|
||||
|
||||
|
@ -8,4 +8,9 @@ export type WebhookDetailsOutletContext = {
|
|||
onHookUpdated: (hook?: Hook) => void;
|
||||
};
|
||||
|
||||
export type WebhookDetailsFormType = BasicWebhookFormType & { headers: HookConfig['headers'] };
|
||||
type HeaderField = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type WebhookDetailsFormType = BasicWebhookFormType & { headers?: HeaderField[] };
|
||||
|
|
|
@ -12,22 +12,33 @@ export const webhookDetailsParser = {
|
|||
config: { url, headers },
|
||||
} = data;
|
||||
|
||||
const headerFields = conditional(
|
||||
headers && Object.entries(headers).map(([key, value]) => ({ key, value }))
|
||||
);
|
||||
|
||||
return {
|
||||
events: conditional(events.length > 0 && events) ?? (event ? [event] : []),
|
||||
name,
|
||||
url,
|
||||
headers,
|
||||
headers: headerFields?.length ? headerFields : [{ key: '', value: '' }],
|
||||
};
|
||||
},
|
||||
toRemoteModel: (formData: WebhookDetailsFormType): Partial<Hook> => {
|
||||
const { name, events, url, headers } = formData;
|
||||
|
||||
const headersObject = conditional(
|
||||
headers &&
|
||||
Object.fromEntries(
|
||||
headers.filter(({ key, value }) => key && value).map(({ key, value }) => [key, value])
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
events,
|
||||
config: {
|
||||
url,
|
||||
headers,
|
||||
headers: headersObject,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -38,6 +38,9 @@ const webhook_details = {
|
|||
custom_headers: 'Benutzerdefinierte Header',
|
||||
custom_headers_tip:
|
||||
'Sie können optional benutzerdefinierte Header zur Nutzlast des Webhooks hinzufügen, um zusätzlichen Kontext oder Metadaten zum Ereignis bereitzustellen.',
|
||||
key_duplicated_error: 'Schlüssel können nicht wiederholt werden.',
|
||||
key_missing_error: 'Key ist erforderlich.',
|
||||
value_missing_error: 'Eine Eingabe ist erforderlich.',
|
||||
test: 'Test',
|
||||
test_webhook: 'Testen Sie Ihren Webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Custom headers',
|
||||
custom_headers_tip:
|
||||
'Optionally, you can add custom headers to the webhook’s payload to provide additional context or metadata about the event.',
|
||||
key_duplicated_error: 'Key cannot be repeated.',
|
||||
key_missing_error: 'Key is required.',
|
||||
value_missing_error: 'Value is required.',
|
||||
test: 'Test',
|
||||
test_webhook: 'Test your webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -38,6 +38,9 @@ const webhook_details = {
|
|||
custom_headers: 'Encabezados Personalizados',
|
||||
custom_headers_tip:
|
||||
'De manera opcional, puede agregar encabezados personalizados al payload del webhook para proporcionar más contexto o metadatos sobre el evento.',
|
||||
key_duplicated_error: 'Las claves no pueden repetirse.',
|
||||
key_missing_error: 'Se requiere clave.',
|
||||
value_missing_error: 'Se requiere valor.',
|
||||
test: 'Prueba',
|
||||
test_webhook: 'Probar su webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -38,6 +38,9 @@ const webhook_details = {
|
|||
custom_headers: 'En-têtes personnalisés',
|
||||
custom_headers_tip:
|
||||
"Optionnellement, vous pouvez ajouter des en-têtes personnalisés à la charge utile du webhook pour fournir un contexte ou des métadonnées supplémentaires sur l'événement.",
|
||||
key_duplicated_error: 'Les clés ne peuvent pas se répéter.',
|
||||
key_missing_error: 'La clé est requise.',
|
||||
value_missing_error: 'La valeur est requise.',
|
||||
test: 'Tester',
|
||||
test_webhook: 'Tester votre webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Intestazioni personalizzate',
|
||||
custom_headers_tip:
|
||||
"Opzionalmente, puoi aggiungere intestazioni personalizzate al payload del webhook per fornire contesto o metadati aggiuntivi sull'evento.",
|
||||
key_duplicated_error: 'Le chiavi non possono essere ripetute.',
|
||||
key_missing_error: 'La chiave è necessaria.',
|
||||
value_missing_error: 'Il valore è obbligatorio.',
|
||||
test: 'Test',
|
||||
test_webhook: 'Testa il tuo webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'カスタムヘッダー',
|
||||
custom_headers_tip:
|
||||
'オプションで、Webhookのペイロードに追加のコンテキストまたはメタデータを提供するために、カスタムヘッダーを追加できます。',
|
||||
key_duplicated_error: 'キーは繰り返すことはできません。',
|
||||
key_missing_error: 'キーは必須です。',
|
||||
value_missing_error: '値が必要です。',
|
||||
test: 'テスト',
|
||||
test_webhook: 'Webhookをテストする',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -36,6 +36,9 @@ const webhook_details = {
|
|||
custom_headers: '사용자 지정 헤더',
|
||||
custom_headers_tip:
|
||||
'이벤트에 대한 컨텍스트 또는 메타데이터를 제공하기 위해 webhook 페이로드에 사용자 지정 헤더를 추가할 수 있습니다.',
|
||||
key_duplicated_error: '키는 반복될 수 없습니다.',
|
||||
key_missing_error: '키는 필수 값입니다.',
|
||||
value_missing_error: '값은 필수 값입니다.',
|
||||
test: '테스트',
|
||||
test_webhook: 'Webhook 테스트',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Niestandardowe nagłówki',
|
||||
custom_headers_tip:
|
||||
'Opcjonalnie możesz dodać niestandardowe nagłówki w ładunku webhooka, aby dostarczyć dodatkowy kontekst lub metadane na temat zdarzenia.',
|
||||
key_duplicated_error: 'Klucze nie mogą się powtarzać.',
|
||||
key_missing_error: 'Klucz jest wymagany.',
|
||||
value_missing_error: 'Wartość jest wymagana.',
|
||||
test: 'Test',
|
||||
test_webhook: 'Wypróbuj swój webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Cabeçalhos personalizados',
|
||||
custom_headers_tip:
|
||||
'Opcionalmente, você pode adicionar cabeçalhos personalizados ao payload do webhook para fornecer contexto ou metadados adicionais sobre o evento.',
|
||||
key_duplicated_error: 'As chaves não podem ser repetidas.',
|
||||
key_missing_error: 'A chave é obrigatória.',
|
||||
value_missing_error: 'O valor é obrigatório.',
|
||||
test: 'Testar',
|
||||
test_webhook: 'Teste seu webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Cabeçalhos personalizados',
|
||||
custom_headers_tip:
|
||||
'Opcionalmente, você pode adicionar cabeçalhos personalizados ao corpo do webhook para fornecer contexto ou metadados adicionais sobre o evento.',
|
||||
key_duplicated_error: 'As chaves não podem ser repetidas.',
|
||||
key_missing_error: 'Key é obrigatório.',
|
||||
value_missing_error: 'O valor é obrigatório.',
|
||||
test: 'Teste',
|
||||
test_webhook: 'Teste seu webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -37,6 +37,9 @@ const webhook_details = {
|
|||
custom_headers: 'Пользовательские заголовки',
|
||||
custom_headers_tip:
|
||||
'При желании вы можете добавлять пользовательские заголовки к нагрузке вебхука, чтобы предоставить дополнительный контекст или метаданные о событии.',
|
||||
key_duplicated_error: 'Ключи не могут повторяться.',
|
||||
key_missing_error: 'Ключ обязателен.',
|
||||
value_missing_error: 'Значение обязательно',
|
||||
test: 'Тестирование',
|
||||
test_webhook: 'Протестировать ваш вебхук',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -36,6 +36,9 @@ const webhook_details = {
|
|||
custom_headers: 'Özel başlıklar',
|
||||
custom_headers_tip:
|
||||
'İsteğin bir parçası olarak webhook’un yüküne isteğin bağlamı veya meta verileri sağlamak için isteğe bağlı olarak özel başlıklar ekleyebilirsiniz.',
|
||||
key_duplicated_error: 'Anahtarlar tekrarlanamaz.',
|
||||
key_missing_error: 'Anahtar gereklidir.',
|
||||
value_missing_error: 'Değer gereklidir.',
|
||||
test: 'Test',
|
||||
test_webhook: 'Webhook’unuzu test edin',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -34,6 +34,9 @@ const webhook_details = {
|
|||
custom_headers: '自定义标头',
|
||||
custom_headers_tip:
|
||||
'选择性地,您可以向 Webhook 负载添加自定义标头,以提供事件的其他上下文或元数据。',
|
||||
key_duplicated_error: 'Key 不能重复。',
|
||||
key_missing_error: '必须填写 Key。',
|
||||
value_missing_error: '必须填写值。',
|
||||
test: '测试',
|
||||
test_webhook: '测试您的 Webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -34,6 +34,9 @@ const webhook_details = {
|
|||
custom_headers: '自定義標頭',
|
||||
custom_headers_tip:
|
||||
'您可以選擇添加自定義標頭到 webhook 的負載,提供有關事件的更多上下文或元數據。',
|
||||
key_duplicated_error: 'Key 不能重複。',
|
||||
key_missing_error: '必須填寫 Key。',
|
||||
value_missing_error: '未填寫值。',
|
||||
test: '測試',
|
||||
test_webhook: '測試您的 webhook',
|
||||
test_webhook_description:
|
||||
|
|
|
@ -34,6 +34,9 @@ const webhook_details = {
|
|||
custom_headers: '自定義標頭',
|
||||
custom_headers_tip:
|
||||
'您可以選擇將自定義標頭添加到有效載荷中,以提供有關事件的其他上下文或元數據。',
|
||||
key_duplicated_error: 'Key 不能重複。',
|
||||
key_missing_error: '必須填寫 Key。',
|
||||
value_missing_error: '未填寫值。',
|
||||
test: '測試',
|
||||
test_webhook: '測試您的 Webhook',
|
||||
test_webhook_description:
|
||||
|
|
Loading…
Add table
Reference in a new issue