feat: add admin maintenance queue
This commit is contained in:
@@ -8,6 +8,10 @@ import type {
|
||||
DashboardData,
|
||||
DashboardHealthSummary,
|
||||
DashboardInsightSeverity,
|
||||
DashboardMaintenanceQueue,
|
||||
DashboardMaintenanceQueueItem,
|
||||
DashboardMaintenanceQueueSummary,
|
||||
DashboardMaintenanceQueueViewItem,
|
||||
DashboardOperationalInsight,
|
||||
DashboardOperationalInsightViewItem,
|
||||
DashboardViewData
|
||||
@@ -21,50 +25,54 @@ type UseDashboardStatsOptions = {
|
||||
|
||||
const activityTimelineLimit = 40
|
||||
|
||||
const emptyDashboardData = (): DashboardViewData => ({
|
||||
hasExtendedStats: false,
|
||||
totalFiles: 0,
|
||||
storageUsed: 0,
|
||||
yesterdayCount: 0,
|
||||
todayCount: 0,
|
||||
yesterdaySize: 0,
|
||||
todaySize: 0,
|
||||
sysUptime: null,
|
||||
activeCount: 0,
|
||||
expiredCount: 0,
|
||||
textCount: 0,
|
||||
fileCount: 0,
|
||||
chunkedCount: 0,
|
||||
usedCount: 0,
|
||||
storageBackend: '-',
|
||||
uploadSizeLimit: 0,
|
||||
openUpload: 0,
|
||||
enableChunk: 0,
|
||||
maxSaveSeconds: 0,
|
||||
healthAttentionCount: 0,
|
||||
healthDangerCount: 0,
|
||||
healthWarningCount: 0,
|
||||
expiringSoonCount: 0,
|
||||
storageIssueCount: 0,
|
||||
neverRetrievedCount: 0,
|
||||
healthyCount: 0,
|
||||
permanentCount: 0,
|
||||
topSuffixes: [],
|
||||
recentFiles: [],
|
||||
recentActivities: [],
|
||||
operationalInsights: [],
|
||||
storageUsedText: '0 Bytes',
|
||||
yesterdaySizeText: '0 Bytes',
|
||||
todaySizeText: '0 Bytes',
|
||||
uploadSizeLimitText: '0 Bytes',
|
||||
sysUptimeText: '-',
|
||||
activeRatio: 0,
|
||||
textRatio: 0,
|
||||
fileRatio: 0,
|
||||
healthyRatio: 0,
|
||||
healthAttentionRatio: 0,
|
||||
todaySizeRatio: 0
|
||||
})
|
||||
function emptyDashboardData(): DashboardViewData {
|
||||
return {
|
||||
hasExtendedStats: false,
|
||||
totalFiles: 0,
|
||||
storageUsed: 0,
|
||||
yesterdayCount: 0,
|
||||
todayCount: 0,
|
||||
yesterdaySize: 0,
|
||||
todaySize: 0,
|
||||
sysUptime: null,
|
||||
activeCount: 0,
|
||||
expiredCount: 0,
|
||||
textCount: 0,
|
||||
fileCount: 0,
|
||||
chunkedCount: 0,
|
||||
usedCount: 0,
|
||||
storageBackend: '-',
|
||||
uploadSizeLimit: 0,
|
||||
openUpload: 0,
|
||||
enableChunk: 0,
|
||||
maxSaveSeconds: 0,
|
||||
healthAttentionCount: 0,
|
||||
healthDangerCount: 0,
|
||||
healthWarningCount: 0,
|
||||
expiringSoonCount: 0,
|
||||
storageIssueCount: 0,
|
||||
neverRetrievedCount: 0,
|
||||
healthyCount: 0,
|
||||
permanentCount: 0,
|
||||
topSuffixes: [],
|
||||
recentFiles: [],
|
||||
recentActivities: [],
|
||||
operationalInsights: [],
|
||||
maintenanceItems: [],
|
||||
maintenanceSummary: emptyMaintenanceSummary(),
|
||||
storageUsedText: '0 Bytes',
|
||||
yesterdaySizeText: '0 Bytes',
|
||||
todaySizeText: '0 Bytes',
|
||||
uploadSizeLimitText: '0 Bytes',
|
||||
sysUptimeText: '-',
|
||||
activeRatio: 0,
|
||||
textRatio: 0,
|
||||
fileRatio: 0,
|
||||
healthyRatio: 0,
|
||||
healthAttentionRatio: 0,
|
||||
todaySizeRatio: 0
|
||||
}
|
||||
}
|
||||
|
||||
const toNumber = (value: number | string | null | undefined) => Number(value || 0)
|
||||
|
||||
@@ -152,6 +160,111 @@ const normalizeOperationalInsights = (
|
||||
}
|
||||
})
|
||||
|
||||
function emptyMaintenanceSummary(): DashboardMaintenanceQueueSummary {
|
||||
return {
|
||||
total: 0,
|
||||
actionableCount: 0,
|
||||
dangerCount: 0,
|
||||
warningCount: 0,
|
||||
successCount: 0,
|
||||
neutralCount: 0,
|
||||
fileQueueCount: 0,
|
||||
settingsCount: 0,
|
||||
strongestSeverity: 'success'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeMaintenanceQueueItems = (
|
||||
items: DashboardMaintenanceQueueItem[] = []
|
||||
): DashboardMaintenanceQueueViewItem[] =>
|
||||
items
|
||||
.filter((item) => item && item.key)
|
||||
.map((item) => {
|
||||
const action = item.action && typeof item.action === 'object' ? item.action : {}
|
||||
const actionTypeValue =
|
||||
item.actionType ??
|
||||
item.action_type ??
|
||||
action.actionType ??
|
||||
action.action_type ??
|
||||
action.type ??
|
||||
'file_queue'
|
||||
const targetHealthValue =
|
||||
item.targetHealth ??
|
||||
item.target_health ??
|
||||
action.targetHealth ??
|
||||
action.target_health ??
|
||||
action.health ??
|
||||
null
|
||||
const suggestedActionValue =
|
||||
item.suggestedAction ??
|
||||
item.suggested_action ??
|
||||
action.suggestedAction ??
|
||||
action.suggested_action ??
|
||||
'monitor'
|
||||
|
||||
return {
|
||||
...item,
|
||||
count: toNumber(item.count),
|
||||
priority: toNumber(item.priority),
|
||||
severity: normalizeInsightSeverity(item.severity),
|
||||
actionTypeValue,
|
||||
suggestedActionValue,
|
||||
targetHealthValue
|
||||
}
|
||||
})
|
||||
|
||||
const normalizeMaintenanceSummary = (
|
||||
summary: Partial<DashboardMaintenanceQueueSummary> | undefined,
|
||||
items: DashboardMaintenanceQueueViewItem[]
|
||||
): DashboardMaintenanceQueueSummary => ({
|
||||
total: toNumber(summary?.total ?? items.length),
|
||||
actionableCount: toNumber(summary?.actionableCount ?? summary?.actionable_count),
|
||||
actionable_count: toNumber(summary?.actionableCount ?? summary?.actionable_count),
|
||||
dangerCount: toNumber(summary?.dangerCount ?? summary?.danger_count),
|
||||
danger_count: toNumber(summary?.dangerCount ?? summary?.danger_count),
|
||||
warningCount: toNumber(summary?.warningCount ?? summary?.warning_count),
|
||||
warning_count: toNumber(summary?.warningCount ?? summary?.warning_count),
|
||||
successCount: toNumber(summary?.successCount ?? summary?.success_count),
|
||||
success_count: toNumber(summary?.successCount ?? summary?.success_count),
|
||||
neutralCount: toNumber(summary?.neutralCount ?? summary?.neutral_count),
|
||||
neutral_count: toNumber(summary?.neutralCount ?? summary?.neutral_count),
|
||||
fileQueueCount: toNumber(summary?.fileQueueCount ?? summary?.file_queue_count),
|
||||
file_queue_count: toNumber(summary?.fileQueueCount ?? summary?.file_queue_count),
|
||||
settingsCount: toNumber(summary?.settingsCount ?? summary?.settings_count),
|
||||
settings_count: toNumber(summary?.settingsCount ?? summary?.settings_count),
|
||||
strongestSeverity: normalizeInsightSeverity(
|
||||
summary?.strongestSeverity ?? summary?.strongest_severity
|
||||
),
|
||||
strongest_severity: normalizeInsightSeverity(
|
||||
summary?.strongestSeverity ?? summary?.strongest_severity
|
||||
)
|
||||
})
|
||||
|
||||
const normalizeMaintenanceQueue = (
|
||||
detail: DashboardData | DashboardMaintenanceQueue
|
||||
): Pick<DashboardViewData, 'maintenanceItems' | 'maintenanceSummary'> => {
|
||||
const source = detail as DashboardData & DashboardMaintenanceQueue
|
||||
const queue = source.maintenanceQueue ?? source.maintenance_queue ?? source
|
||||
const items = normalizeMaintenanceQueueItems(
|
||||
queue?.items ??
|
||||
queue?.maintenanceItems ??
|
||||
queue?.maintenance_items ??
|
||||
source.maintenanceItems ??
|
||||
source.maintenance_items ??
|
||||
[]
|
||||
)
|
||||
const rawSummary =
|
||||
queue?.summary ??
|
||||
queue?.maintenanceSummary ??
|
||||
queue?.maintenance_summary ??
|
||||
source.maintenanceSummary ??
|
||||
source.maintenance_summary
|
||||
return {
|
||||
maintenanceItems: items,
|
||||
maintenanceSummary: normalizeMaintenanceSummary(rawSummary, items)
|
||||
}
|
||||
}
|
||||
|
||||
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
|
||||
'healthAttentionCount',
|
||||
'healthDangerCount',
|
||||
@@ -256,6 +369,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
|
||||
dashboardData.operationalInsights = normalizeOperationalInsights(
|
||||
detail.operationalInsights ?? detail.operational_insights ?? detail.insights ?? []
|
||||
)
|
||||
const maintenanceQueue = normalizeMaintenanceQueue(detail)
|
||||
dashboardData.maintenanceItems = maintenanceQueue.maintenanceItems
|
||||
dashboardData.maintenanceSummary = maintenanceQueue.maintenanceSummary
|
||||
|
||||
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
||||
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
||||
|
||||
@@ -101,6 +101,54 @@ export default {
|
||||
success: 'Stable',
|
||||
neutral: 'Suggestion'
|
||||
},
|
||||
maintenanceQueueTitle: 'Maintenance Queue',
|
||||
maintenanceQueueDesc:
|
||||
'Groups file health, retention policy, and upload settings into actionable queues. Click an item to open the related view.',
|
||||
maintenanceActionable: 'Actionable',
|
||||
maintenanceFileQueue: 'File queue',
|
||||
maintenanceSettings: 'Settings',
|
||||
maintenanceQueueActionSettings: 'Adjust settings',
|
||||
maintenanceQueueActionFileQueue: 'View queue',
|
||||
maintenanceQueueItems: {
|
||||
storage_issue: {
|
||||
title: '{count} storage issues',
|
||||
description:
|
||||
'Review records missing download metadata and verify storage settings and file details.'
|
||||
},
|
||||
expired_cleanup: {
|
||||
title: '{count} expired items',
|
||||
description:
|
||||
'Review expired shares, delete stale records, or extend files that should stay available.'
|
||||
},
|
||||
expiring_soon: {
|
||||
title: '{count} expiring soon',
|
||||
description: 'These shares will expire soon. Extend them early or notify recipients.'
|
||||
},
|
||||
never_retrieved: {
|
||||
title: '{count} never retrieved',
|
||||
description:
|
||||
'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.'
|
||||
},
|
||||
permanent_review: {
|
||||
title: '{count} permanent shares',
|
||||
description:
|
||||
'Periodically review long-retention shares to prevent unbounded historical growth.'
|
||||
},
|
||||
guest_upload_retention: {
|
||||
title: 'Guest uploads lack retention limits',
|
||||
description:
|
||||
'Guest uploads are enabled without a maximum retention period. Add a default retention policy.'
|
||||
},
|
||||
chunking_disabled: {
|
||||
title: 'Large-file experience can improve',
|
||||
description:
|
||||
"Today's capacity is near the single-file limit. Enable chunked upload for better reliability."
|
||||
},
|
||||
healthy: {
|
||||
title: 'Maintenance queue is stable',
|
||||
description: 'No maintenance item needs priority attention right now.'
|
||||
}
|
||||
},
|
||||
viewAllActivities: 'View all',
|
||||
activityTimelineTitle: 'Activity Timeline',
|
||||
activityTimelineFiltered: 'Current Results',
|
||||
@@ -993,6 +1041,54 @@ export default {
|
||||
success: 'Stable',
|
||||
neutral: 'Suggestion'
|
||||
},
|
||||
maintenanceQueueTitle: 'Maintenance Queue',
|
||||
maintenanceQueueDesc:
|
||||
'Groups file health, retention policy, and upload settings into actionable queues. Click an item to open the related view.',
|
||||
maintenanceActionable: 'Actionable',
|
||||
maintenanceFileQueue: 'File queue',
|
||||
maintenanceSettings: 'Settings',
|
||||
maintenanceQueueActionSettings: 'Adjust settings',
|
||||
maintenanceQueueActionFileQueue: 'View queue',
|
||||
maintenanceQueueItems: {
|
||||
storage_issue: {
|
||||
title: '{count} storage issues',
|
||||
description:
|
||||
'Review records missing download metadata and verify storage settings and file details.'
|
||||
},
|
||||
expired_cleanup: {
|
||||
title: '{count} expired items',
|
||||
description:
|
||||
'Review expired shares, delete stale records, or extend files that should stay available.'
|
||||
},
|
||||
expiring_soon: {
|
||||
title: '{count} expiring soon',
|
||||
description: 'These shares will expire soon. Extend them early or notify recipients.'
|
||||
},
|
||||
never_retrieved: {
|
||||
title: '{count} never retrieved',
|
||||
description:
|
||||
'Review shares that have not been retrieved to identify ineffective uploads or follow-up gaps.'
|
||||
},
|
||||
permanent_review: {
|
||||
title: '{count} permanent shares',
|
||||
description:
|
||||
'Periodically review long-retention shares to prevent unbounded historical growth.'
|
||||
},
|
||||
guest_upload_retention: {
|
||||
title: 'Guest uploads lack retention limits',
|
||||
description:
|
||||
'Guest uploads are enabled without a maximum retention period. Add a default retention policy.'
|
||||
},
|
||||
chunking_disabled: {
|
||||
title: 'Large-file experience can improve',
|
||||
description:
|
||||
"Today's capacity is near the single-file limit. Enable chunked upload for better reliability."
|
||||
},
|
||||
healthy: {
|
||||
title: 'Maintenance queue is stable',
|
||||
description: 'No maintenance item needs priority attention right now.'
|
||||
}
|
||||
},
|
||||
viewAllActivities: 'View all',
|
||||
activityTimelineTitle: 'Activity Timeline',
|
||||
activityTimelineFiltered: 'Current Results',
|
||||
|
||||
@@ -100,6 +100,47 @@ export default {
|
||||
success: '稳定',
|
||||
neutral: '建议'
|
||||
},
|
||||
maintenanceQueueTitle: '维护队列',
|
||||
maintenanceQueueDesc: '把文件健康、保留策略和上传配置聚合成可处理队列,点击进入对应视图。',
|
||||
maintenanceActionable: '待处理',
|
||||
maintenanceFileQueue: '文件队列',
|
||||
maintenanceSettings: '配置项',
|
||||
maintenanceQueueActionSettings: '调整设置',
|
||||
maintenanceQueueActionFileQueue: '查看队列',
|
||||
maintenanceQueueItems: {
|
||||
storage_issue: {
|
||||
title: '存储异常 {count} 项',
|
||||
description: '优先核对缺少下载元数据的记录,确认存储配置和文件详情。'
|
||||
},
|
||||
expired_cleanup: {
|
||||
title: '已过期 {count} 项',
|
||||
description: '集中检查过期分享,删除无效记录或续期仍需保留的文件。'
|
||||
},
|
||||
expiring_soon: {
|
||||
title: '即将过期 {count} 项',
|
||||
description: '这些分享将在短时间内失效,适合提前续期或通知使用方。'
|
||||
},
|
||||
never_retrieved: {
|
||||
title: '未取件 {count} 项',
|
||||
description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。'
|
||||
},
|
||||
permanent_review: {
|
||||
title: '永久有效 {count} 项',
|
||||
description: '定期复核长期保留的分享,避免历史文件无限增长。'
|
||||
},
|
||||
guest_upload_retention: {
|
||||
title: '游客上传缺少保留上限',
|
||||
description: '游客上传已开启且没有最长保存时间,建议补充默认保留策略。'
|
||||
},
|
||||
chunking_disabled: {
|
||||
title: '大文件体验可优化',
|
||||
description: '今日容量接近单文件上限,开启分片上传可提升上传稳定性。'
|
||||
},
|
||||
healthy: {
|
||||
title: '维护队列稳定',
|
||||
description: '当前未发现需要优先处理的维护事项。'
|
||||
}
|
||||
},
|
||||
viewAllActivities: '查看全部',
|
||||
activityTimelineTitle: '操作时间线',
|
||||
activityTimelineFiltered: '当前结果',
|
||||
@@ -938,6 +979,47 @@ export default {
|
||||
success: '稳定',
|
||||
neutral: '建议'
|
||||
},
|
||||
maintenanceQueueTitle: '维护队列',
|
||||
maintenanceQueueDesc: '把文件健康、保留策略和上传配置聚合成可处理队列,点击进入对应视图。',
|
||||
maintenanceActionable: '待处理',
|
||||
maintenanceFileQueue: '文件队列',
|
||||
maintenanceSettings: '配置项',
|
||||
maintenanceQueueActionSettings: '调整设置',
|
||||
maintenanceQueueActionFileQueue: '查看队列',
|
||||
maintenanceQueueItems: {
|
||||
storage_issue: {
|
||||
title: '存储异常 {count} 项',
|
||||
description: '优先核对缺少下载元数据的记录,确认存储配置和文件详情。'
|
||||
},
|
||||
expired_cleanup: {
|
||||
title: '已过期 {count} 项',
|
||||
description: '集中检查过期分享,删除无效记录或续期仍需保留的文件。'
|
||||
},
|
||||
expiring_soon: {
|
||||
title: '即将过期 {count} 项',
|
||||
description: '这些分享将在短时间内失效,适合提前续期或通知使用方。'
|
||||
},
|
||||
never_retrieved: {
|
||||
title: '未取件 {count} 项',
|
||||
description: '查看创建后尚未被取件的分享,用于识别无效上传或触达缺口。'
|
||||
},
|
||||
permanent_review: {
|
||||
title: '永久有效 {count} 项',
|
||||
description: '定期复核长期保留的分享,避免历史文件无限增长。'
|
||||
},
|
||||
guest_upload_retention: {
|
||||
title: '游客上传缺少保留上限',
|
||||
description: '游客上传已开启且没有最长保存时间,建议补充默认保留策略。'
|
||||
},
|
||||
chunking_disabled: {
|
||||
title: '大文件体验可优化',
|
||||
description: '今日容量接近单文件上限,开启分片上传可提升上传稳定性。'
|
||||
},
|
||||
healthy: {
|
||||
title: '维护队列稳定',
|
||||
description: '当前未发现需要优先处理的维护事项。'
|
||||
}
|
||||
},
|
||||
viewAllActivities: '查看全部',
|
||||
activityTimelineTitle: '操作时间线',
|
||||
activityTimelineFiltered: '当前结果',
|
||||
|
||||
@@ -3,7 +3,8 @@ import type {
|
||||
ApiResponse,
|
||||
DashboardActivitiesParams,
|
||||
DashboardActivitiesResponse,
|
||||
DashboardData
|
||||
DashboardData,
|
||||
DashboardMaintenanceQueue
|
||||
} from '@/types'
|
||||
|
||||
export class StatsService {
|
||||
@@ -11,6 +12,10 @@ export class StatsService {
|
||||
return api.get('/admin/dashboard')
|
||||
}
|
||||
|
||||
static async getMaintenanceQueue(): Promise<ApiResponse<DashboardMaintenanceQueue>> {
|
||||
return api.get('/admin/dashboard/maintenance-queue')
|
||||
}
|
||||
|
||||
static async getActivities(
|
||||
params: DashboardActivitiesParams
|
||||
): Promise<ApiResponse<DashboardActivitiesResponse>> {
|
||||
|
||||
@@ -46,6 +46,12 @@ export interface DashboardData {
|
||||
operationalInsights?: DashboardOperationalInsight[]
|
||||
operational_insights?: DashboardOperationalInsight[]
|
||||
insights?: DashboardOperationalInsight[]
|
||||
maintenanceQueue?: DashboardMaintenanceQueue
|
||||
maintenance_queue?: DashboardMaintenanceQueue
|
||||
maintenanceItems?: DashboardMaintenanceQueueItem[]
|
||||
maintenance_items?: DashboardMaintenanceQueueItem[]
|
||||
maintenanceSummary?: Partial<DashboardMaintenanceQueueSummary>
|
||||
maintenance_summary?: Partial<DashboardMaintenanceQueueSummary>
|
||||
}
|
||||
|
||||
export interface DashboardSuffixStat {
|
||||
@@ -125,6 +131,69 @@ export interface DashboardActivitiesResponse {
|
||||
|
||||
export type DashboardInsightSeverity = 'danger' | 'warning' | 'success' | 'neutral'
|
||||
|
||||
export interface DashboardMaintenanceQueueAction {
|
||||
type?: string
|
||||
actionType?: string
|
||||
action_type?: string
|
||||
health?: AdminFileHealthFilter | string | null
|
||||
targetHealth?: AdminFileHealthFilter | string | null
|
||||
target_health?: AdminFileHealthFilter | string | null
|
||||
suggestedAction?: string
|
||||
suggested_action?: string
|
||||
}
|
||||
|
||||
export interface DashboardMaintenanceQueueItem {
|
||||
key: string
|
||||
severity: DashboardInsightSeverity | string
|
||||
category?: string
|
||||
priority?: number
|
||||
count: number
|
||||
action?: DashboardMaintenanceQueueAction
|
||||
actionType?: string
|
||||
action_type?: string
|
||||
suggestedAction?: string
|
||||
suggested_action?: string
|
||||
targetHealth?: AdminFileHealthFilter | string | null
|
||||
target_health?: AdminFileHealthFilter | string | null
|
||||
}
|
||||
|
||||
export interface DashboardMaintenanceQueueSummary {
|
||||
total: number
|
||||
actionableCount: number
|
||||
actionable_count?: number
|
||||
dangerCount: number
|
||||
danger_count?: number
|
||||
warningCount: number
|
||||
warning_count?: number
|
||||
successCount: number
|
||||
success_count?: number
|
||||
neutralCount: number
|
||||
neutral_count?: number
|
||||
fileQueueCount: number
|
||||
file_queue_count?: number
|
||||
settingsCount: number
|
||||
settings_count?: number
|
||||
strongestSeverity: DashboardInsightSeverity
|
||||
strongest_severity?: DashboardInsightSeverity
|
||||
}
|
||||
|
||||
export interface DashboardMaintenanceQueue {
|
||||
items?: DashboardMaintenanceQueueItem[]
|
||||
maintenanceItems?: DashboardMaintenanceQueueItem[]
|
||||
maintenance_items?: DashboardMaintenanceQueueItem[]
|
||||
summary?: Partial<DashboardMaintenanceQueueSummary>
|
||||
maintenanceSummary?: Partial<DashboardMaintenanceQueueSummary>
|
||||
maintenance_summary?: Partial<DashboardMaintenanceQueueSummary>
|
||||
}
|
||||
|
||||
export interface DashboardMaintenanceQueueViewItem extends DashboardMaintenanceQueueItem {
|
||||
severity: DashboardInsightSeverity
|
||||
priority: number
|
||||
actionTypeValue: string
|
||||
suggestedActionValue: string
|
||||
targetHealthValue: AdminFileHealthFilter | string | null
|
||||
}
|
||||
|
||||
export interface DashboardOperationalInsightAction {
|
||||
type?: string
|
||||
actionType?: string
|
||||
@@ -174,6 +243,12 @@ export type DashboardViewData = Omit<
|
||||
| 'operationalInsights'
|
||||
| 'operational_insights'
|
||||
| 'insights'
|
||||
| 'maintenanceQueue'
|
||||
| 'maintenance_queue'
|
||||
| 'maintenanceItems'
|
||||
| 'maintenance_items'
|
||||
| 'maintenanceSummary'
|
||||
| 'maintenance_summary'
|
||||
| 'storageUsed'
|
||||
| 'yesterdaySize'
|
||||
| 'todaySize'
|
||||
@@ -195,6 +270,8 @@ export type DashboardViewData = Omit<
|
||||
recentFiles: DashboardRecentFile[]
|
||||
recentActivities: DashboardActivityViewItem[]
|
||||
operationalInsights: DashboardOperationalInsightViewItem[]
|
||||
maintenanceItems: DashboardMaintenanceQueueViewItem[]
|
||||
maintenanceSummary: DashboardMaintenanceQueueSummary
|
||||
storageUsed: number
|
||||
yesterdaySize: number
|
||||
todaySize: number
|
||||
|
||||
@@ -134,6 +134,80 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="dashboardData.hasExtendedStats && dashboardData.maintenanceItems.length > 0"
|
||||
class="mt-6 rounded-lg p-5 shadow-sm"
|
||||
:class="[panelClass]"
|
||||
>
|
||||
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||
{{ t('admin.dashboard.maintenanceQueueTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.maintenanceQueueDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 sm:min-w-[24rem]">
|
||||
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
|
||||
<p class="text-xs" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.maintenanceActionable') }}
|
||||
</p>
|
||||
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
|
||||
{{ dashboardData.maintenanceSummary.actionableCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
|
||||
<p class="text-xs" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.maintenanceFileQueue') }}
|
||||
</p>
|
||||
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
|
||||
{{ dashboardData.maintenanceSummary.fileQueueCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="rounded-lg border px-3 py-2" :class="[subtlePanelClass]">
|
||||
<p class="text-xs" :class="[mutedTextClass]">
|
||||
{{ t('admin.dashboard.maintenanceSettings') }}
|
||||
</p>
|
||||
<strong class="mt-1 block text-xl" :class="[primaryTextClass]">
|
||||
{{ dashboardData.maintenanceSummary.settingsCount }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<button
|
||||
v-for="item in dashboardData.maintenanceItems"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="group flex min-h-36 flex-col rounded-lg border p-4 text-left transition-colors"
|
||||
:class="getMaintenanceQueueClass(item.severity)"
|
||||
@click="openMaintenanceQueueItem(item)"
|
||||
>
|
||||
<span class="flex items-start justify-between gap-3">
|
||||
<span class="rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-current/15">
|
||||
{{ getOperationalInsightSeverityLabel(item.severity) }}
|
||||
</span>
|
||||
<component :is="getMaintenanceQueueIcon(item.severity)" class="h-5 w-5 shrink-0" />
|
||||
</span>
|
||||
<strong class="mt-3 text-base">
|
||||
{{ getMaintenanceQueueTitle(item) }}
|
||||
</strong>
|
||||
<span class="mt-2 line-clamp-2 text-sm opacity-80">
|
||||
{{ getMaintenanceQueueDescription(item) }}
|
||||
</span>
|
||||
<span class="mt-auto flex items-center justify-between gap-2 pt-4 text-sm font-medium">
|
||||
<span>{{ getMaintenanceQueueActionLabel(item) }}</span>
|
||||
<ArrowRightIcon
|
||||
class="h-4 w-4 shrink-0 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
@@ -739,6 +813,7 @@ import type {
|
||||
DashboardActivityViewItem,
|
||||
DashboardHealthAction,
|
||||
DashboardInsightSeverity,
|
||||
DashboardMaintenanceQueueViewItem,
|
||||
DashboardOperationalInsightViewItem
|
||||
} from '@/types'
|
||||
|
||||
@@ -912,6 +987,42 @@ const openOperationalInsight = (insight: DashboardOperationalInsightViewItem) =>
|
||||
})
|
||||
}
|
||||
|
||||
const getMaintenanceQueueIcon = (severity: DashboardInsightSeverity) =>
|
||||
operationalInsightIconMap[severity]
|
||||
|
||||
const getMaintenanceQueueClass = (severity: DashboardInsightSeverity) =>
|
||||
getOperationalInsightClass(severity)
|
||||
|
||||
const getMaintenanceQueueTitle = (item: DashboardMaintenanceQueueViewItem) => {
|
||||
const key = `admin.dashboard.maintenanceQueueItems.${item.key}.title`
|
||||
const title = t(key, { count: item.count })
|
||||
return title === key ? item.key : title
|
||||
}
|
||||
|
||||
const getMaintenanceQueueDescription = (item: DashboardMaintenanceQueueViewItem) => {
|
||||
const key = `admin.dashboard.maintenanceQueueItems.${item.key}.description`
|
||||
const description = t(key, { count: item.count })
|
||||
return description === key ? item.key : description
|
||||
}
|
||||
|
||||
const getMaintenanceQueueActionLabel = (item: DashboardMaintenanceQueueViewItem) =>
|
||||
item.actionTypeValue === 'settings'
|
||||
? t('admin.dashboard.maintenanceQueueActionSettings')
|
||||
: t('admin.dashboard.maintenanceQueueActionFileQueue')
|
||||
|
||||
const openMaintenanceQueueItem = (item: DashboardMaintenanceQueueViewItem) => {
|
||||
if (item.actionTypeValue === 'settings') {
|
||||
void router.push({ path: ROUTES.SETTINGS })
|
||||
return
|
||||
}
|
||||
|
||||
const health = item.targetHealthValue
|
||||
void router.push({
|
||||
path: ROUTES.FILE_MANAGE,
|
||||
query: health ? { health } : {}
|
||||
})
|
||||
}
|
||||
|
||||
const formatCreatedAt = (value: string | null) => {
|
||||
if (!value) return '-'
|
||||
return formatTimestamp(value, 'datetime')
|
||||
|
||||
Reference in New Issue
Block a user