diff --git a/src/api/modules/internal-surfaces.ts b/src/api/modules/internal-surfaces.ts index a9f6feb..2df82f5 100644 --- a/src/api/modules/internal-surfaces.ts +++ b/src/api/modules/internal-surfaces.ts @@ -6,7 +6,7 @@ import type { ApiResponse } from '@/api/types' * gated by our own admin permissions. The backend registry is * config/internal_surfaces.php; the surface id must match a key there. */ -export type InternalSurface = 'db-studio' | 'minio-console' +export type InternalSurface = 'db-studio' | 'minio-console' | 'horizon' export interface InternalSurfaceSession { kind: 'internal_surface_session' diff --git a/src/auth/permissions.ts b/src/auth/permissions.ts index 5a254fa..92b2187 100644 --- a/src/auth/permissions.ts +++ b/src/auth/permissions.ts @@ -38,6 +38,7 @@ export const PERM = { // (the `admin` role is synced to admin:% keys and would otherwise inherit it) DB_STUDIO_ACCESS: 'auth:db-studio:access', MINIO_CONSOLE_ACCESS: 'auth:minio-console:access', + HORIZON_ACCESS: 'auth:horizon:access', // role management — only super-admin holds these ROLE_READ: 'auth:role:read', ROLE_CREATE: 'auth:role:create', diff --git a/src/components/layout/nav.ts b/src/components/layout/nav.ts index 3d9f196..9ebac09 100644 --- a/src/components/layout/nav.ts +++ b/src/components/layout/nav.ts @@ -1,6 +1,7 @@ import type { LucideIcon } from 'lucide-react' import { can } from '@/auth/store' import { + Activity, BookOpenText, Bot, ClipboardList, @@ -141,6 +142,12 @@ export const NAV_GROUPS: NavGroup[] = [ icon: HardDrive, permission: 'auth:minio-console:access', }, + { + label: '队列监控台', + to: '/horizon', + icon: Activity, + permission: 'auth:horizon:access', + }, ], }, ] diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 2951392..0ca2abf 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -19,6 +19,7 @@ import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots' import { Route as AuthedPushAppsRouteImport } from './routes/_authed/push-apps' import { Route as AuthedMinioConsoleRouteImport } from './routes/_authed/minio-console' import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales' +import { Route as AuthedHorizonRouteImport } from './routes/_authed/horizon' import { Route as AuthedFilexEventsRouteImport } from './routes/_authed/filex-events' import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports' import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations' @@ -93,6 +94,11 @@ const AuthedLocalesRoute = AuthedLocalesRouteImport.update({ path: '/locales', getParentRoute: () => AuthedRoute, } as any) +const AuthedHorizonRoute = AuthedHorizonRouteImport.update({ + id: '/horizon', + path: '/horizon', + getParentRoute: () => AuthedRoute, +} as any) const AuthedFilexEventsRoute = AuthedFilexEventsRouteImport.update({ id: '/filex-events', path: '/filex-events', @@ -231,6 +237,7 @@ export interface FileRoutesByFullPath { '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute '/filex-events': typeof AuthedFilexEventsRoute + '/horizon': typeof AuthedHorizonRoute '/locales': typeof AuthedLocalesRoute '/minio-console': typeof AuthedMinioConsoleRoute '/push-apps': typeof AuthedPushAppsRoute @@ -265,6 +272,7 @@ export interface FileRoutesByTo { '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute '/filex-events': typeof AuthedFilexEventsRoute + '/horizon': typeof AuthedHorizonRoute '/locales': typeof AuthedLocalesRoute '/minio-console': typeof AuthedMinioConsoleRoute '/push-apps': typeof AuthedPushAppsRoute @@ -302,6 +310,7 @@ export interface FileRoutesById { '/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/_authed/event-reports': typeof AuthedEventReportsRoute '/_authed/filex-events': typeof AuthedFilexEventsRoute + '/_authed/horizon': typeof AuthedHorizonRoute '/_authed/locales': typeof AuthedLocalesRoute '/_authed/minio-console': typeof AuthedMinioConsoleRoute '/_authed/push-apps': typeof AuthedPushAppsRoute @@ -340,6 +349,7 @@ export interface FileRouteTypes { | '/dimzou-translations' | '/event-reports' | '/filex-events' + | '/horizon' | '/locales' | '/minio-console' | '/push-apps' @@ -374,6 +384,7 @@ export interface FileRouteTypes { | '/dimzou-translations' | '/event-reports' | '/filex-events' + | '/horizon' | '/locales' | '/minio-console' | '/push-apps' @@ -410,6 +421,7 @@ export interface FileRouteTypes { | '/_authed/dimzou-translations' | '/_authed/event-reports' | '/_authed/filex-events' + | '/_authed/horizon' | '/_authed/locales' | '/_authed/minio-console' | '/_authed/push-apps' @@ -514,6 +526,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedLocalesRouteImport parentRoute: typeof AuthedRoute } + '/_authed/horizon': { + id: '/_authed/horizon' + path: '/horizon' + fullPath: '/horizon' + preLoaderRoute: typeof AuthedHorizonRouteImport + parentRoute: typeof AuthedRoute + } '/_authed/filex-events': { id: '/_authed/filex-events' path: '/filex-events' @@ -693,6 +712,7 @@ interface AuthedRouteChildren { AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute AuthedEventReportsRoute: typeof AuthedEventReportsRoute AuthedFilexEventsRoute: typeof AuthedFilexEventsRoute + AuthedHorizonRoute: typeof AuthedHorizonRoute AuthedLocalesRoute: typeof AuthedLocalesRoute AuthedMinioConsoleRoute: typeof AuthedMinioConsoleRoute AuthedPushAppsRoute: typeof AuthedPushAppsRoute @@ -727,6 +747,7 @@ const AuthedRouteChildren: AuthedRouteChildren = { AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute, AuthedEventReportsRoute: AuthedEventReportsRoute, AuthedFilexEventsRoute: AuthedFilexEventsRoute, + AuthedHorizonRoute: AuthedHorizonRoute, AuthedLocalesRoute: AuthedLocalesRoute, AuthedMinioConsoleRoute: AuthedMinioConsoleRoute, AuthedPushAppsRoute: AuthedPushAppsRoute, diff --git a/src/routes/_authed/horizon.tsx b/src/routes/_authed/horizon.tsx new file mode 100644 index 0000000..bc47976 --- /dev/null +++ b/src/routes/_authed/horizon.tsx @@ -0,0 +1,70 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' + +import { openInternalSurface } from '@/api/modules/internal-surfaces' +import { PERM, requirePermission } from '@/auth/permissions' +import { PageHeader } from '@/components/page-header' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { handleApiError } from '@/lib/errors' + +export const Route = createFileRoute('/_authed/horizon')({ + beforeLoad: () => requirePermission(PERM.HORIZON_ACCESS), + component: HorizonPage, +}) + +function HorizonPage() { + const [busy, setBusy] = useState(false) + + const open = async () => { + setBusy(true) + const tab = window.open('about:blank', '_blank') + + try { + const res = await openInternalSurface('horizon') + if (tab) { + tab.opener = null + tab.location.replace(res.data.url) + } else { + window.location.assign(res.data.url) + } + } catch (error) { + tab?.close() + handleApiError(error) + } finally { + setBusy(false) + } + } + + return ( +
+ 任务详情会展示队列载荷,可能包含用户数据。重试或删除失败任务会直接作用于线上队列,请在排查问题时谨慎操作。 +
+ +