feat(admin): add Horizon dashboard entry
CI / Typecheck, test & build (push) Successful in 1m1s
CI / Deploy to Cloudflare Workers (push) Successful in 1m4s

This commit is contained in:
2026-09-12 07:21:20 +08:00
parent c987d9c0b1
commit 3c850528a4
5 changed files with 100 additions and 1 deletions
+1 -1
View File
@@ -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'
+1
View File
@@ -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',
+7
View File
@@ -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',
},
],
},
]
+21
View File
@@ -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,
+70
View File
@@ -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 (
<div>
<PageHeader
title="队列监控台"
description="在浏览器中查看队列吞吐、等待时间与失败任务(Laravel Horizon"
/>
<Card className="max-w-lg">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardDescription>
60 30
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground text-sm">
线
</p>
<Button onClick={open} disabled={busy}>
{busy ? '正在打开…' : '打开队列监控台'}
</Button>
</CardContent>
</Card>
</div>
)
}