Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(console, phrases): update the supported webhook events #5856

Merged
merged 11 commits into from
May 15, 2024
12 changes: 12 additions & 0 deletions .changeset/curvy-boxes-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@logto/console": patch
"@logto/phrases": patch
---

replace the i18n translated hook event label with the hook event value directly in the console

- remove all the legacy interaction hook events i18n phrases
- replace the translated label with the hook event value directly in the console
- `Create new account` -> `PostRegister`
- `Sign in` -> `PostSignIn`
- `Reset password` -> `PostResetPassword`
simeng-li marked this conversation as resolved.
Show resolved Hide resolved
71 changes: 45 additions & 26 deletions packages/console/src/components/BasicWebhookForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import { type HookEvent, type Hook, type HookConfig, InteractionHookEvent } from '@logto/schemas';
import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { hookEventLabel } from '@/consts/webhooks';
import { CheckboxGroup } from '@/ds-components/Checkbox';
import { isDevFeaturesEnabled } from '@/consts/env';
import {
dataHookEventsLabel,
interactionHookEvents,
schemaGroupedDataHookEvents,
} from '@/consts/webhooks';
import CategorizedCheckboxGroup, {
type CheckboxOptionGroup,
} from '@/ds-components/Checkbox/CategorizedCheckboxGroup';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import { uriValidator } from '@/utils/validator';

import * as styles from './index.module.scss';

// TODO: Implement all hook events
const hookEventOptions = Object.values(InteractionHookEvent).map((event) => ({
title: hookEventLabel[event],
value: event,
}));
const hookEventGroups: Array<CheckboxOptionGroup<HookEvent>> = [
// TODO: Remove dev feature guard

Check warning on line 21 in packages/console/src/components/BasicWebhookForm/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/console/src/components/BasicWebhookForm/index.tsx#L21

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Remove dev feature guard'.
...(isDevFeaturesEnabled
? schemaGroupedDataHookEvents.map(([schema, events]) => ({
title: dataHookEventsLabel[schema],
options: events.map((event) => ({
value: event,
})),
}))
: []),
{
title: 'webhooks.schemas.interaction',
options: interactionHookEvents.map((event) => ({
value: event,
})),
},
];

export type BasicWebhookFormType = {
name: Hook['name'];
Expand All @@ -32,24 +51,6 @@

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 })}
Expand All @@ -71,6 +72,24 @@
error={errors.url?.type === 'required' ? true : errors.url?.message}
/>
</FormField>
<FormField
title="webhooks.create_form.events"
tip={t('webhooks.create_form.events_description')}
>
<Controller
name="events"
control={control}
defaultValue={[]}
rules={{
validate: (value) =>
value.length === 0 ? t('webhooks.create_form.missing_event_error') : true,
}}
render={({ field: { onChange, value } }) => (
<CategorizedCheckboxGroup value={value} groups={hookEventGroups} onChange={onChange} />
)}
/>
{errors.events && <div className={styles.errorMessage}>{errors.events.message}</div>}
</FormField>
</>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/console/src/consts/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { yes } from '@silverhand/essentials';
const isProduction = process.env.NODE_ENV === 'production';
export const isCloud = yes(process.env.IS_CLOUD);
export const adminEndpoint = process.env.ADMIN_ENDPOINT;
// eslint-disable-next-line import/no-unused-modules

export const isDevFeaturesEnabled =
!isProduction || yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST);
70 changes: 52 additions & 18 deletions packages/console/src/consts/webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { InteractionHookEvent, type LogKey } from '@logto/schemas';
import {
DataHookSchema,
InteractionHookEvent,
hookEvents,
type DataHookEvent,
} from '@logto/schemas';

type HookEventLabel = {
// TODO: Implement all hook events
[key in InteractionHookEvent]: AdminConsoleKey;
};
export const dataHookEventsLabel = Object.freeze({
[DataHookSchema.User]: 'webhooks.schemas.user',
[DataHookSchema.Organization]: 'webhooks.schemas.organization',
[DataHookSchema.Role]: 'webhooks.schemas.role',
[DataHookSchema.Scope]: 'webhooks.schemas.scope',
[DataHookSchema.OrganizationRole]: 'webhooks.schemas.organization_role',
[DataHookSchema.OrganizationScope]: 'webhooks.schemas.organization_scope',
} satisfies Record<DataHookSchema, AdminConsoleKey>);

export const interactionHookEvents = Object.values(InteractionHookEvent);

const dataHookEvents: DataHookEvent[] = hookEvents.filter(
// eslint-disable-next-line no-restricted-syntax
(event): event is DataHookEvent => !interactionHookEvents.includes(event as InteractionHookEvent)
);

