feat: add admin file health filters
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
AdminFileDetailViewItem,
|
||||
AdminFileInsightSeverity,
|
||||
AdminFilePatchPayload,
|
||||
AdminFileHealthFilter,
|
||||
AdminFileListParams,
|
||||
AdminFilePolicyAction,
|
||||
AdminFilePolicyActionRequest,
|
||||
@@ -39,6 +40,12 @@ const emptySummary = (): AdminFileSummary => ({
|
||||
textCount: 0,
|
||||
fileCount: 0,
|
||||
chunkedCount: 0,
|
||||
healthAttentionCount: 0,
|
||||
healthDangerCount: 0,
|
||||
healthWarningCount: 0,
|
||||
expiringSoonCount: 0,
|
||||
storageIssueCount: 0,
|
||||
neverRetrievedCount: 0,
|
||||
storageUsed: 0,
|
||||
usedCount: 0
|
||||
})
|
||||
@@ -72,6 +79,12 @@ type DetailPolicyActionOption = {
|
||||
description: string
|
||||
}
|
||||
|
||||
type HealthFilterOption = {
|
||||
value: AdminFileHealthFilter
|
||||
label: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
const emptyBatchEditForm = (): AdminBatchEditForm => ({
|
||||
mode: 'expiresAt',
|
||||
expired_at: '',
|
||||
@@ -106,6 +119,7 @@ export function useAdminFiles() {
|
||||
keyword: '',
|
||||
status: 'all',
|
||||
type: 'all',
|
||||
health: 'all',
|
||||
sortBy: 'created_at',
|
||||
sortOrder: 'desc'
|
||||
})
|
||||
@@ -157,6 +171,7 @@ export function useAdminFiles() {
|
||||
keyword: params.value.keyword,
|
||||
status: params.value.status === 'all' ? undefined : params.value.status,
|
||||
type: params.value.type === 'all' ? undefined : params.value.type,
|
||||
health: params.value.health === 'all' ? undefined : params.value.health,
|
||||
sortBy: params.value.sortBy,
|
||||
sortOrder: params.value.sortOrder
|
||||
}))
|
||||
@@ -165,9 +180,49 @@ export function useAdminFiles() {
|
||||
() =>
|
||||
Boolean(params.value.keyword?.trim()) ||
|
||||
params.value.status !== 'all' ||
|
||||
params.value.type !== 'all'
|
||||
params.value.type !== 'all' ||
|
||||
params.value.health !== 'all'
|
||||
)
|
||||
|
||||
const healthFilterOptions = computed<HealthFilterOption[]>(() => [
|
||||
{ value: 'all', label: t('fileManage.healthFilters.all'), count: summary.value.totalFiles },
|
||||
{
|
||||
value: 'attention',
|
||||
label: t('fileManage.healthFilters.attention'),
|
||||
count: summary.value.healthAttentionCount
|
||||
},
|
||||
{
|
||||
value: 'danger',
|
||||
label: t('fileManage.healthFilters.danger'),
|
||||
count: summary.value.healthDangerCount
|
||||
},
|
||||
{
|
||||
value: 'warning',
|
||||
label: t('fileManage.healthFilters.warning'),
|
||||
count: summary.value.healthWarningCount
|
||||
},
|
||||
{
|
||||
value: 'expiring_soon',
|
||||
label: t('fileManage.healthFilters.expiringSoon'),
|
||||
count: summary.value.expiringSoonCount
|
||||
},
|
||||
{
|
||||
value: 'storage_issue',
|
||||
label: t('fileManage.healthFilters.storageIssue'),
|
||||
count: summary.value.storageIssueCount
|
||||
},
|
||||
{
|
||||
value: 'never_retrieved',
|
||||
label: t('fileManage.healthFilters.neverRetrieved'),
|
||||
count: summary.value.neverRetrievedCount
|
||||
},
|
||||
{
|
||||
value: 'healthy',
|
||||
label: t('fileManage.healthFilters.healthy'),
|
||||
count: Math.max(summary.value.totalFiles - summary.value.healthAttentionCount, 0)
|
||||
}
|
||||
])
|
||||
|
||||
const detailPolicyActionOptions = computed<DetailPolicyActionOption[]>(() => [
|
||||
{
|
||||
action: 'extend_24h',
|
||||
@@ -237,6 +292,19 @@ export function useAdminFiles() {
|
||||
textCount: files.filter((file) => file.isTextFile).length,
|
||||
fileCount: files.filter((file) => !file.isTextFile).length,
|
||||
chunkedCount: files.filter((file) => file.isChunkedFile).length,
|
||||
healthAttentionCount: files.filter((file) =>
|
||||
['danger', 'warning'].includes(file.statusInsightSeverity)
|
||||
).length,
|
||||
healthDangerCount: files.filter((file) => file.statusInsightSeverity === 'danger').length,
|
||||
healthWarningCount: files.filter((file) => file.statusInsightSeverity === 'warning').length,
|
||||
expiringSoonCount: files.filter((file) => file.statusInsightReasons.includes('expires_soon'))
|
||||
.length,
|
||||
storageIssueCount: files.filter((file) =>
|
||||
file.statusInsightReasons.includes('storage_metadata_incomplete')
|
||||
).length,
|
||||
neverRetrievedCount: files.filter((file) =>
|
||||
file.statusInsightReasons.includes('never_retrieved')
|
||||
).length,
|
||||
storageUsed: files.reduce((totalSize, file) => totalSize + normalizeCount(file.size), 0),
|
||||
usedCount: files.reduce(
|
||||
(totalUsed, file) => totalUsed + normalizeCount(file.usedCount ?? file.used_count),
|
||||
@@ -244,29 +312,6 @@ export function useAdminFiles() {
|
||||
)
|
||||
})
|
||||
|
||||
const createFileViewItem = (file: FileListItem): AdminFileViewItem => {
|
||||
const isTextFile = inferIsText(file)
|
||||
const isExpiredFile = inferIsExpired(file)
|
||||
const isChunkedFile = inferIsChunked(file)
|
||||
const remainingDownloadsValue = getRemainingDownloads(file)
|
||||
const usedCount = normalizeCount(file.usedCount ?? file.used_count)
|
||||
|
||||
return {
|
||||
...file,
|
||||
displayName: file.name || `${file.prefix}${file.suffix}` || file.code,
|
||||
displaySize: formatFileSize(file.size),
|
||||
displayExpiredAt: file.expired_at
|
||||
? formatTimestamp(file.expired_at)
|
||||
: t('send.expiration.units.forever'),
|
||||
displayUsage: `${usedCount} ${t('common.times')}`,
|
||||
isTextFile,
|
||||
isExpiredFile,
|
||||
isChunkedFile,
|
||||
remainingDownloadsValue,
|
||||
canPreviewText: isTextFile
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeInsightSeverity = (
|
||||
severity: AdminFileInsightSeverity | undefined
|
||||
): AdminFileInsightSeverity => {
|
||||
@@ -279,6 +324,45 @@ export function useAdminFiles() {
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
const createFileViewItem = (file: FileListItem): AdminFileViewItem => {
|
||||
const isTextFile = inferIsText(file)
|
||||
const isExpiredFile = inferIsExpired(file)
|
||||
const isChunkedFile = inferIsChunked(file)
|
||||
const remainingDownloadsValue = getRemainingDownloads(file)
|
||||
const usedCount = normalizeCount(file.usedCount ?? file.used_count)
|
||||
const statusInsights = file.statusInsights ?? file.status_insights
|
||||
const isPermanentFile =
|
||||
!file.expired_at &&
|
||||
(file.expired_count === null || file.expired_count === undefined || file.expired_count < 0)
|
||||
const statusInsightState =
|
||||
statusInsights?.state ??
|
||||
(isExpiredFile ? 'expired' : isPermanentFile ? 'permanent' : 'available')
|
||||
const statusInsightNextAction =
|
||||
statusInsights?.nextAction ?? statusInsights?.next_action ?? 'monitor'
|
||||
const statusInsightSeverity = normalizeInsightSeverity(statusInsights?.severity)
|
||||
|
||||
return {
|
||||
...file,
|
||||
displayName: file.name || `${file.prefix}${file.suffix}` || file.code,
|
||||
displaySize: formatFileSize(file.size),
|
||||
displayExpiredAt: file.expired_at
|
||||
? formatTimestamp(file.expired_at)
|
||||
: t('send.expiration.units.forever'),
|
||||
displayUsage: `${usedCount} ${t('common.times')}`,
|
||||
displayHealthState: t(`fileManage.insightStates.${statusInsightState}`),
|
||||
displayHealthAction: t(`fileManage.insightActions.${statusInsightNextAction}`),
|
||||
isTextFile,
|
||||
isExpiredFile,
|
||||
isChunkedFile,
|
||||
remainingDownloadsValue,
|
||||
canPreviewText: isTextFile,
|
||||
statusInsightSeverity,
|
||||
statusInsightState,
|
||||
statusInsightNextAction,
|
||||
statusInsightReasons: statusInsights?.reasons || []
|
||||
}
|
||||
}
|
||||
|
||||
const formatTimelineValue = (item: AdminFileDetailTimelineItem) => {
|
||||
if (item.key === 'download_limit') {
|
||||
if (item.status === 'unlimited') return t('fileManage.unlimited')
|
||||
@@ -627,6 +711,7 @@ export function useAdminFiles() {
|
||||
params.value.keyword = ''
|
||||
params.value.status = 'all'
|
||||
params.value.type = 'all'
|
||||
params.value.health = 'all'
|
||||
params.value.sortBy = 'created_at'
|
||||
params.value.sortOrder = 'desc'
|
||||
clearSelection()
|
||||
@@ -645,6 +730,12 @@ export function useAdminFiles() {
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
const setHealthFilter = async (health: AdminFileHealthFilter) => {
|
||||
params.value.health = health
|
||||
params.value.page = 1
|
||||
await loadFiles()
|
||||
}
|
||||
|
||||
const handlePageChange = async (page: number | string) => {
|
||||
if (typeof page === 'string') return
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
@@ -1010,6 +1101,7 @@ export function useAdminFiles() {
|
||||
batchEditForm,
|
||||
detailPolicyActionOptions,
|
||||
downloadingFileId,
|
||||
healthFilterOptions,
|
||||
hasSelectedFiles,
|
||||
params,
|
||||
previewFile,
|
||||
@@ -1050,6 +1142,7 @@ export function useAdminFiles() {
|
||||
openTextPreview,
|
||||
refreshFiles,
|
||||
resetFilters,
|
||||
setHealthFilter,
|
||||
setStatusFilter,
|
||||
setTypeFilter,
|
||||
toggleCurrentPageSelection,
|
||||
|
||||
@@ -395,12 +395,23 @@ export default {
|
||||
storageUsed: 'Storage Used',
|
||||
statusLabel: 'Status',
|
||||
typeLabel: 'Type',
|
||||
healthLabel: 'Health',
|
||||
all: 'All',
|
||||
active: 'Available',
|
||||
expired: 'Expired',
|
||||
fileType: 'File',
|
||||
textType: 'Text',
|
||||
chunkedType: 'Chunked',
|
||||
healthFilters: {
|
||||
all: 'All',
|
||||
attention: 'Needs Attention',
|
||||
danger: 'Issues',
|
||||
warning: 'Warnings',
|
||||
expiringSoon: 'Expiring Soon',
|
||||
storageIssue: 'Storage Issues',
|
||||
neverRetrieved: 'Never Retrieved',
|
||||
healthy: 'Healthy'
|
||||
},
|
||||
sortBy: 'Sort',
|
||||
unlimited: 'Unlimited',
|
||||
remaining: '{count} left',
|
||||
|
||||
@@ -393,12 +393,23 @@ export default {
|
||||
storageUsed: '占用空间',
|
||||
statusLabel: '状态',
|
||||
typeLabel: '类型',
|
||||
healthLabel: '健康',
|
||||
all: '全部',
|
||||
active: '可取件',
|
||||
expired: '已过期',
|
||||
fileType: '文件',
|
||||
textType: '文本',
|
||||
chunkedType: '分片',
|
||||
healthFilters: {
|
||||
all: '全部',
|
||||
attention: '需关注',
|
||||
danger: '异常',
|
||||
warning: '预警',
|
||||
expiringSoon: '即将过期',
|
||||
storageIssue: '存储异常',
|
||||
neverRetrieved: '未取件',
|
||||
healthy: '健康'
|
||||
},
|
||||
sortBy: '排序',
|
||||
unlimited: '不限次数',
|
||||
remaining: '剩余 {count} 次',
|
||||
|
||||
+26
-4
@@ -28,6 +28,8 @@ export interface FileListItem {
|
||||
is_expired?: boolean
|
||||
isChunked?: boolean
|
||||
is_chunked?: boolean
|
||||
statusInsights?: AdminFileDetailStatusInsights
|
||||
status_insights?: AdminFileDetailStatusInsights
|
||||
remainingDownloads?: number | null
|
||||
remaining_downloads?: number | null
|
||||
usedCount?: number
|
||||
@@ -41,11 +43,17 @@ export interface AdminFileViewItem extends FileListItem {
|
||||
displaySize: string
|
||||
displayExpiredAt: string
|
||||
displayUsage: string
|
||||
displayHealthState: string
|
||||
displayHealthAction: string
|
||||
isTextFile: boolean
|
||||
isExpiredFile: boolean
|
||||
isChunkedFile: boolean
|
||||
remainingDownloadsValue: number | null
|
||||
canPreviewText: boolean
|
||||
statusInsightSeverity: AdminFileInsightSeverity
|
||||
statusInsightState: string
|
||||
statusInsightNextAction: string
|
||||
statusInsightReasons: string[]
|
||||
}
|
||||
|
||||
export interface AdminFileSummary {
|
||||
@@ -55,12 +63,29 @@ export interface AdminFileSummary {
|
||||
textCount: number
|
||||
fileCount: number
|
||||
chunkedCount: number
|
||||
healthAttentionCount: number
|
||||
healthDangerCount: number
|
||||
healthWarningCount: number
|
||||
expiringSoonCount: number
|
||||
storageIssueCount: number
|
||||
neverRetrievedCount: number
|
||||
storageUsed: number
|
||||
usedCount: number
|
||||
}
|
||||
|
||||
export type AdminFileStatusFilter = 'all' | 'active' | 'expired'
|
||||
export type AdminFileTypeFilter = 'all' | 'file' | 'text' | 'chunked'
|
||||
export type AdminFileHealthFilter =
|
||||
| 'all'
|
||||
| 'attention'
|
||||
| 'danger'
|
||||
| 'warning'
|
||||
| 'healthy'
|
||||
| 'expired'
|
||||
| 'expiring_soon'
|
||||
| 'storage_issue'
|
||||
| 'never_retrieved'
|
||||
| 'permanent'
|
||||
export type AdminFileSortBy = 'created_at' | 'expired_at' | 'name' | 'size' | 'used_count' | 'code'
|
||||
export type AdminFileSortOrder = 'asc' | 'desc'
|
||||
|
||||
@@ -70,6 +95,7 @@ export interface AdminFileListParams {
|
||||
keyword?: string
|
||||
status?: AdminFileStatusFilter
|
||||
type?: AdminFileTypeFilter
|
||||
health?: AdminFileHealthFilter
|
||||
sortBy?: AdminFileSortBy
|
||||
sortOrder?: AdminFileSortOrder
|
||||
}
|
||||
@@ -241,10 +267,6 @@ export interface AdminFileDetailViewItem extends AdminFileViewItem {
|
||||
filePathValue?: string | null
|
||||
uuidFileNameValue?: string | null
|
||||
uploadIdValue?: string | null
|
||||
statusInsightSeverity: AdminFileInsightSeverity
|
||||
statusInsightState: string
|
||||
statusInsightNextAction: string
|
||||
statusInsightReasons: string[]
|
||||
statusInsightMetrics?: AdminFileDetailInsightMetrics
|
||||
detailTimeline: AdminFileDetailTimelineViewItem[]
|
||||
}
|
||||
|
||||
@@ -98,6 +98,24 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]">
|
||||
<ActivityIcon class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('fileManage.healthLabel') }}
|
||||
</span>
|
||||
<button
|
||||
v-for="option in healthFilterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors"
|
||||
:class="getHealthFilterClass(option.value)"
|
||||
@click="setHealthFilter(option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="typeof option.count === 'number'">({{ option.count }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center text-xs font-medium" :class="[mutedTextClass]">
|
||||
<ArchiveIcon class="mr-1 h-3.5 w-3.5" />
|
||||
@@ -297,6 +315,22 @@
|
||||
<p class="mt-0.5 truncate text-xs" :class="[mutedTextClass]">
|
||||
{{ file.suffix || t('fileManage.textType') }}
|
||||
</p>
|
||||
<div class="mt-2 flex max-w-sm flex-wrap items-center gap-1.5">
|
||||
<span
|
||||
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="getInsightBadgeClass(file.statusInsightSeverity)"
|
||||
:title="file.displayHealthAction"
|
||||
>
|
||||
{{ file.displayHealthState }}
|
||||
</span>
|
||||
<span
|
||||
class="max-w-[16rem] truncate text-xs"
|
||||
:class="[mutedTextClass]"
|
||||
:title="file.displayHealthAction"
|
||||
>
|
||||
{{ file.displayHealthAction }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -997,6 +1031,7 @@ import { computed, onMounted, type Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type {
|
||||
AdminBatchEditMode,
|
||||
AdminFileHealthFilter,
|
||||
AdminFileInsightSeverity,
|
||||
AdminFilePolicyAction,
|
||||
AdminFileSortBy,
|
||||
@@ -1064,6 +1099,7 @@ const {
|
||||
batchEditForm,
|
||||
detailPolicyActionOptions,
|
||||
downloadingFileId,
|
||||
healthFilterOptions,
|
||||
hasSelectedFiles,
|
||||
params,
|
||||
previewFile,
|
||||
@@ -1103,6 +1139,7 @@ const {
|
||||
openTextPreview,
|
||||
refreshFiles,
|
||||
resetFilters,
|
||||
setHealthFilter,
|
||||
setStatusFilter,
|
||||
setTypeFilter,
|
||||
toggleCurrentPageSelection,
|
||||
@@ -1353,6 +1390,9 @@ const getStatusFilterClass = (value: AdminFileStatusFilter) =>
|
||||
|
||||
const getTypeFilterClass = (value: AdminFileTypeFilter) => getPillClass(params.value.type === value)
|
||||
|
||||
const getHealthFilterClass = (value: AdminFileHealthFilter) =>
|
||||
getPillClass(params.value.health === value)
|
||||
|
||||
const getBatchEditModeClass = (value: AdminBatchEditMode) => {
|
||||
if (batchEditForm.value.mode === value) {
|
||||
return isDarkMode.value
|
||||
|
||||
Reference in New Issue
Block a user