feat(admin): add Horizon dashboard entry
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user