0
Fork 0
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:
Xiao Yijun 2023-05-12 11:58:50 +08:00 committed by GitHub
parent 6c3a5a6899
commit 124e0bca0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 217 additions and 5 deletions

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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>

View file

@ -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[] };

View file

@ -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,
},
};
},

View file

@ -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:

View file

@ -37,6 +37,9 @@ const webhook_details = {
custom_headers: 'Custom headers',
custom_headers_tip:
'Optionally, you can add custom headers to the webhooks 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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -36,6 +36,9 @@ const webhook_details = {
custom_headers: 'Özel başlıklar',
custom_headers_tip:
'İsteğin bir parçası olarak webhookun 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: 'Webhookunuzu test edin',
test_webhook_description:

View file

@ -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:

View file

@ -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:

View file

@ -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: