diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9409323..f8552aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: specifier: ^1.4.13 version: 1.4.13(@antv/l7@2.20.14)(react-dom@18.2.0)(react@18.2.0) '@i18next-toolkit/react': - specifier: ^1.0.4 - version: 1.0.4(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.0.5 + version: 1.0.5(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) '@loadable/component': specifier: ^5.16.3 version: 5.16.3(react@18.2.0) @@ -5913,8 +5913,8 @@ packages: - buffer dev: true - /@i18next-toolkit/react@1.0.4(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ZhVIgath7x++jeBe83XRZReHp2VP6tLapVY3zRM0oWnUDaewcmIdKsemorzojqbw0jLorP8gKq4UNR4tMoiVTQ==} + /@i18next-toolkit/react@1.0.5(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Hc34LF6oT602YG/1orQhZhUyZcAr5pznx0oUPQ+5frI0Hg8eE2ahpY0G7XxDZ7+0D0Bcr3E5n2uNEdzg8IDm5A==} peerDependencies: '@types/react': ^18.2.55 react: ^18.2.0 diff --git a/src/client/hooks/useConfig.ts b/src/client/hooks/useConfig.ts index af58930..210a1b0 100644 --- a/src/client/hooks/useConfig.ts +++ b/src/client/hooks/useConfig.ts @@ -2,6 +2,7 @@ import { AppRouterOutput, trpc } from '../api/trpc'; const defaultGlobalConfig: AppRouterOutput['global']['config'] = { allowRegister: false, + alphaMode: false, }; /** diff --git a/src/client/i18next-toolkit.config.js b/src/client/i18next-toolkit.config.cjs similarity index 100% rename from src/client/i18next-toolkit.config.js rename to src/client/i18next-toolkit.config.cjs diff --git a/src/client/package.json b/src/client/package.json index 30b3877..67e1e3e 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -18,7 +18,7 @@ "@ant-design/icons": "^5.2.6", "@antv/l7": "^2.20.14", "@antv/larkmap": "^1.4.13", - "@i18next-toolkit/react": "^1.0.4", + "@i18next-toolkit/react": "^1.0.5", "@loadable/component": "^5.16.3", "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "4.33.0", diff --git a/src/client/pages/Settings/Usage.tsx b/src/client/pages/Settings/Usage.tsx new file mode 100644 index 0000000..2d9398d --- /dev/null +++ b/src/client/pages/Settings/Usage.tsx @@ -0,0 +1,62 @@ +import { Card, Statistic } from 'antd'; +import React, { useMemo } from 'react'; +import { PageHeader } from '../../components/PageHeader'; +import { useTranslation } from '@i18next-toolkit/react'; +import { trpc } from '../../api/trpc'; +import { useCurrentWorkspaceId } from '../../store/user'; +import dayjs from 'dayjs'; +import { formatNumber } from '../../utils/common'; + +export const Usage: React.FC = React.memo(() => { + const { t } = useTranslation(); + const workspaceId = useCurrentWorkspaceId(); + const [startDate, endDate] = useMemo( + () => [dayjs().startOf('month'), dayjs().endOf('day')], + [] + ); + + const { data } = trpc.billing.usage.useQuery({ + workspaceId, + startAt: startDate.valueOf(), + endAt: endDate.valueOf(), + }); + + return ( +
+ + + +
+ {t('Statistic Date')}: + + {startDate.format('YYYY/MM/DD')} - {endDate.format('YYYY/MM/DD')} + +
+ +
+ + + + + + + + + + + +
+
+
+ ); +}); +Usage.displayName = 'Usage'; diff --git a/src/client/pages/Settings/index.tsx b/src/client/pages/Settings/index.tsx index 26a4253..3846d5b 100644 --- a/src/client/pages/Settings/index.tsx +++ b/src/client/pages/Settings/index.tsx @@ -1,5 +1,5 @@ import { Menu, MenuProps } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'; import { WebsiteInfo } from '../../components/website/WebsiteInfo'; import { WebsiteList } from '../../components/website/WebsiteList'; @@ -8,34 +8,46 @@ import { NotificationList } from './NotificationList'; import { Profile } from './Profile'; import { AuditLog } from './AuditLog'; import { Trans } from '@i18next-toolkit/react'; - -const items: MenuProps['items'] = [ - { - key: 'websites', - label: Websites, - }, - { - key: 'notifications', - label: Notifications, - }, - { - key: 'auditLog', - label: Audit Log, - }, - { - key: 'profile', - label: Profile, - }, -]; +import { compact } from 'lodash-es'; +import { useGlobalConfig } from '../../hooks/useConfig'; +import { Usage } from './Usage'; export const SettingsPage: React.FC = React.memo(() => { const navigate = useNavigate(); const { pathname } = useLocation(); + const { alphaMode } = useGlobalConfig(); const onClick: MenuProps['onClick'] = useEvent((e) => { navigate(`/settings/${e.key}`); }); + const items: MenuProps['items'] = useMemo( + () => + compact([ + { + key: 'websites', + label: Websites, + }, + { + key: 'notifications', + label: Notifications, + }, + { + key: 'auditLog', + label: Audit Log, + }, + { + key: 'profile', + label: Profile, + }, + alphaMode && { + key: 'usage', + label: Usage, + }, + ]), + [alphaMode] + ); + const selectedKey = (items.find((item) => pathname.startsWith(`/settings/${item?.key}`)) ?.key as string) ?? 'websites'; @@ -59,6 +71,8 @@ export const SettingsPage: React.FC = React.memo(() => { } /> } /> } /> + + {alphaMode && } />} diff --git a/src/client/public/locales/de/translation.json b/src/client/public/locales/de/translation.json index c27e6cc..b58687a 100644 --- a/src/client/public/locales/de/translation.json +++ b/src/client/public/locales/de/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "Löschen", "k3de768a1": "Passwort ändern", "k3e757ddf": "Hier gibt es noch keinen Monitor.", + "k42347b91": "Website Event Count", "k43e21ee9": "Client-Reporter herunterladen", "k44cad477": "(Aktuell)", "k46f0adde": "Besucherkarte", @@ -103,6 +104,7 @@ "k88a9bf01": "Abzeichen anzeigen", "k88d2647b": "Website", "k89056082": "(30 Tage)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "Dienste", "k8bac6ae0": "6h", "k8ef56a20": "Maximale Wiederholungen, bevor der Dienst als ausgefallen markiert wird und eine Benachrichtigung gesendet wird", @@ -113,6 +115,7 @@ "k90a82c67": "Konto registrieren", "k90b668e5": "Letzte 24 Stunden", "k93374bc9": "Website löschen", + "k951a939a": "Website Accepted Count", "k97ddb155": "Aktuelle Antwort anzeigen", "k98f433ee": "Reporter herunterladen von", "k9a272ecf": "Sind das Ihre Server?", @@ -146,6 +149,7 @@ "kb5673707": "Letzte 7 Tage", "kb659c1bc": "Zert. Ablauf", "kb8de8c50": "BCC", + "kbb31d3db": "Statistic Date", "kbb58c99c": "Erfolgreich gelöscht", "kbcf67f53": "Neues Passwort", "kbd1e7dee": "Nutzung: {{usage}}ms", diff --git a/src/client/public/locales/en/translation.json b/src/client/public/locales/en/translation.json index a9e00b9..1610224 100644 --- a/src/client/public/locales/en/translation.json +++ b/src/client/public/locales/en/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "Delete", "k3de768a1": "Change password", "k3e757ddf": "Here is no monitor yet.", + "k42347b91": "Website Event Count", "k43e21ee9": "Download Client Reportor", "k44cad477": "(Current)", "k46f0adde": "Visitor Map", @@ -103,6 +104,7 @@ "k88a9bf01": "Show Badge", "k88d2647b": "Website", "k89056082": "(30 days)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "Services", "k8bac6ae0": "6h", "k8ef56a20": "Maximum retries before the service is marked as down and a notification is sent", @@ -113,6 +115,7 @@ "k90a82c67": "Register Account", "k90b668e5": "Last 24 Hours", "k93374bc9": "Delete Website", + "k951a939a": "Website Accepted Count", "k97ddb155": "Show Current Response", "k98f433ee": "Download reporter from", "k9a272ecf": "Is this your servers?", @@ -146,6 +149,7 @@ "kb5673707": "Last 7 days", "kb659c1bc": "Cert Exp.", "kb8de8c50": "BCC", + "kbb31d3db": "Statistic Date", "kbb58c99c": "Delete Success", "kbcf67f53": "New Password", "kbd1e7dee": "Usage: {{usage}}ms", diff --git a/src/client/public/locales/fr/translation.json b/src/client/public/locales/fr/translation.json index 806f6c8..ad1989b 100644 --- a/src/client/public/locales/fr/translation.json +++ b/src/client/public/locales/fr/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "Supprimer", "k3de768a1": "Changer le mot de passe", "k3e757ddf": "Il n'y a pas encore de moniteur ici.", + "k42347b91": "Website Event Count", "k43e21ee9": "Télécharger le rapporteur client", "k44cad477": "(Actuel)", "k46f0adde": "Carte des visiteurs", @@ -103,6 +104,7 @@ "k88a9bf01": "Afficher le badge", "k88d2647b": "Site Web", "k89056082": "(30 jours)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "Services", "k8bac6ae0": "6h", "k8ef56a20": "Nombre maximal de tentatives avant que le service ne soit marqué comme étant en panne et qu'une notification soit envoyée", @@ -113,6 +115,7 @@ "k90a82c67": "Créer un compte", "k90b668e5": "24 dernières heures", "k93374bc9": "Supprimer le site Web", + "k951a939a": "Website Accepted Count", "k97ddb155": "Afficher la réponse actuelle", "k98f433ee": "Télécharger le rapporteur de", "k9a272ecf": "S'agit-il de vos serveurs ?", @@ -146,6 +149,7 @@ "kb5673707": "7 derniers jours", "kb659c1bc": "Expiration du cert.", "kb8de8c50": "CCI", + "kbb31d3db": "Statistic Date", "kbb58c99c": "Suppression réussie", "kbcf67f53": "Nouveau mot de passe", "kbd1e7dee": "Utilisation : {{usage}}ms", diff --git a/src/client/public/locales/jp/translation.json b/src/client/public/locales/jp/translation.json index d5f1fd2..98b25b2 100644 --- a/src/client/public/locales/jp/translation.json +++ b/src/client/public/locales/jp/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "削除", "k3de768a1": "パスワードを変更", "k3e757ddf": "モニターがまだありません。", + "k42347b91": "Website Event Count", "k43e21ee9": "クライアントレポーターをダウンロード", "k44cad477": "(現在)", "k46f0adde": "訪問者マップ", @@ -103,6 +104,7 @@ "k88a9bf01": "バッジを表示", "k88d2647b": "ウェブサイト", "k89056082": "(30日間)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "サービス", "k8bac6ae0": "6時間", "k8ef56a20": "サービスがダウンとマークされ、通知が送信される前の最大リトライ回数", @@ -113,6 +115,7 @@ "k90a82c67": "アカウントを登録", "k90b668e5": "過去24時間", "k93374bc9": "ウェブサイトを削除", + "k951a939a": "Website Accepted Count", "k97ddb155": "現在の応答を表示", "k98f433ee": "からレポーターをダウンロード", "k9a272ecf": "これはあなたのサーバーですか?", @@ -146,6 +149,7 @@ "kb5673707": "過去7日間", "kb659c1bc": "証明書の有効期限", "kb8de8c50": "BCC", + "kbb31d3db": "Statistic Date", "kbb58c99c": "削除に成功しました", "kbcf67f53": "新しいパスワード", "kbd1e7dee": "使用量:{{usage}}ms", diff --git a/src/client/public/locales/ru/translation.json b/src/client/public/locales/ru/translation.json index 97347b4..7af131d 100644 --- a/src/client/public/locales/ru/translation.json +++ b/src/client/public/locales/ru/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "Удалить", "k3de768a1": "Сменить пароль", "k3e757ddf": "Здесь пока нет мониторов.", + "k42347b91": "Website Event Count", "k43e21ee9": "Скачать клиентский отчет", "k44cad477": "(Текущий)", "k46f0adde": "Карта посетителей", @@ -103,6 +104,7 @@ "k88a9bf01": "Показать значок", "k88d2647b": "Веб-сайт", "k89056082": "(30 дней)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "Сервисы", "k8bac6ae0": "6ч", "k8ef56a20": "Максимальное количество попыток перед тем, как сервис будет помечен как недоступный и будет отправлено уведомление", @@ -113,6 +115,7 @@ "k90a82c67": "Зарегистрировать аккаунт", "k90b668e5": "Последние 24 часа", "k93374bc9": "Удалить веб-сайт", + "k951a939a": "Website Accepted Count", "k97ddb155": "Показать текущий ответ", "k98f433ee": "Скачать репортер с", "k9a272ecf": "Это ваши серверы?", @@ -146,6 +149,7 @@ "kb5673707": "Последние 7 дней", "kb659c1bc": "Истечение серт.", "kb8de8c50": "Скрытая копия", + "kbb31d3db": "Statistic Date", "kbb58c99c": "Успешно удалено", "kbcf67f53": "Новый пароль", "kbd1e7dee": "Использование: {{usage}}мс", diff --git a/src/client/public/locales/zh/translation.json b/src/client/public/locales/zh/translation.json index c534816..ed8adad 100644 --- a/src/client/public/locales/zh/translation.json +++ b/src/client/public/locales/zh/translation.json @@ -46,6 +46,7 @@ "k3dbe79b1": "删除", "k3de768a1": "更改密码", "k3e757ddf": "这里还没有监控器。", + "k42347b91": "Website Event Count", "k43e21ee9": "下载客户端报告器", "k44cad477": "(当前)", "k46f0adde": "访问者地图", @@ -103,6 +104,7 @@ "k88a9bf01": "显示徽章", "k88d2647b": "网站", "k89056082": "(30天)", + "k89d54f7a": "Monitor Execution Count", "k8a44833f": "服务", "k8bac6ae0": "6小时", "k8ef56a20": "服务被标记为下线并发送通知前的最大重试次数", @@ -113,6 +115,7 @@ "k90a82c67": "注册账户", "k90b668e5": "最近24小时", "k93374bc9": "删除网站", + "k951a939a": "Website Accepted Count", "k97ddb155": "显示当前响应", "k98f433ee": "从这里下载报告器", "k9a272ecf": "这是您的服务器吗?", @@ -146,6 +149,7 @@ "kb5673707": "最近7天", "kb659c1bc": "证书到期", "kb8de8c50": "密送", + "kbb31d3db": "Statistic Date", "kbb58c99c": "删除成功", "kbcf67f53": "新密码", "kbd1e7dee": "使用量:{{usage}}毫秒", diff --git a/src/server/trpc/routers/auditLog.ts b/src/server/trpc/routers/auditLog.ts index 27642d2..de6efcd 100644 --- a/src/server/trpc/routers/auditLog.ts +++ b/src/server/trpc/routers/auditLog.ts @@ -11,7 +11,7 @@ export const auditLogRouter = router({ openapi: { method: 'GET', path: '/fetchByCursor', - tags: [OPENAPI_TAG.WORKSPACE], + tags: [OPENAPI_TAG.AUDIT_LOG], description: 'Fetch workspace audit log', }, }) diff --git a/src/server/trpc/routers/billing.ts b/src/server/trpc/routers/billing.ts new file mode 100644 index 0000000..07e35f0 --- /dev/null +++ b/src/server/trpc/routers/billing.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { router, workspaceProcedure } from '../trpc'; +import { OPENAPI_TAG } from '../../utils/const'; +import { prisma } from '../../model/_client'; + +export const billingRouter = router({ + usage: workspaceProcedure + .meta({ + openapi: { + method: 'GET', + path: '/usage', + tags: [OPENAPI_TAG.BILLING], + description: 'get workspace usage', + }, + }) + .input( + z.object({ + startAt: z.number(), + endAt: z.number(), + }) + ) + .output( + z.object({ + websiteAcceptedCount: z.number(), + websiteEventCount: z.number(), + monitorExecutionCount: z.number(), + }) + ) + .query(async ({ input }) => { + const { workspaceId, startAt, endAt } = input; + + const res = await prisma.workspaceDailyUsage.aggregate({ + where: { + workspaceId, + date: { + gte: new Date(startAt), + lte: new Date(endAt), + }, + }, + _sum: { + websiteAcceptedCount: true, + websiteEventCount: true, + monitorExecutionCount: true, + }, + }); + + return { + websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0, + websiteEventCount: res._sum.websiteEventCount ?? 0, + monitorExecutionCount: res._sum.monitorExecutionCount ?? 0, + }; + }), +}); diff --git a/src/server/trpc/routers/global.ts b/src/server/trpc/routers/global.ts index 4f091a8..e9f6baa 100644 --- a/src/server/trpc/routers/global.ts +++ b/src/server/trpc/routers/global.ts @@ -20,6 +20,7 @@ export const globalRouter = router({ websiteId: z.string().optional(), amapToken: z.string().optional(), mapboxToken: z.string().optional(), + alphaMode: z.boolean(), }) ) .query(async ({ input }) => { @@ -28,10 +29,7 @@ export const globalRouter = router({ websiteId: env.websiteId, amapToken: env.amapToken, mapboxToken: env.mapboxToken, + alphaMode: env.alphaMode, }; }), }); - -function checkEnvTrusty(env: string | undefined): boolean { - return env === '1' || env === 'true'; -} diff --git a/src/server/trpc/routers/index.ts b/src/server/trpc/routers/index.ts index 33e04aa..8f5da64 100644 --- a/src/server/trpc/routers/index.ts +++ b/src/server/trpc/routers/index.ts @@ -7,6 +7,7 @@ import { workspaceRouter } from './workspace'; import { globalRouter } from './global'; import { serverStatusRouter } from './serverStatus'; import { auditLogRouter } from './auditLog'; +import { billingRouter } from './billing'; export const appRouter = router({ global: globalRouter, @@ -17,6 +18,7 @@ export const appRouter = router({ monitor: monitorRouter, serverStatus: serverStatusRouter, auditLog: auditLogRouter, + billing: billingRouter, }); export type AppRouter = typeof appRouter; diff --git a/src/server/utils/const.ts b/src/server/utils/const.ts index 2d2119e..8b72749 100644 --- a/src/server/utils/const.ts +++ b/src/server/utils/const.ts @@ -107,4 +107,6 @@ export enum OPENAPI_TAG { USER = 'User', WEBSITE = 'Website', MONITOR = 'Monitor', + AUDIT_LOG = 'AuditLog', + BILLING = 'Billing', } diff --git a/src/server/utils/env.ts b/src/server/utils/env.ts index ce583ef..e47cce2 100644 --- a/src/server/utils/env.ts +++ b/src/server/utils/env.ts @@ -10,6 +10,7 @@ export const env = { dbDebug: checkEnvTrusty(process.env.DB_DEBUG), amapToken: process.env.AMAP_TOKEN, mapboxToken: process.env.MAPBOX_TOKEN, + alphaMode: checkEnvTrusty(process.env.ALPHA_MODE), }; export function checkEnvTrusty(env: string | undefined): boolean {