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:
parent
166c6f7da0
commit
86dee64576
21 changed files with 109 additions and 32 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 请求。',
|
||||
|
|
|
@ -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 請求。',
|
||||
|
|
|
@ -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 請求。',
|
||||
|
|
Loading…
Reference in a new issue