feat: add admin activity stream
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import { computed, ref, reactive } from 'vue'
|
import { computed, ref, reactive } from 'vue'
|
||||||
import { StatsService } from '@/services'
|
import { StatsService } from '@/services'
|
||||||
import type { DashboardData, DashboardHealthSummary, DashboardViewData } from '@/types'
|
import type {
|
||||||
|
DashboardActivity,
|
||||||
|
DashboardActivityViewItem,
|
||||||
|
DashboardData,
|
||||||
|
DashboardHealthSummary,
|
||||||
|
DashboardViewData
|
||||||
|
} from '@/types'
|
||||||
import { formatFileSize, getErrorMessage } from '@/utils/common'
|
import { formatFileSize, getErrorMessage } from '@/utils/common'
|
||||||
|
|
||||||
type UseDashboardStatsOptions = {
|
type UseDashboardStatsOptions = {
|
||||||
@@ -37,6 +43,7 @@ const emptyDashboardData = (): DashboardViewData => ({
|
|||||||
permanentCount: 0,
|
permanentCount: 0,
|
||||||
topSuffixes: [],
|
topSuffixes: [],
|
||||||
recentFiles: [],
|
recentFiles: [],
|
||||||
|
recentActivities: [],
|
||||||
storageUsedText: '0 Bytes',
|
storageUsedText: '0 Bytes',
|
||||||
yesterdaySizeText: '0 Bytes',
|
yesterdaySizeText: '0 Bytes',
|
||||||
todaySizeText: '0 Bytes',
|
todaySizeText: '0 Bytes',
|
||||||
@@ -72,6 +79,20 @@ const normalizeRecentFiles = (recentFiles: DashboardViewData['recentFiles']) =>
|
|||||||
usedCount: toNumber(file.usedCount)
|
usedCount: toNumber(file.usedCount)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const normalizeRecentActivities = (
|
||||||
|
activities: DashboardActivity[] = []
|
||||||
|
): DashboardActivityViewItem[] =>
|
||||||
|
activities
|
||||||
|
.filter((activity) => activity && activity.id && activity.action)
|
||||||
|
.map((activity) => ({
|
||||||
|
...activity,
|
||||||
|
count: toNumber(activity.count) || 1,
|
||||||
|
targetTypeValue: activity.targetType ?? activity.target_type ?? 'system',
|
||||||
|
targetNameValue: activity.targetName ?? activity.target_name ?? '',
|
||||||
|
createdAtValue: activity.createdAt ?? activity.created_at ?? null,
|
||||||
|
meta: activity.meta && typeof activity.meta === 'object' ? activity.meta : {}
|
||||||
|
}))
|
||||||
|
|
||||||
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
|
const healthSummaryKeys: (keyof DashboardHealthSummary)[] = [
|
||||||
'healthAttentionCount',
|
'healthAttentionCount',
|
||||||
'healthDangerCount',
|
'healthDangerCount',
|
||||||
@@ -147,6 +168,9 @@ export function useDashboardStats(options: UseDashboardStatsOptions = {}) {
|
|||||||
})
|
})
|
||||||
dashboardData.topSuffixes = detail.topSuffixes || []
|
dashboardData.topSuffixes = detail.topSuffixes || []
|
||||||
dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || [])
|
dashboardData.recentFiles = normalizeRecentFiles(detail.recentFiles || [])
|
||||||
|
dashboardData.recentActivities = normalizeRecentActivities(
|
||||||
|
detail.recentActivities ?? detail.recent_activities ?? []
|
||||||
|
)
|
||||||
|
|
||||||
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
dashboardData.storageUsedText = formatFileSize(dashboardData.storageUsed)
|
||||||
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
dashboardData.yesterdaySizeText = formatFileSize(dashboardData.yesterdaySize)
|
||||||
|
|||||||
@@ -88,6 +88,27 @@ export default {
|
|||||||
todayCapacityReference: 'Today Size / Single File Limit',
|
todayCapacityReference: 'Today Size / Single File Limit',
|
||||||
fileTypeDistribution: 'Type Distribution',
|
fileTypeDistribution: 'Type Distribution',
|
||||||
textType: 'Text',
|
textType: 'Text',
|
||||||
|
recentActivities: 'Recent Activity',
|
||||||
|
recentActivitiesDesc: 'Key admin changes for quick operational traceability.',
|
||||||
|
noActivities: 'No activity yet',
|
||||||
|
activityUnknownTarget: 'Unnamed target',
|
||||||
|
activityDescription: 'Target: {target}',
|
||||||
|
activityBatchDescription: 'Target: {target}, {count} items',
|
||||||
|
activityActions: {
|
||||||
|
'file.delete': 'Deleted file',
|
||||||
|
'files.batch_delete': 'Batch deleted files',
|
||||||
|
'file.update': 'Edited file',
|
||||||
|
'files.batch_update': 'Batch updated files',
|
||||||
|
'file.policy_action': 'Updated file policy',
|
||||||
|
'files.batch_policy_action': 'Batch updated policies',
|
||||||
|
'file.metadata_update': 'Updated operations notes',
|
||||||
|
'file.view_preset_create': 'Created view preset',
|
||||||
|
'file.view_preset_update': 'Updated view preset',
|
||||||
|
'file.view_preset_delete': 'Deleted view preset',
|
||||||
|
'config.update': 'Updated system settings',
|
||||||
|
'local_file.delete': 'Deleted local file',
|
||||||
|
'local_file.share': 'Shared local file'
|
||||||
|
},
|
||||||
recentFiles: 'Recent Shares',
|
recentFiles: 'Recent Shares',
|
||||||
recentFilesDesc: 'Recently created share records for quick status checks.',
|
recentFilesDesc: 'Recently created share records for quick status checks.',
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
@@ -829,6 +850,27 @@ export default {
|
|||||||
todayCapacityReference: 'Today Size / Single File Limit',
|
todayCapacityReference: 'Today Size / Single File Limit',
|
||||||
fileTypeDistribution: 'Type Distribution',
|
fileTypeDistribution: 'Type Distribution',
|
||||||
textType: 'Text',
|
textType: 'Text',
|
||||||
|
recentActivities: 'Recent Activity',
|
||||||
|
recentActivitiesDesc: 'Key admin changes for quick operational traceability.',
|
||||||
|
noActivities: 'No activity yet',
|
||||||
|
activityUnknownTarget: 'Unnamed target',
|
||||||
|
activityDescription: 'Target: {target}',
|
||||||
|
activityBatchDescription: 'Target: {target}, {count} items',
|
||||||
|
activityActions: {
|
||||||
|
'file.delete': 'Deleted file',
|
||||||
|
'files.batch_delete': 'Batch deleted files',
|
||||||
|
'file.update': 'Edited file',
|
||||||
|
'files.batch_update': 'Batch updated files',
|
||||||
|
'file.policy_action': 'Updated file policy',
|
||||||
|
'files.batch_policy_action': 'Batch updated policies',
|
||||||
|
'file.metadata_update': 'Updated operations notes',
|
||||||
|
'file.view_preset_create': 'Created view preset',
|
||||||
|
'file.view_preset_update': 'Updated view preset',
|
||||||
|
'file.view_preset_delete': 'Deleted view preset',
|
||||||
|
'config.update': 'Updated system settings',
|
||||||
|
'local_file.delete': 'Deleted local file',
|
||||||
|
'local_file.share': 'Shared local file'
|
||||||
|
},
|
||||||
recentFiles: 'Recent Shares',
|
recentFiles: 'Recent Shares',
|
||||||
recentFilesDesc: 'Recently created share records for quick status checks.',
|
recentFilesDesc: 'Recently created share records for quick status checks.',
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
|
|||||||
@@ -88,6 +88,27 @@ export default {
|
|||||||
todayCapacityReference: '今日容量 / 单文件上限',
|
todayCapacityReference: '今日容量 / 单文件上限',
|
||||||
fileTypeDistribution: '类型分布',
|
fileTypeDistribution: '类型分布',
|
||||||
textType: '文本',
|
textType: '文本',
|
||||||
|
recentActivities: '最近操作',
|
||||||
|
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
||||||
|
noActivities: '暂无操作记录',
|
||||||
|
activityUnknownTarget: '未命名对象',
|
||||||
|
activityDescription: '对象:{target}',
|
||||||
|
activityBatchDescription: '对象:{target},共 {count} 项',
|
||||||
|
activityActions: {
|
||||||
|
'file.delete': '删除文件',
|
||||||
|
'files.batch_delete': '批量删除文件',
|
||||||
|
'file.update': '编辑文件',
|
||||||
|
'files.batch_update': '批量更新文件',
|
||||||
|
'file.policy_action': '更新文件策略',
|
||||||
|
'files.batch_policy_action': '批量更新策略',
|
||||||
|
'file.metadata_update': '更新运营备注',
|
||||||
|
'file.view_preset_create': '创建视图预设',
|
||||||
|
'file.view_preset_update': '覆盖视图预设',
|
||||||
|
'file.view_preset_delete': '删除视图预设',
|
||||||
|
'config.update': '更新系统配置',
|
||||||
|
'local_file.delete': '删除本地文件',
|
||||||
|
'local_file.share': '分享本地文件'
|
||||||
|
},
|
||||||
recentFiles: '最近分享',
|
recentFiles: '最近分享',
|
||||||
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
||||||
available: '可取件',
|
available: '可取件',
|
||||||
@@ -791,6 +812,27 @@ export default {
|
|||||||
todayCapacityReference: '今日容量 / 单文件上限',
|
todayCapacityReference: '今日容量 / 单文件上限',
|
||||||
fileTypeDistribution: '类型分布',
|
fileTypeDistribution: '类型分布',
|
||||||
textType: '文本',
|
textType: '文本',
|
||||||
|
recentActivities: '最近操作',
|
||||||
|
recentActivitiesDesc: '记录管理员最近的关键改动,便于回溯处理链路。',
|
||||||
|
noActivities: '暂无操作记录',
|
||||||
|
activityUnknownTarget: '未命名对象',
|
||||||
|
activityDescription: '对象:{target}',
|
||||||
|
activityBatchDescription: '对象:{target},共 {count} 项',
|
||||||
|
activityActions: {
|
||||||
|
'file.delete': '删除文件',
|
||||||
|
'files.batch_delete': '批量删除文件',
|
||||||
|
'file.update': '编辑文件',
|
||||||
|
'files.batch_update': '批量更新文件',
|
||||||
|
'file.policy_action': '更新文件策略',
|
||||||
|
'files.batch_policy_action': '批量更新策略',
|
||||||
|
'file.metadata_update': '更新运营备注',
|
||||||
|
'file.view_preset_create': '创建视图预设',
|
||||||
|
'file.view_preset_update': '覆盖视图预设',
|
||||||
|
'file.view_preset_delete': '删除视图预设',
|
||||||
|
'config.update': '更新系统配置',
|
||||||
|
'local_file.delete': '删除本地文件',
|
||||||
|
'local_file.share': '分享本地文件'
|
||||||
|
},
|
||||||
recentFiles: '最近分享',
|
recentFiles: '最近分享',
|
||||||
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
recentFilesDesc: '最近创建的分享记录,便于快速核对状态。',
|
||||||
available: '可取件',
|
available: '可取件',
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export interface DashboardData {
|
|||||||
healthSummary?: Partial<DashboardHealthSummary>
|
healthSummary?: Partial<DashboardHealthSummary>
|
||||||
topSuffixes?: DashboardSuffixStat[]
|
topSuffixes?: DashboardSuffixStat[]
|
||||||
recentFiles?: DashboardRecentFile[]
|
recentFiles?: DashboardRecentFile[]
|
||||||
|
recentActivities?: DashboardActivity[]
|
||||||
|
recent_activities?: DashboardActivity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardSuffixStat {
|
export interface DashboardSuffixStat {
|
||||||
@@ -62,6 +64,27 @@ export interface DashboardRecentFile {
|
|||||||
isExpired: boolean
|
isExpired: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardActivity {
|
||||||
|
id: string
|
||||||
|
action: string
|
||||||
|
targetType?: string
|
||||||
|
target_type?: string
|
||||||
|
targetId?: string | number | null
|
||||||
|
target_id?: string | number | null
|
||||||
|
targetName?: string
|
||||||
|
target_name?: string
|
||||||
|
count: number
|
||||||
|
meta?: Record<string, unknown>
|
||||||
|
createdAt?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardActivityViewItem extends DashboardActivity {
|
||||||
|
targetTypeValue: string
|
||||||
|
targetNameValue: string
|
||||||
|
createdAtValue: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type DashboardViewData = Omit<
|
export type DashboardViewData = Omit<
|
||||||
DashboardData,
|
DashboardData,
|
||||||
| keyof DashboardHealthSummary
|
| keyof DashboardHealthSummary
|
||||||
@@ -78,6 +101,8 @@ export type DashboardViewData = Omit<
|
|||||||
| 'maxSaveSeconds'
|
| 'maxSaveSeconds'
|
||||||
| 'topSuffixes'
|
| 'topSuffixes'
|
||||||
| 'recentFiles'
|
| 'recentFiles'
|
||||||
|
| 'recentActivities'
|
||||||
|
| 'recent_activities'
|
||||||
| 'storageUsed'
|
| 'storageUsed'
|
||||||
| 'yesterdaySize'
|
| 'yesterdaySize'
|
||||||
| 'todaySize'
|
| 'todaySize'
|
||||||
@@ -97,6 +122,7 @@ export type DashboardViewData = Omit<
|
|||||||
maxSaveSeconds: number
|
maxSaveSeconds: number
|
||||||
topSuffixes: DashboardSuffixStat[]
|
topSuffixes: DashboardSuffixStat[]
|
||||||
recentFiles: DashboardRecentFile[]
|
recentFiles: DashboardRecentFile[]
|
||||||
|
recentActivities: DashboardActivityViewItem[]
|
||||||
storageUsed: number
|
storageUsed: number
|
||||||
yesterdaySize: number
|
yesterdaySize: number
|
||||||
todaySize: number
|
todaySize: number
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-4">
|
||||||
<section class="rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
<section class="rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||||
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||||
{{ t('admin.dashboard.fileTypeDistribution') }}
|
{{ t('admin.dashboard.fileTypeDistribution') }}
|
||||||
@@ -255,6 +255,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||||
|
<div class="mb-4 flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ t('admin.dashboard.recentActivities') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm" :class="[mutedTextClass]">
|
||||||
|
{{ t('admin.dashboard.recentActivitiesDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<HistoryIcon
|
||||||
|
class="h-5 w-5 shrink-0"
|
||||||
|
:class="[isDarkMode ? 'text-indigo-300' : 'text-indigo-500']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="dashboardData.recentActivities.length === 0"
|
||||||
|
class="rounded-lg border px-4 py-6 text-center text-sm"
|
||||||
|
:class="[subtlePanelClass, mutedTextClass]"
|
||||||
|
>
|
||||||
|
{{ t('admin.dashboard.noActivities') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="activity in dashboardData.recentActivities"
|
||||||
|
:key="activity.id"
|
||||||
|
class="rounded-lg border px-3 py-3"
|
||||||
|
:class="[subtlePanelClass]"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
class="mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
||||||
|
:class="getActivityIconClass(activity.action)"
|
||||||
|
>
|
||||||
|
<component :is="getActivityIcon(activity.action)" class="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="text-sm font-medium" :class="[primaryTextClass]">
|
||||||
|
{{ getActivityTitle(activity.action) }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 truncate text-xs" :class="[mutedTextClass]">
|
||||||
|
{{ getActivityDescription(activity) }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs" :class="[mutedTextClass]">
|
||||||
|
{{ formatCreatedAt(activity.createdAtValue) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
<section class="xl:col-span-2 rounded-lg p-5 shadow-sm" :class="[panelClass]">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -369,13 +422,18 @@ import {
|
|||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
DownloadCloudIcon,
|
DownloadCloudIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
FilesIcon,
|
FilesIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
HardDriveIcon,
|
HardDriveIcon,
|
||||||
|
HistoryIcon,
|
||||||
|
PencilIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
|
SlidersHorizontalIcon,
|
||||||
|
TrashIcon,
|
||||||
UploadCloudIcon
|
UploadCloudIcon
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
@@ -383,7 +441,7 @@ import { useDashboardStats, useInjectedDarkMode } from '@/composables'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { formatFileSize, formatTimestamp } from '@/utils/common'
|
import { formatFileSize, formatTimestamp } from '@/utils/common'
|
||||||
import { ROUTES } from '@/constants'
|
import { ROUTES } from '@/constants'
|
||||||
import type { DashboardHealthAction } from '@/types'
|
import type { DashboardActivityViewItem, DashboardHealthAction } from '@/types'
|
||||||
|
|
||||||
const isDarkMode = useInjectedDarkMode()
|
const isDarkMode = useInjectedDarkMode()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -463,6 +521,71 @@ const formatCreatedAt = (value: string | null) => {
|
|||||||
return formatTimestamp(value, 'datetime')
|
return formatTimestamp(value, 'datetime')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivityTone = 'danger' | 'warning' | 'success' | 'neutral'
|
||||||
|
|
||||||
|
const activityIconMap: Record<string, Component> = {
|
||||||
|
'file.delete': TrashIcon,
|
||||||
|
'files.batch_delete': TrashIcon,
|
||||||
|
'file.update': PencilIcon,
|
||||||
|
'files.batch_update': PencilIcon,
|
||||||
|
'file.policy_action': ClockIcon,
|
||||||
|
'files.batch_policy_action': ClockIcon,
|
||||||
|
'file.metadata_update': FileTextIcon,
|
||||||
|
'file.view_preset_create': SlidersHorizontalIcon,
|
||||||
|
'file.view_preset_update': SlidersHorizontalIcon,
|
||||||
|
'file.view_preset_delete': SlidersHorizontalIcon,
|
||||||
|
'config.update': SlidersHorizontalIcon,
|
||||||
|
'local_file.delete': TrashIcon,
|
||||||
|
'local_file.share': UploadCloudIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityIcon = (action: string) => activityIconMap[action] || HistoryIcon
|
||||||
|
|
||||||
|
const getActivityTone = (action: string): ActivityTone => {
|
||||||
|
if (action.includes('delete')) return 'danger'
|
||||||
|
if (action.includes('policy') || action.includes('update')) return 'warning'
|
||||||
|
if (action.includes('share') || action.includes('create')) return 'success'
|
||||||
|
return 'neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityIconClass = (action: string) => {
|
||||||
|
const tone = getActivityTone(action)
|
||||||
|
const darkClasses: Record<ActivityTone, string> = {
|
||||||
|
danger: 'bg-red-500/10 text-red-300',
|
||||||
|
warning: 'bg-amber-500/10 text-amber-300',
|
||||||
|
success: 'bg-emerald-500/10 text-emerald-300',
|
||||||
|
neutral: 'bg-gray-700 text-gray-300'
|
||||||
|
}
|
||||||
|
const lightClasses: Record<ActivityTone, string> = {
|
||||||
|
danger: 'bg-red-50 text-red-700',
|
||||||
|
warning: 'bg-amber-50 text-amber-700',
|
||||||
|
success: 'bg-emerald-50 text-emerald-700',
|
||||||
|
neutral: 'bg-gray-100 text-gray-700'
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDarkMode.value ? darkClasses[tone] : lightClasses[tone]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityTitle = (action: string) => {
|
||||||
|
const key = `admin.dashboard.activityActions.${action}`
|
||||||
|
const title = t(key)
|
||||||
|
return title === key ? action : title
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActivityDescription = (activity: DashboardActivityViewItem) => {
|
||||||
|
const targetName = activity.targetNameValue || t('admin.dashboard.activityUnknownTarget')
|
||||||
|
if (activity.count > 1) {
|
||||||
|
return t('admin.dashboard.activityBatchDescription', {
|
||||||
|
target: targetName,
|
||||||
|
count: activity.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('admin.dashboard.activityDescription', {
|
||||||
|
target: targetName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const healthActionIconMap: Record<DashboardHealthAction['tone'], Component> = {
|
const healthActionIconMap: Record<DashboardHealthAction['tone'], Component> = {
|
||||||
danger: AlertTriangleIcon,
|
danger: AlertTriangleIcon,
|
||||||
warning: AlertTriangleIcon,
|
warning: AlertTriangleIcon,
|
||||||
|
|||||||
Reference in New Issue
Block a user