@wakastellar/ui 2.4.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/antivirus-dashboard/index.d.ts +44 -0
- package/dist/blocks/clamav-service-status/index.d.ts +35 -0
- package/dist/blocks/file-scan-uploader/index.d.ts +29 -0
- package/dist/blocks/index.d.ts +18 -9
- package/dist/blocks/quarantine-manager/index.d.ts +27 -0
- package/dist/blocks/scan-history-log/index.d.ts +28 -0
- package/dist/blocks/scan-policy-editor/index.d.ts +27 -0
- package/dist/blocks/scan-report-generator/index.d.ts +47 -0
- package/dist/blocks/signature-database-manager/index.d.ts +39 -0
- package/dist/blocks/threat-alert-banner/index.d.ts +26 -0
- package/dist/components/index.d.ts +4 -4
- package/dist/components/waka-signature-pad/index.d.ts +1 -1
- package/dist/exceljs.min-BcLLX0PC.js +29 -0
- package/dist/exceljs.min-KOayaaQ4.mjs +23013 -0
- package/dist/export.cjs.js +1 -1
- package/dist/export.d.ts +2 -2
- package/dist/export.es.js +1 -1
- package/dist/index.cjs.js +136 -136
- package/dist/index.es.js +29978 -27215
- package/dist/stories/Button.stories.d.ts +1 -1
- package/dist/stories/Header.stories.d.ts +1 -1
- package/dist/stories/Page.stories.d.ts +1 -1
- package/dist/useDataTableImport-COVnvslz.js +9 -0
- package/dist/useDataTableImport-DAlxBY8w.mjs +237 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/package.json +6 -5
- package/src/blocks/antivirus-dashboard/AntivirusDashboard.stories.tsx +291 -0
- package/src/blocks/antivirus-dashboard/index.tsx +525 -0
- package/src/blocks/clamav-service-status/ClamAVServiceStatus.stories.tsx +195 -0
- package/src/blocks/clamav-service-status/index.tsx +370 -0
- package/src/blocks/file-scan-uploader/FileScanUploader.stories.tsx +257 -0
- package/src/blocks/file-scan-uploader/index.tsx +311 -0
- package/src/blocks/index.ts +163 -11
- package/src/blocks/quarantine-manager/QuarantineManager.stories.tsx +209 -0
- package/src/blocks/quarantine-manager/index.tsx +435 -0
- package/src/blocks/scan-history-log/ScanHistoryLog.stories.tsx +231 -0
- package/src/blocks/scan-history-log/index.tsx +406 -0
- package/src/blocks/scan-policy-editor/ScanPolicyEditor.stories.tsx +106 -0
- package/src/blocks/scan-policy-editor/index.tsx +418 -0
- package/src/blocks/scan-report-generator/ScanReportGenerator.stories.tsx +232 -0
- package/src/blocks/scan-report-generator/index.tsx +612 -0
- package/src/blocks/sidebar/index.tsx +2 -1
- package/src/blocks/signature-database-manager/SignatureDatabaseManager.stories.tsx +279 -0
- package/src/blocks/signature-database-manager/index.tsx +470 -0
- package/src/blocks/theme-creator-block/index.tsx +16 -2
- package/src/blocks/threat-alert-banner/ThreatAlertBanner.stories.tsx +152 -0
- package/src/blocks/threat-alert-banner/index.tsx +320 -0
- package/src/components/DataTable/DataTable.stories.tsx +203 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +38 -31
- package/src/components/DataTable/hooks/useDataTableImport.ts +31 -20
- package/src/components/error-boundary/ErrorBoundary.stories.tsx +125 -0
- package/src/components/index.ts +45 -4
- package/src/components/language-selector/LanguageSelector.stories.tsx +112 -0
- package/src/components/theme-selector/ThemeSelector.stories.tsx +77 -0
- package/src/components/toaster/Toaster.stories.tsx +67 -0
- package/src/components/waka-activity-feed/WakaActivityFeed.stories.tsx +116 -0
- package/src/components/waka-ad-banner/WakaAdBanner.stories.tsx +102 -0
- package/src/components/waka-ad-fallback/WakaAdFallback.stories.tsx +117 -0
- package/src/components/waka-ad-inline/WakaAdInline.stories.tsx +105 -0
- package/src/components/waka-ad-interstitial/WakaAdInterstitial.stories.tsx +92 -0
- package/src/components/waka-ad-placeholder/WakaAdPlaceholder.stories.tsx +89 -0
- package/src/components/waka-ad-provider/WakaAdProvider.stories.tsx +110 -0
- package/src/components/waka-ad-sidebar/WakaAdSidebar.stories.tsx +89 -0
- package/src/components/waka-ad-sidebar/index.tsx +3 -2
- package/src/components/waka-ad-sticky-footer/WakaAdStickyFooter.stories.tsx +88 -0
- package/src/components/waka-address-autocomplete/WakaAddressAutocomplete.stories.tsx +46 -0
- package/src/components/waka-admincrumb/WakaAdmincrumb.stories.tsx +166 -0
- package/src/components/waka-alert-panel/WakaAlertPanel.stories.tsx +45 -0
- package/src/components/waka-alert-stack/WakaAlertStack.stories.tsx +62 -0
- package/src/components/waka-allocation-matrix/WakaAllocationMatrix.stories.tsx +68 -0
- package/src/components/waka-approval-chain/WakaApprovalChain.stories.tsx +63 -0
- package/src/components/waka-audit-log/WakaAuditLog.stories.tsx +73 -0
- package/src/components/waka-autocomplete/WakaAutocomplete.stories.tsx +132 -172
- package/src/components/waka-biometric-prompt/WakaBiometricPrompt.stories.tsx +48 -0
- package/src/components/waka-breadcrumb/WakaBreadcrumb.stories.tsx +74 -191
- package/src/components/waka-breadcrumb-path/WakaBreadcrumbPath.stories.tsx +40 -0
- package/src/components/waka-budget-burn/WakaBudgetBurn.stories.tsx +86 -0
- package/src/components/waka-capacity-planner/WakaCapacityPlanner.stories.tsx +273 -0
- package/src/components/waka-cart-summary/WakaCartSummary.stories.tsx +176 -0
- package/src/components/waka-cart-summary/index.tsx +19 -10
- package/src/components/waka-challenge-timer/WakaChallengeTimer.stories.tsx +98 -0
- package/src/components/waka-chat-bubble/WakaChatBubble.stories.tsx +118 -0
- package/src/components/waka-checklist/WakaChecklist.stories.tsx +71 -0
- package/src/components/waka-checkout-stepper/WakaCheckoutStepper.stories.tsx +102 -0
- package/src/components/waka-cohort-table/WakaCohortTable.stories.tsx +56 -0
- package/src/components/waka-color-picker/WakaColorPicker.stories.tsx +99 -155
- package/src/components/waka-combo-counter/WakaComboCounter.stories.tsx +128 -0
- package/src/components/waka-command-bar/WakaCommandBar.stories.tsx +45 -0
- package/src/components/waka-compare-period/WakaComparePeriod.stories.tsx +76 -0
- package/src/components/waka-config-comparator/WakaConfigComparator.stories.tsx +143 -0
- package/src/components/waka-connection-matrix/WakaConnectionMatrix.stories.tsx +52 -0
- package/src/components/waka-content-recommendation/WakaContentRecommendation.stories.tsx +41 -0
- package/src/components/waka-coupon-input/WakaCouponInput.stories.tsx +126 -0
- package/src/components/waka-credit-card-input/WakaCreditCardInput.stories.tsx +120 -0
- package/src/components/waka-datetime-picker.form-integration/WakaDateTimePickerForm.stories.tsx +79 -0
- package/src/components/waka-dependency-tree/WakaDependencyTree.stories.tsx +72 -0
- package/src/components/waka-device-trust/WakaDeviceTrust.stories.tsx +109 -0
- package/src/components/waka-empty-state/WakaEmptyState.stories.tsx +87 -0
- package/src/components/waka-feature-announcement/WakaFeatureAnnouncement.stories.tsx +47 -0
- package/src/components/waka-feature-flag-row/WakaFeatureFlagRow.stories.tsx +188 -0
- package/src/components/waka-file-upload/WakaFileUpload.stories.tsx +118 -174
- package/src/components/waka-floating-nav/WakaFloatingNav.stories.tsx +53 -0
- package/src/components/waka-goal-progress/WakaGoalProgress.stories.tsx +137 -0
- package/src/components/waka-hotspot/WakaHotspot.stories.tsx +56 -0
- package/src/components/waka-invoice-preview/WakaInvoicePreview.stories.tsx +169 -0
- package/src/components/waka-kpi-dashboard/WakaKpiDashboard.stories.tsx +46 -0
- package/src/components/waka-level-progress/WakaLevelProgress.stories.tsx +94 -75
- package/src/components/waka-liquid-button/WakaLiquidButton.stories.tsx +45 -0
- package/src/components/waka-magic-link/WakaMagicLink.stories.tsx +61 -0
- package/src/components/waka-magnetic-button/WakaMagneticButton.stories.tsx +40 -0
- package/src/components/waka-mention-input/WakaMentionInput.stories.tsx +140 -0
- package/src/components/waka-milestone-road/WakaMilestoneRoad.stories.tsx +143 -0
- package/src/components/waka-orbital-menu/WakaOrbitalMenu.stories.tsx +54 -0
- package/src/components/waka-order-tracker/WakaOrderTracker.stories.tsx +163 -0
- package/src/components/waka-outstream-video/WakaOutstreamVideo.stories.tsx +94 -0
- package/src/components/waka-pagination/WakaPagination.stories.tsx +110 -280
- package/src/components/waka-password-strength/WakaPasswordStrength.stories.tsx +132 -268
- package/src/components/waka-payment-method-picker/WakaPaymentMethodPicker.stories.tsx +141 -0
- package/src/components/waka-permission-matrix/WakaPermissionMatrix.stories.tsx +124 -0
- package/src/components/waka-phone-input/WakaPhoneInput.stories.tsx +56 -0
- package/src/components/waka-points-popup/WakaPointsPopup.stories.tsx +96 -0
- package/src/components/waka-power-up/WakaPowerUp.stories.tsx +121 -0
- package/src/components/waka-presence-indicator/WakaPresenceIndicator.stories.tsx +49 -0
- package/src/components/waka-pricing-table/WakaPricingTable.stories.tsx +159 -0
- package/src/components/waka-product-card/WakaProductCard.stories.tsx +202 -0
- package/src/components/waka-progress-onboarding/WakaProgressOnboarding.stories.tsx +57 -0
- package/src/components/waka-pull-to-refresh/WakaPullToRefresh.stories.tsx +51 -0
- package/src/components/waka-rank-badge/WakaRankBadge.stories.tsx +108 -0
- package/src/components/waka-rating-input/WakaRatingInput.stories.tsx +51 -0
- package/src/components/waka-reaction-picker/WakaReactionPicker.stories.tsx +52 -0
- package/src/components/waka-region-map/WakaRegionMap.stories.tsx +181 -0
- package/src/components/waka-resource-pool/WakaResourcePool.stories.tsx +70 -0
- package/src/components/waka-rich-text-editor/WakaRichTextEditor.stories.tsx +108 -197
- package/src/components/waka-rollback-slider/WakaRollbackSlider.stories.tsx +41 -0
- package/src/components/waka-schedule-picker/WakaSchedulePicker.stories.tsx +64 -0
- package/src/components/waka-season-pass/WakaSeasonPass.stories.tsx +107 -0
- package/src/components/waka-security-scan-result/WakaSecurityScanResult.stories.tsx +146 -0
- package/src/components/waka-security-score/WakaSecurityScore.stories.tsx +63 -0
- package/src/components/waka-session-manager/WakaSessionManager.stories.tsx +68 -0
- package/src/components/waka-signature-pad/WakaSignaturePad.stories.tsx +159 -0
- package/src/components/waka-signature-pad/index.tsx +5 -3
- package/src/components/waka-sla-tracker/WakaSlaTracker.stories.tsx +65 -0
- package/src/components/waka-slider-range/WakaSliderRange.stories.tsx +66 -0
- package/src/components/waka-sponsored-badge/WakaSponsoredBadge.stories.tsx +60 -0
- package/src/components/waka-sponsored-card/WakaSponsoredCard.stories.tsx +64 -0
- package/src/components/waka-sponsored-feed/WakaSponsoredFeed.stories.tsx +58 -0
- package/src/components/waka-spotlight/WakaSpotlight.stories.tsx +53 -0
- package/src/components/waka-stats-hexagon/WakaStatsHexagon.stories.tsx +161 -0
- package/src/components/waka-stepper/WakaStepper.stories.tsx +137 -410
- package/src/components/waka-swipe-card/WakaSwipeCard.stories.tsx +51 -0
- package/src/components/waka-tag-input/WakaTagInput.stories.tsx +224 -0
- package/src/components/waka-team-banner/WakaTeamBanner.stories.tsx +50 -0
- package/src/components/waka-theme-creator/WakaThemeCreator.stories.tsx +58 -0
- package/src/components/waka-theme-manager/WakaThemeManager.stories.tsx +298 -0
- package/src/components/waka-theme-manager/index.tsx +6 -11
- package/src/components/waka-thread-view/WakaThreadView.stories.tsx +143 -0
- package/src/components/waka-timeline/WakaTimeline.stories.tsx +171 -324
- package/src/components/waka-tooltip-tour/WakaTooltipTour.stories.tsx +92 -0
- package/src/components/waka-tour-guide/WakaTourGuide.stories.tsx +89 -0
- package/src/components/waka-treemap-chart/WakaTreemapChart.stories.tsx +234 -129
- package/src/components/waka-treemap-chart/index.tsx +2 -2
- package/src/components/waka-two-factor-setup/WakaTwoFactorSetup.stories.tsx +142 -0
- package/src/components/waka-typing-indicator/WakaTypingIndicator.stories.tsx +134 -0
- package/src/components/waka-video-ad/WakaVideoAd.stories.tsx +138 -0
- package/src/components/waka-video-call/WakaVideoCall.stories.tsx +186 -0
- package/src/components/waka-video-overlay/WakaVideoOverlay.stories.tsx +100 -0
- package/src/components/waka-voice-message/WakaVoiceMessage.stories.tsx +190 -0
- package/src/components/waka-welcome-modal/WakaWelcomeModal.stories.tsx +87 -0
- package/src/components/waka-xp-bar/WakaXPBar.stories.tsx +29 -29
- package/dist/useDataTableImport-D8R2HQl6.mjs +0 -229
- package/dist/useDataTableImport-S_hhA5Wo.js +0 -9
- package/src/components/DataTable/README.md +0 -446
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Badge } from "../../components/badge"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../components/card"
|
|
8
|
+
import { Progress } from "../../components/progress"
|
|
9
|
+
import { Separator } from "../../components/separator"
|
|
10
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/tabs"
|
|
11
|
+
import {
|
|
12
|
+
Shield,
|
|
13
|
+
ShieldAlert,
|
|
14
|
+
ShieldCheck,
|
|
15
|
+
ShieldX,
|
|
16
|
+
Activity,
|
|
17
|
+
AlertTriangle,
|
|
18
|
+
CheckCircle,
|
|
19
|
+
XCircle,
|
|
20
|
+
Clock,
|
|
21
|
+
FileText,
|
|
22
|
+
TrendingUp,
|
|
23
|
+
PlayCircle,
|
|
24
|
+
Archive,
|
|
25
|
+
AlertCircle,
|
|
26
|
+
} from "lucide-react"
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// TYPES
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
export type ServiceHealth = "healthy" | "degraded" | "down"
|
|
33
|
+
export type ScanResult = "clean" | "infected" | "error"
|
|
34
|
+
export type ThreatSeverity = "critical" | "high" | "medium" | "low"
|
|
35
|
+
|
|
36
|
+
export interface ScanEntry {
|
|
37
|
+
id: string
|
|
38
|
+
filename: string
|
|
39
|
+
result: ScanResult
|
|
40
|
+
threatName?: string
|
|
41
|
+
scanDate: Date
|
|
42
|
+
duration: number // ms
|
|
43
|
+
fileSize: number
|
|
44
|
+
userId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ThreatInfo {
|
|
48
|
+
name: string
|
|
49
|
+
count: number
|
|
50
|
+
severity: ThreatSeverity
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface WeeklyStat {
|
|
54
|
+
day: string
|
|
55
|
+
scans: number
|
|
56
|
+
threats: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AntivirusDashboardProps {
|
|
60
|
+
serviceHealth?: ServiceHealth
|
|
61
|
+
clamavVersion?: string
|
|
62
|
+
signatureCount?: number
|
|
63
|
+
lastSignatureUpdate?: Date
|
|
64
|
+
scansToday?: number
|
|
65
|
+
threatsDetected?: number
|
|
66
|
+
quarantinedFiles?: number
|
|
67
|
+
detectionRate?: number
|
|
68
|
+
recentScans?: ScanEntry[]
|
|
69
|
+
weeklyStats?: WeeklyStat[]
|
|
70
|
+
topThreats?: ThreatInfo[]
|
|
71
|
+
onStartScan?: () => void
|
|
72
|
+
onViewQuarantine?: () => void
|
|
73
|
+
className?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// CONFIG & HELPERS
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
const serviceHealthConfig: Record<ServiceHealth, { color: string; icon: React.ReactNode; label: string }> = {
|
|
81
|
+
healthy: { color: "text-green-500 bg-green-500/10", icon: <ShieldCheck className="h-4 w-4" />, label: "Healthy" },
|
|
82
|
+
degraded: { color: "text-yellow-500 bg-yellow-500/10", icon: <ShieldAlert className="h-4 w-4" />, label: "Degraded" },
|
|
83
|
+
down: { color: "text-red-500 bg-red-500/10", icon: <ShieldX className="h-4 w-4" />, label: "Down" },
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const scanResultConfig: Record<ScanResult, { color: string; icon: React.ReactNode; label: string; bgColor: string }> = {
|
|
87
|
+
clean: { color: "text-green-500", icon: <CheckCircle className="h-4 w-4" />, label: "Clean", bgColor: "bg-green-500" },
|
|
88
|
+
infected: { color: "text-red-500", icon: <XCircle className="h-4 w-4" />, label: "Infected", bgColor: "bg-red-500" },
|
|
89
|
+
error: { color: "text-orange-500", icon: <AlertCircle className="h-4 w-4" />, label: "Error", bgColor: "bg-orange-500" },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const threatSeverityConfig: Record<ThreatSeverity, { color: string; bgColor: string }> = {
|
|
93
|
+
critical: { color: "text-red-500", bgColor: "bg-red-500" },
|
|
94
|
+
high: { color: "text-orange-500", bgColor: "bg-orange-500" },
|
|
95
|
+
medium: { color: "text-yellow-500", bgColor: "bg-yellow-500" },
|
|
96
|
+
low: { color: "text-blue-500", bgColor: "bg-blue-500" },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatFileSize(bytes: number): string {
|
|
100
|
+
if (bytes < 1024) return `${bytes} B`
|
|
101
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
102
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatDuration(ms: number): string {
|
|
106
|
+
if (ms < 1000) return `${ms}ms`
|
|
107
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
108
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatRelativeTime(date: Date): string {
|
|
112
|
+
const now = new Date()
|
|
113
|
+
const diff = now.getTime() - date.getTime()
|
|
114
|
+
const minutes = Math.floor(diff / 60000)
|
|
115
|
+
const hours = Math.floor(minutes / 60)
|
|
116
|
+
const days = Math.floor(hours / 24)
|
|
117
|
+
|
|
118
|
+
if (minutes < 1) return "just now"
|
|
119
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
120
|
+
if (hours < 24) return `${hours}h ago`
|
|
121
|
+
return `${days}d ago`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// SUBCOMPONENTS
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
function ServiceHealthBadge({ health }: { health: ServiceHealth }) {
|
|
129
|
+
const config = serviceHealthConfig[health]
|
|
130
|
+
return (
|
|
131
|
+
<Badge variant="outline" className={cn("gap-1", config.color)}>
|
|
132
|
+
{config.icon}
|
|
133
|
+
{config.label}
|
|
134
|
+
</Badge>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ScanResultBadge({ result }: { result: ScanResult }) {
|
|
139
|
+
const config = scanResultConfig[result]
|
|
140
|
+
return (
|
|
141
|
+
<Badge variant="outline" className={cn("gap-1", config.color)}>
|
|
142
|
+
{config.icon}
|
|
143
|
+
{config.label}
|
|
144
|
+
</Badge>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function KPICard({
|
|
149
|
+
title,
|
|
150
|
+
value,
|
|
151
|
+
icon,
|
|
152
|
+
trend,
|
|
153
|
+
color = "text-primary"
|
|
154
|
+
}: {
|
|
155
|
+
title: string
|
|
156
|
+
value: string | number
|
|
157
|
+
icon: React.ReactNode
|
|
158
|
+
trend?: string
|
|
159
|
+
color?: string
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<Card>
|
|
163
|
+
<CardContent className="p-6">
|
|
164
|
+
<div className="flex items-center justify-between">
|
|
165
|
+
<div>
|
|
166
|
+
<p className="text-sm text-muted-foreground mb-1">{title}</p>
|
|
167
|
+
<div className="flex items-baseline gap-2">
|
|
168
|
+
<p className={cn("text-3xl font-bold", color)}>{value}</p>
|
|
169
|
+
{trend && (
|
|
170
|
+
<span className="text-xs text-muted-foreground">{trend}</span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div className={cn("p-3 rounded-lg", `${color.replace("text-", "bg-")}/10`)}>
|
|
175
|
+
{icon}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</CardContent>
|
|
179
|
+
</Card>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function WeeklyChart({ stats }: { stats: WeeklyStat[] }) {
|
|
184
|
+
const maxValue = Math.max(...stats.map(s => Math.max(s.scans, s.threats)), 1)
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Card>
|
|
188
|
+
<CardHeader>
|
|
189
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
190
|
+
<TrendingUp className="h-4 w-4" />
|
|
191
|
+
Weekly Activity
|
|
192
|
+
</CardTitle>
|
|
193
|
+
</CardHeader>
|
|
194
|
+
<CardContent>
|
|
195
|
+
<div className="space-y-4">
|
|
196
|
+
{stats.map((stat, index) => {
|
|
197
|
+
const scansPercent = (stat.scans / maxValue) * 100
|
|
198
|
+
const threatsPercent = (stat.threats / maxValue) * 100
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div key={stat.day} className="space-y-2">
|
|
202
|
+
<div className="flex items-center justify-between text-sm">
|
|
203
|
+
<span className="text-muted-foreground min-w-12">{stat.day}</span>
|
|
204
|
+
<div className="flex items-center gap-4 text-xs">
|
|
205
|
+
<span className="flex items-center gap-1">
|
|
206
|
+
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
|
207
|
+
{stat.scans} scans
|
|
208
|
+
</span>
|
|
209
|
+
<span className="flex items-center gap-1">
|
|
210
|
+
<div className="w-2 h-2 rounded-full bg-red-500" />
|
|
211
|
+
{stat.threats} threats
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="space-y-1">
|
|
216
|
+
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
|
|
217
|
+
<div
|
|
218
|
+
className="h-full bg-blue-500 rounded-full transition-all"
|
|
219
|
+
style={{ width: `${scansPercent}%` }}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
{stat.threats > 0 && (
|
|
223
|
+
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
|
|
224
|
+
<div
|
|
225
|
+
className="h-full bg-red-500 rounded-full transition-all"
|
|
226
|
+
style={{ width: `${threatsPercent}%` }}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
})}
|
|
234
|
+
</div>
|
|
235
|
+
</CardContent>
|
|
236
|
+
</Card>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function RecentScansTable({ scans }: { scans: ScanEntry[] }) {
|
|
241
|
+
return (
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
245
|
+
<Activity className="h-4 w-4" />
|
|
246
|
+
Recent Scans
|
|
247
|
+
</CardTitle>
|
|
248
|
+
</CardHeader>
|
|
249
|
+
<CardContent>
|
|
250
|
+
<div className="space-y-3">
|
|
251
|
+
{scans.length === 0 ? (
|
|
252
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
253
|
+
<Shield className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
254
|
+
<p className="text-sm">No scans yet</p>
|
|
255
|
+
</div>
|
|
256
|
+
) : (
|
|
257
|
+
scans.map((scan) => (
|
|
258
|
+
<div
|
|
259
|
+
key={scan.id}
|
|
260
|
+
className={cn(
|
|
261
|
+
"flex items-center justify-between p-3 rounded-lg border transition-colors",
|
|
262
|
+
scan.result === "infected" && "border-red-500/50 bg-red-500/5"
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
266
|
+
<div className={cn("p-2 rounded-lg shrink-0", scanResultConfig[scan.result].color.replace("text-", "bg-") + "/10")}>
|
|
267
|
+
{scanResultConfig[scan.result].icon}
|
|
268
|
+
</div>
|
|
269
|
+
<div className="flex-1 min-w-0">
|
|
270
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
271
|
+
<p className="text-sm font-medium truncate">{scan.filename}</p>
|
|
272
|
+
<ScanResultBadge result={scan.result} />
|
|
273
|
+
</div>
|
|
274
|
+
{scan.threatName && (
|
|
275
|
+
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
|
|
276
|
+
<AlertTriangle className="h-3 w-3" />
|
|
277
|
+
{scan.threatName}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
281
|
+
<span className="flex items-center gap-1">
|
|
282
|
+
<Clock className="h-3 w-3" />
|
|
283
|
+
{formatRelativeTime(scan.scanDate)}
|
|
284
|
+
</span>
|
|
285
|
+
<span>{formatDuration(scan.duration)}</span>
|
|
286
|
+
<span>{formatFileSize(scan.fileSize)}</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
))
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</CardContent>
|
|
295
|
+
</Card>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function TopThreatsTable({ threats }: { threats: ThreatInfo[] }) {
|
|
300
|
+
return (
|
|
301
|
+
<Card>
|
|
302
|
+
<CardHeader>
|
|
303
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
304
|
+
<AlertTriangle className="h-4 w-4" />
|
|
305
|
+
Top Threats
|
|
306
|
+
</CardTitle>
|
|
307
|
+
</CardHeader>
|
|
308
|
+
<CardContent>
|
|
309
|
+
<div className="space-y-3">
|
|
310
|
+
{threats.length === 0 ? (
|
|
311
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
312
|
+
<ShieldCheck className="h-12 w-12 mx-auto mb-3 text-green-500 opacity-50" />
|
|
313
|
+
<p className="text-sm">No threats detected</p>
|
|
314
|
+
</div>
|
|
315
|
+
) : (
|
|
316
|
+
threats.map((threat, index) => {
|
|
317
|
+
const sevConfig = threatSeverityConfig[threat.severity]
|
|
318
|
+
return (
|
|
319
|
+
<div key={threat.name} className="flex items-center gap-3">
|
|
320
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-muted text-sm font-medium">
|
|
321
|
+
{index + 1}
|
|
322
|
+
</div>
|
|
323
|
+
<div className="flex-1 min-w-0">
|
|
324
|
+
<div className="flex items-center gap-2">
|
|
325
|
+
<p className="text-sm font-medium truncate">{threat.name}</p>
|
|
326
|
+
<Badge
|
|
327
|
+
variant="outline"
|
|
328
|
+
className={cn("text-xs capitalize", sevConfig.color)}
|
|
329
|
+
>
|
|
330
|
+
{threat.severity}
|
|
331
|
+
</Badge>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="flex items-center gap-2 mt-1">
|
|
334
|
+
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
|
335
|
+
<div
|
|
336
|
+
className={cn("h-full rounded-full", sevConfig.bgColor)}
|
|
337
|
+
style={{ width: `${Math.min((threat.count / Math.max(...threats.map(t => t.count))) * 100, 100)}%` }}
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
<span className="text-xs text-muted-foreground font-medium min-w-12 text-right">
|
|
341
|
+
{threat.count} {threat.count === 1 ? 'time' : 'times'}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
</CardContent>
|
|
351
|
+
</Card>
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ============================================
|
|
356
|
+
// MAIN COMPONENT
|
|
357
|
+
// ============================================
|
|
358
|
+
|
|
359
|
+
export function AntivirusDashboard({
|
|
360
|
+
serviceHealth = "healthy",
|
|
361
|
+
clamavVersion = "1.0.3",
|
|
362
|
+
signatureCount = 8642758,
|
|
363
|
+
lastSignatureUpdate,
|
|
364
|
+
scansToday = 0,
|
|
365
|
+
threatsDetected = 0,
|
|
366
|
+
quarantinedFiles = 0,
|
|
367
|
+
detectionRate = 0,
|
|
368
|
+
recentScans = [],
|
|
369
|
+
weeklyStats = [],
|
|
370
|
+
topThreats = [],
|
|
371
|
+
onStartScan,
|
|
372
|
+
onViewQuarantine,
|
|
373
|
+
className,
|
|
374
|
+
}: AntivirusDashboardProps) {
|
|
375
|
+
return (
|
|
376
|
+
<div className={cn("space-y-6", className)}>
|
|
377
|
+
{/* Header */}
|
|
378
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start">
|
|
379
|
+
<div>
|
|
380
|
+
<div className="flex items-center gap-3">
|
|
381
|
+
<div className="p-2 rounded-lg bg-primary/10">
|
|
382
|
+
<Shield className="h-6 w-6 text-primary" />
|
|
383
|
+
</div>
|
|
384
|
+
<div>
|
|
385
|
+
<h1 className="text-2xl font-bold">Antivirus ClamAV</h1>
|
|
386
|
+
<p className="text-sm text-muted-foreground">
|
|
387
|
+
Version {clamavVersion} • {signatureCount.toLocaleString()} signatures
|
|
388
|
+
{lastSignatureUpdate && (
|
|
389
|
+
<> • Updated {formatRelativeTime(lastSignatureUpdate)}</>
|
|
390
|
+
)}
|
|
391
|
+
</p>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="flex items-center gap-2">
|
|
396
|
+
<ServiceHealthBadge health={serviceHealth} />
|
|
397
|
+
{onViewQuarantine && (
|
|
398
|
+
<Button variant="outline" onClick={onViewQuarantine}>
|
|
399
|
+
<Archive className="h-4 w-4 mr-2" />
|
|
400
|
+
Quarantine
|
|
401
|
+
</Button>
|
|
402
|
+
)}
|
|
403
|
+
{onStartScan && (
|
|
404
|
+
<Button onClick={onStartScan}>
|
|
405
|
+
<PlayCircle className="h-4 w-4 mr-2" />
|
|
406
|
+
Start Scan
|
|
407
|
+
</Button>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
{/* KPI Cards */}
|
|
413
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
414
|
+
<KPICard
|
|
415
|
+
title="Scans Today"
|
|
416
|
+
value={scansToday.toLocaleString()}
|
|
417
|
+
icon={<Activity className="h-6 w-6" />}
|
|
418
|
+
color="text-blue-500"
|
|
419
|
+
/>
|
|
420
|
+
<KPICard
|
|
421
|
+
title="Threats Detected"
|
|
422
|
+
value={threatsDetected.toLocaleString()}
|
|
423
|
+
icon={<AlertTriangle className="h-6 w-6" />}
|
|
424
|
+
color="text-red-500"
|
|
425
|
+
/>
|
|
426
|
+
<KPICard
|
|
427
|
+
title="Quarantined Files"
|
|
428
|
+
value={quarantinedFiles.toLocaleString()}
|
|
429
|
+
icon={<Archive className="h-6 w-6" />}
|
|
430
|
+
color="text-orange-500"
|
|
431
|
+
/>
|
|
432
|
+
<KPICard
|
|
433
|
+
title="Detection Rate"
|
|
434
|
+
value={`${detectionRate.toFixed(1)}%`}
|
|
435
|
+
icon={<ShieldCheck className="h-6 w-6" />}
|
|
436
|
+
color="text-green-500"
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{/* Charts and Stats */}
|
|
441
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
442
|
+
<WeeklyChart stats={weeklyStats} />
|
|
443
|
+
<TopThreatsTable threats={topThreats} />
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
{/* Recent Scans */}
|
|
447
|
+
<RecentScansTable scans={recentScans} />
|
|
448
|
+
</div>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================
|
|
453
|
+
// PRESET DATA
|
|
454
|
+
// ============================================
|
|
455
|
+
|
|
456
|
+
export const defaultRecentScans: ScanEntry[] = [
|
|
457
|
+
{
|
|
458
|
+
id: "scan1",
|
|
459
|
+
filename: "document.pdf",
|
|
460
|
+
result: "clean",
|
|
461
|
+
scanDate: new Date(Date.now() - 5 * 60000),
|
|
462
|
+
duration: 234,
|
|
463
|
+
fileSize: 2048576,
|
|
464
|
+
userId: "user123",
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: "scan2",
|
|
468
|
+
filename: "malicious_script.js",
|
|
469
|
+
result: "infected",
|
|
470
|
+
threatName: "Trojan.GenericKD.67890123",
|
|
471
|
+
scanDate: new Date(Date.now() - 15 * 60000),
|
|
472
|
+
duration: 156,
|
|
473
|
+
fileSize: 45678,
|
|
474
|
+
userId: "user456",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
id: "scan3",
|
|
478
|
+
filename: "eicar_test.txt",
|
|
479
|
+
result: "infected",
|
|
480
|
+
threatName: "Eicar-Test-Signature",
|
|
481
|
+
scanDate: new Date(Date.now() - 30 * 60000),
|
|
482
|
+
duration: 89,
|
|
483
|
+
fileSize: 68,
|
|
484
|
+
userId: "admin",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "scan4",
|
|
488
|
+
filename: "report_2024.xlsx",
|
|
489
|
+
result: "clean",
|
|
490
|
+
scanDate: new Date(Date.now() - 45 * 60000),
|
|
491
|
+
duration: 567,
|
|
492
|
+
fileSize: 5242880,
|
|
493
|
+
userId: "user789",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "scan5",
|
|
497
|
+
filename: "suspicious_archive.zip",
|
|
498
|
+
result: "infected",
|
|
499
|
+
threatName: "Win.Trojan.Genome-9999999",
|
|
500
|
+
scanDate: new Date(Date.now() - 60 * 60000),
|
|
501
|
+
duration: 1234,
|
|
502
|
+
fileSize: 10485760,
|
|
503
|
+
userId: "user321",
|
|
504
|
+
},
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
export const defaultWeeklyStats: WeeklyStat[] = [
|
|
508
|
+
{ day: "Mon", scans: 145, threats: 3 },
|
|
509
|
+
{ day: "Tue", scans: 198, threats: 5 },
|
|
510
|
+
{ day: "Wed", scans: 167, threats: 2 },
|
|
511
|
+
{ day: "Thu", scans: 223, threats: 8 },
|
|
512
|
+
{ day: "Fri", scans: 189, threats: 4 },
|
|
513
|
+
{ day: "Sat", scans: 98, threats: 1 },
|
|
514
|
+
{ day: "Sun", scans: 76, threats: 0 },
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
export const defaultTopThreats: ThreatInfo[] = [
|
|
518
|
+
{ name: "Eicar-Test-Signature", count: 12, severity: "low" },
|
|
519
|
+
{ name: "Trojan.GenericKD.67890123", count: 8, severity: "high" },
|
|
520
|
+
{ name: "Win.Trojan.Genome-9999999", count: 5, severity: "critical" },
|
|
521
|
+
{ name: "JS.Downloader.Nemucod", count: 4, severity: "medium" },
|
|
522
|
+
{ name: "PHP.Webshell.Generic", count: 3, severity: "high" },
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
export default AntivirusDashboard
|