const isDataHookSchema = (schema: string): schema is DataHookSchema =>
// eslint-disable-next-line no-restricted-syntax
Object.values(DataHookSchema).includes(schema as DataHookSchema);

// Group DataHook events by schema
// TODO: Replace this using `groupBy` once Node v22 goes LTS

Check warning on line 30 in packages/console/src/consts/webhooks.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/console/src/consts/webhooks.ts#L30

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Replace this using `groupBy` once...'.
const schemaGroupedDataHookEventsMap = dataHookEvents.reduce<Map<DataHookSchema, DataHookEvent[]>>(
(eventGroup, event) => {
const [schema] = event.split('.');

if (schema && isDataHookSchema(schema)) {
eventGroup.set(schema, [...(eventGroup.get(schema) ?? []), event]);
}

export const hookEventLabel = Object.freeze({
[InteractionHookEvent.PostRegister]: 'webhooks.events.post_register',
[InteractionHookEvent.PostResetPassword]: 'webhooks.events.post_reset_password',
[InteractionHookEvent.PostSignIn]: 'webhooks.events.post_sign_in',
}) satisfies HookEventLabel;
return eventGroup;
},
new Map()
);

type HookEventLogKey = {
// TODO: Implement all hook events
[key in InteractionHookEvent]: LogKey;
// Sort the grouped `DataHook` events per console product design
const hookEventSchemaOrder: {
simeng-li marked this conversation as resolved.
Show resolved Hide resolved
[key in DataHookSchema]: number;
} = {
[DataHookSchema.User]: 0,
[DataHookSchema.Organization]: 1,
[DataHookSchema.Role]: 2,
[DataHookSchema.OrganizationRole]: 3,
[DataHookSchema.Scope]: 4,
[DataHookSchema.OrganizationScope]: 5,
};

export const hookEventLogKey = Object.freeze({
[InteractionHookEvent.PostRegister]: 'TriggerHook.PostRegister',
[InteractionHookEvent.PostResetPassword]: 'TriggerHook.PostResetPassword',
[InteractionHookEvent.PostSignIn]: 'TriggerHook.PostSignIn',
}) satisfies HookEventLogKey;
export const schemaGroupedDataHookEvents = Array.from(schemaGroupedDataHookEventsMap.entries())
.slice()
.sort(([schemaA], [schemaB]) => hookEventSchemaOrder[schemaA] - hookEventSchemaOrder[schemaB]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use '@/scss/underscore' as _;

.groupTitle {
font: var(--font-body-2);
color: var(--color-text-secondary);
margin-bottom: _.unit(2);
}

.groupList {
// Max two columns
gap: _.unit(5);
display: grid;
grid-template-columns: repeat(2, 1fr);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type AdminConsoleKey } from '@logto/phrases';
import classNames from 'classnames';

import DynamicT from '@/ds-components/DynamicT';

import CheckboxGroup, { type Option } from '../CheckboxGroup';

import * as styles from './index.module.scss';

export type CheckboxOptionGroup<T> = {
title: AdminConsoleKey;
options: Array<Option<T>>;
};

type Props<T> = {
readonly groups: Array<CheckboxOptionGroup<T>>;
readonly value: T[];
readonly onChange: (value: T[]) => void;
readonly className?: string;
};

function CategorizedCheckboxGroup<T extends string>({
groups,
value: checkedValues,
onChange,
className,
}: Props<T>) {
return (
<div className={classNames(styles.groupList, className)}>
{groups.map(({ title, options }) => (
<div key={title}>
<div className={styles.groupTitle}>
<DynamicT forKey={title} />
</div>
<CheckboxGroup options={options} value={checkedValues} onChange={onChange} />
</div>
))}
</div>
);
}

export default CategorizedCheckboxGroup;
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import Checkbox from '../Checkbox';

import * as styles from './index.module.scss';

type Option<T> = {
title: AdminConsoleKey;
export type Option<T> = {
title?: AdminConsoleKey;
tag?: ReactNode;
value: T;
};
Expand Down Expand Up @@ -42,7 +42,7 @@ function CheckboxGroup<T extends string>({
key={value}
label={
<>
<DynamicT forKey={title} />
{title ? <DynamicT forKey={title} /> : value}
{tag}
</>
}
Expand Down
11 changes: 5 additions & 6 deletions packages/console/src/pages/AuditLogDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Application, User, Log, Hook } from '@logto/schemas';
/* eslint-disable complexity */
import type { Application, Hook, Log, User } from '@logto/schemas';
import { demoAppApplicationId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
Expand All @@ -10,13 +11,13 @@ import DetailsPage from '@/components/DetailsPage';
import PageMeta from '@/components/PageMeta';
import UserName from '@/components/UserName';
import { logEventTitle } from '@/consts/logs';
import { hookEventLogKey } from '@/consts/webhooks';
import Card from '@/ds-components/Card';
import CodeEditor from '@/ds-components/CodeEditor';
import DangerousRaw from '@/ds-components/DangerousRaw';
import FormField from '@/ds-components/FormField';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import type { RequestError } from '@/hooks/use-api';
import { isWebhookEventLogKey } from '@/pages/WebhookDetails/utils';
import { getUserTitle } from '@/utils/user';

import EventIcon from './components/EventIcon';
Expand All @@ -28,9 +29,6 @@ const getAuditLogDetailsRelatedResourceLink = (pathname: string) =>
const getDetailsTabNavLink = (logId: string, userId?: string) =>
userId ? `/users/${userId}/logs/${logId}` : `/audit-logs/${logId}`;

const isWebhookEventLog = (key?: string) =>
key && Object.values<string>(hookEventLogKey).includes(key);

function AuditLogDetails() {
const { appId, userId, hookId, logId } = useParams();
const { pathname } = useLocation();
Expand Down Expand Up @@ -70,7 +68,7 @@ function AuditLogDetails() {
return null;
}

const isWebHookEvent = isWebhookEventLog(data?.key);
const isWebHookEvent = isWebhookEventLogKey(data?.key ?? '');

return (
<DetailsPage
Expand Down Expand Up @@ -161,3 +159,4 @@ function AuditLogDetails() {
}

export default AuditLogDetails;
/* eslint-enable complexity */
25 changes: 11 additions & 14 deletions packages/console/src/pages/WebhookDetails/WebhookLogs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Log, InteractionHookEvent } from '@logto/schemas';
import { hookEvents, type Log } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
Expand All @@ -8,8 +8,8 @@
import EventSelector from '@/components/AuditLogTable/components/EventSelector';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import { defaultPageSize } from '@/consts';
import { hookEventLabel, hookEventLogKey } from '@/consts/webhooks';
import DynamicT from '@/ds-components/DynamicT';
import { isDevFeaturesEnabled } from '@/consts/env';
import { interactionHookEvents } from '@/consts/webhooks';
import Table from '@/ds-components/Table';
import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
Expand All @@ -18,13 +18,16 @@
import { buildUrl } from '@/utils/url';

import { type WebhookDetailsOutletContext } from '../types';
import { buildHookEventLogKey, getHookEventKey } from '../utils';

import * as styles from './index.module.scss';

// TODO: Implement all hook events
const hookLogEventOptions = Object.values(InteractionHookEvent).map((event) => ({
title: <DynamicT forKey={hookEventLabel[event]} />,
value: hookEventLogKey[event],
// TODO: Remove dev feature guard

Check warning on line 25 in packages/console/src/pages/WebhookDetails/WebhookLogs/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/console/src/pages/WebhookDetails/WebhookLogs/index.tsx#L25

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Remove dev feature guard'.
const webhookEvents = isDevFeaturesEnabled ? hookEvents : interactionHookEvents;

const hookLogEventOptions = webhookEvents.map((event) => ({
title: event,
value: buildHookEventLogKey(event),
}));

function WebhookLogs() {
Expand Down Expand Up @@ -96,13 +99,7 @@
title: t('logs.event'),
dataIndex: 'event',
colSpan: 6,
render: ({ key }) => {
// TODO: Implement all hook events
const event = Object.values(InteractionHookEvent).find(
(event) => hookEventLogKey[event] === key
);
return conditional(event && t(hookEventLabel[event])) ?? '-';
},
render: ({ key }) => getHookEventKey(key),
},
{
title: t('logs.time'),
Expand Down
19 changes: 18 additions & 1 deletion packages/console/src/pages/WebhookDetails/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Hook } from '@logto/schemas';
import { hookEvents, type Hook, type HookEvent, type WebhookLogKey } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';

import { type WebhookDetailsFormType } from './types';
Expand Down Expand Up @@ -47,3 +47,20 @@ export const webhookDetailsParser = {
};
},
};

export const buildHookEventLogKey = (event: HookEvent): WebhookLogKey => `TriggerHook.${event}`;

export const isWebhookEventLogKey = (logKey: string): logKey is WebhookLogKey => {
const [prefix, ...events] = logKey.split('.');

// eslint-disable-next-line no-restricted-syntax
return prefix === 'TriggerHook' && hookEvents.includes(events.join('.') as HookEvent);
};

export const getHookEventKey = (logKey: string) => {
if (!isWebhookEventLogKey(logKey)) {
return ' - ';
}

return logKey.replace('TriggerHook.', '');
};
Loading
Loading