0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): display webhook execution stats (#3883)

This commit is contained in:
Xiao Yijun 2023-05-24 08:31:26 +08:00 committed by GitHub
parent 166c6f7da0
commit 86dee64576
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 109 additions and 32 deletions

View file

@ -26,10 +26,6 @@
> div {
display: flex;
align-items: center;
> *:not(:last-child) {
margin-right: _.unit(2);
}
}
.title {
@ -40,11 +36,23 @@
.text {
font: var(--font-label-2);
color: var(--color-text-secondary);
margin-right: _.unit(2);
}
.verticalBar {
@include _.vertical-bar;
height: 12px;
margin: 0 _.unit(2);
}
.state {
display: flex;
align-items: center;
font: var(--font-body-2);
.successRate {
margin-right: _.unit(1);
}
}
}
}

View file

@ -1,5 +1,5 @@
import { withAppInsights } from '@logto/app-insights/react';
import { type Hook } from '@logto/schemas';
import { type HookResponse, type Hook } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
@ -26,6 +26,9 @@ import Tag from '@/components/Tag';
import { WebhookDetailsTabs } from '@/consts';
import useApi, { type RequestError } from '@/hooks/use-api';
import useTheme from '@/hooks/use-theme';
import { buildUrl } from '@/utils/url';
import SuccessRate from '../Webhooks/components/SuccessRate';
import * as styles from './index.module.scss';
import { type WebhookDetailsOutletContext } from './types';
@ -36,7 +39,9 @@ function WebhookDetails() {
const isPageHasTable = pathname.endsWith(WebhookDetailsTabs.RecentRequests);
const navigate = useNavigate();
const { id } = useParams();
const { data, error, mutate } = useSWR<Hook, RequestError>(id && `api/hooks/${id}`);
const { data, error, mutate } = useSWR<HookResponse, RequestError>(
id && buildUrl(`api/hooks/${id}`, { includeExecutionStats: String(true) })
);
const isLoading = !data && !error;
const api = useApi();
@ -52,7 +57,7 @@ function WebhookDetails() {
useEffect(() => {
setIsDeleteFormOpen(false);
setIsDisableFormOpen(false);
}, [pathname]);
}, [data?.enabled, pathname]);
const onDelete = async () => {
if (!data || isDeleting) {
@ -109,7 +114,19 @@ function WebhookDetails() {
<div className={styles.title}>{data.name}</div>
<div>
{isEnabled ? (
<div>Success Rate (WIP)</div>
<div className={styles.state}>
<SuccessRate
className={styles.successRate}
successCount={data.executionStats.successCount}
totalCount={data.executionStats.requestCount}
/>
<DynamicT forKey="webhook_details.success_rate" />
<div className={styles.verticalBar} />
<DynamicT
forKey="webhook_details.requests"
interpolation={{ value: data.executionStats.requestCount }}
/>
</div>
) : (
<Tag type="state" status="info">
<DynamicT forKey="webhook_details.not_in_use" />
@ -191,7 +208,12 @@ function WebhookDetails() {
hook: data,
isDeleting,
onHookUpdated: (hook) => {
void mutate(hook);
if (hook) {
const { executionStats } = data;
void mutate({ ...hook, executionStats });
return;
}
void mutate();
},
} satisfies WebhookDetailsOutletContext
}

View file

@ -1,9 +1,9 @@
import { type Hook } from '@logto/schemas';
import { type HookResponse, type Hook } from '@logto/schemas';
import { type BasicWebhookFormType } from '../Webhooks/types';
export type WebhookDetailsOutletContext = {
hook: Hook;
hook: HookResponse;
isDeleting: boolean;
onHookUpdated: (hook?: Hook) => void;
};

View file

@ -0,0 +1,24 @@
import Tag from '@/components/Tag';
type Props = {
successCount: number;
totalCount: number;
className?: string;
};
function SuccessRate({ successCount, totalCount, className }: Props) {
if (totalCount === 0) {
return <div>-</div>;
}
const percent = (successCount / totalCount) * 100;
const statusStyle = percent < 90 ? 'error' : percent < 99 ? 'alert' : 'success';
return (
<Tag variant="plain" type="state" status={statusStyle} className={className}>
{percent.toFixed(2)}%
</Tag>
);
}
export default SuccessRate;

View file

@ -1,5 +1,13 @@
@use '@/scss/underscore' as _;
.icon {
width: 40px;
height: 40px;
flex-shrink: 0;
}
.requests {
font: var(--font-body-2);
text-align: right;
margin-right: _.unit(4);
}

View file

@ -1,5 +1,5 @@
import { withAppInsights } from '@logto/app-insights/react';
import { type HookEvent, type Hook, Theme } from '@logto/schemas';
import { type HookEvent, type Hook, Theme, type HookResponse } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -16,11 +16,14 @@ import DynamicT from '@/components/DynamicT';
import ItemPreview from '@/components/ItemPreview';
import ListPage from '@/components/ListPage';
import TablePlaceholder from '@/components/Table/TablePlaceholder';
import Tag from '@/components/Tag';
import { hookEventLabel } from '@/consts/webhooks';
import { type RequestError } from '@/hooks/use-api';
import useTheme from '@/hooks/use-theme';
import { buildUrl } from '@/utils/url';
import CreateFormModal from './components/CreateFormModal';
import SuccessRate from './components/SuccessRate';
import * as styles from './index.module.scss';
const webhooksPathname = '/webhooks';
@ -32,7 +35,9 @@ function Webhooks() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { pathname } = useLocation();
const isCreateNew = pathname === createWebhookPathname;
const { data, error, mutate } = useSWR<Hook[], RequestError>('api/hooks');
const { data, error, mutate } = useSWR<HookResponse[], RequestError>(
buildUrl('api/hooks', { includeExecutionStats: String(true) })
);
const isLoading = !data && !error;
const navigate = useNavigate();
const theme = useTheme();
@ -87,16 +92,26 @@ function Webhooks() {
title: <DynamicT forKey="webhooks.table.success_rate" />,
dataIndex: 'successRate',
colSpan: 3,
render: () => {
return <div>WIP</div>;
render: ({ enabled, executionStats: { successCount, requestCount } }) => {
return enabled ? (
<SuccessRate successCount={successCount} totalCount={requestCount} />
) : (
<Tag type="state" status="info" variant="plain">
<DynamicT forKey="webhook_details.not_in_use" />
</Tag>
);
},
},
{
title: <DynamicT forKey="webhooks.table.requests" />,
dataIndex: 'Requests',
colSpan: 2,
render: () => {
return <div>WIP</div>;
render: ({ enabled, executionStats: { requestCount } }) => {
return (
<div className={styles.requests}>
{enabled ? requestCount.toLocaleString() : '-'}
</div>
);
},
},
],

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook-Details',
back_to_webhooks: 'Zurück zu Webhooks',
not_in_use: 'Nicht in Nutzung',
success_rate: '{{value, number}} Erfolgsrate',
success_rate: 'Erfolgsrate',
requests: '{{value, number}} Requests in 24 Stunden',
disable_webhook: 'Webhook deaktivieren',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook details',
back_to_webhooks: 'Back to Webhooks',
not_in_use: 'Not in use',
success_rate: '{{value, number}} success rate',
success_rate: 'success rate',
requests: '{{value, number}} requests in 24h',
disable_webhook: 'Disable webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Detalles de Webhook',
back_to_webhooks: 'Volver a Webhooks',
not_in_use: 'No se está usando',
success_rate: 'Tasa de éxito: {{value, number}}',
success_rate: 'Tasa de éxito',
requests: 'Solicitudes en 24h: {{value, number}}',
disable_webhook: 'Desactivar webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Détails du webhook',
back_to_webhooks: 'Retour aux webhooks',
not_in_use: "Pas en cours d'utilisation",
success_rate: 'Taux de réussite de {{value, number}}',
success_rate: 'Taux de réussite',
requests: '{{value, number}} requêtes en 24h',
disable_webhook: 'Désactiver le webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Dettagli webhook',
back_to_webhooks: 'Torna ai Webhook',
not_in_use: 'Non in uso',
success_rate: 'Tasso di successo {{value,number}}',
success_rate: 'Tasso di successo',
requests: '{{value, number}} richieste in 24h',
disable_webhook: 'Disabilita webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhookの詳細',
back_to_webhooks: 'Webhooksに戻る',
not_in_use: '使用されていない',
success_rate: '{{value, number}}成功率',
success_rate: '成功率',
requests: '24時間に{{value, number}}件のリクエスト',
disable_webhook: 'Webhookを無効にする',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook 담당자',
back_to_webhooks: 'Webhooks로 돌아가기',
not_in_use: '사용 중이 아님',
success_rate: '{{value, number}} 성공율',
success_rate: '성공율',
requests: '24시간 동안 {{value, number}}개의 요청',
disable_webhook: 'Webhook 비활성화',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Szczegóły webhooka',
back_to_webhooks: 'Wróć do Webhooks',
not_in_use: 'Nieaktywne',
success_rate: 'Stosunek sukcesów: {{value, number}}',
success_rate: 'Stosunek sukcesów',
requests: '{{value, number}} żądań w ciągu 24h',
disable_webhook: 'Wyłącz webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Detalhes do webhook',
back_to_webhooks: 'Voltar para Webhooks',
not_in_use: 'Não em uso',
success_rate: 'Taxa de sucesso de {{value, number}}',
success_rate: 'Taxa de sucesso',
requests: '{{value, number}} requisições em 24h',
disable_webhook: 'Desativar webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Detalhes do webhook',
back_to_webhooks: 'Voltar para Webhooks',
not_in_use: 'Não está em uso',
success_rate: 'Taxa de sucesso de {{value, number}}',
success_rate: 'Taxa de sucesso',
requests: '{{value, number}} solicitações em 24h',
disable_webhook: 'Desativar o webhook',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Данные вебхука',
back_to_webhooks: 'Вернуться к вебхукам',
not_in_use: 'Не используется',
success_rate: 'Коэффициент успешности {{value, number}}',
success_rate: 'Коэффициент успешности',
requests: '{{value, number}} запросов за 24 часа',
disable_webhook: 'Отключить вебхук',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook ayrıntıları',
back_to_webhooks: 'Webhooklara geri dön',
not_in_use: 'Kullanılmıyor',
success_rate: '{{value, number}} başarı oranı',
success_rate: 'başarı oranı',
requests: '24 saatte {{value, number}} istek',
disable_webhook: 'Webhooku devre dışı bırak',
disable_reminder:

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook 详情',
back_to_webhooks: '返回 Webhooks',
not_in_use: '未使用',
success_rate: '{{value, number}} 成功率',
success_rate: '成功率',
requests: '24 小时内请求次数:{{value, number}}',
disable_webhook: '禁用 Webhook',
disable_reminder: '是否确定重新激活此 Webhook重新激活后将不会向端点 URL 发送 HTTP 请求。',

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook 詳細資料',
back_to_webhooks: '返回 Webhooks',
not_in_use: '未啟用',
success_rate: '{{value, number}} 成功率',
success_rate: '成功率',
requests: '24 小時內收到 {{value, number}} 個請求',
disable_webhook: '停用 webhook',
disable_reminder: '確定要重新啟用此 webhook重新啟用後不會對端點 URL 發送 HTTP 請求。',

View file

@ -2,7 +2,7 @@ const webhook_details = {
page_title: 'Webhook 詳情',
back_to_webhooks: '返回 Webhooks',
not_in_use: '未使用',
success_rate: '{{value, number}} 成功率',
success_rate: '成功率',
requests: '24 小時內 {{value, number}} 個請求',
disable_webhook: '停用 Webhook',
disable_reminder: '確定要重新啟用此 Webhook 嗎?繼續操作後,將不會向端點 URL 發送 HTTP 請求。',