@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.
Files changed (173) hide show
  1. package/dist/blocks/antivirus-dashboard/index.d.ts +44 -0
  2. package/dist/blocks/clamav-service-status/index.d.ts +35 -0
  3. package/dist/blocks/file-scan-uploader/index.d.ts +29 -0
  4. package/dist/blocks/index.d.ts +18 -9
  5. package/dist/blocks/quarantine-manager/index.d.ts +27 -0
  6. package/dist/blocks/scan-history-log/index.d.ts +28 -0
  7. package/dist/blocks/scan-policy-editor/index.d.ts +27 -0
  8. package/dist/blocks/scan-report-generator/index.d.ts +47 -0
  9. package/dist/blocks/signature-database-manager/index.d.ts +39 -0
  10. package/dist/blocks/threat-alert-banner/index.d.ts +26 -0
  11. package/dist/components/index.d.ts +4 -4
  12. package/dist/components/waka-signature-pad/index.d.ts +1 -1
  13. package/dist/exceljs.min-BcLLX0PC.js +29 -0
  14. package/dist/exceljs.min-KOayaaQ4.mjs +23013 -0
  15. package/dist/export.cjs.js +1 -1
  16. package/dist/export.d.ts +2 -2
  17. package/dist/export.es.js +1 -1
  18. package/dist/index.cjs.js +136 -136
  19. package/dist/index.es.js +29978 -27215
  20. package/dist/stories/Button.stories.d.ts +1 -1
  21. package/dist/stories/Header.stories.d.ts +1 -1
  22. package/dist/stories/Page.stories.d.ts +1 -1
  23. package/dist/useDataTableImport-COVnvslz.js +9 -0
  24. package/dist/useDataTableImport-DAlxBY8w.mjs +237 -0
  25. package/dist/utils/index.d.ts +1 -0
  26. package/dist/utils/logger.d.ts +9 -0
  27. package/package.json +6 -5
  28. package/src/blocks/antivirus-dashboard/AntivirusDashboard.stories.tsx +291 -0
  29. package/src/blocks/antivirus-dashboard/index.tsx +525 -0
  30. package/src/blocks/clamav-service-status/ClamAVServiceStatus.stories.tsx +195 -0
  31. package/src/blocks/clamav-service-status/index.tsx +370 -0
  32. package/src/blocks/file-scan-uploader/FileScanUploader.stories.tsx +257 -0
  33. package/src/blocks/file-scan-uploader/index.tsx +311 -0
  34. package/src/blocks/index.ts +163 -11
  35. package/src/blocks/quarantine-manager/QuarantineManager.stories.tsx +209 -0
  36. package/src/blocks/quarantine-manager/index.tsx +435 -0
  37. package/src/blocks/scan-history-log/ScanHistoryLog.stories.tsx +231 -0
  38. package/src/blocks/scan-history-log/index.tsx +406 -0
  39. package/src/blocks/scan-policy-editor/ScanPolicyEditor.stories.tsx +106 -0
  40. package/src/blocks/scan-policy-editor/index.tsx +418 -0
  41. package/src/blocks/scan-report-generator/ScanReportGenerator.stories.tsx +232 -0
  42. package/src/blocks/scan-report-generator/index.tsx +612 -0
  43. package/src/blocks/sidebar/index.tsx +2 -1
  44. package/src/blocks/signature-database-manager/SignatureDatabaseManager.stories.tsx +279 -0
  45. package/src/blocks/signature-database-manager/index.tsx +470 -0
  46. package/src/blocks/theme-creator-block/index.tsx +16 -2
  47. package/src/blocks/threat-alert-banner/ThreatAlertBanner.stories.tsx +152 -0
  48. package/src/blocks/threat-alert-banner/index.tsx +320 -0
  49. package/src/components/DataTable/DataTable.stories.tsx +203 -0
  50. package/src/components/DataTable/hooks/useDataTableExport.ts +38 -31
  51. package/src/components/DataTable/hooks/useDataTableImport.ts +31 -20
  52. package/src/components/error-boundary/ErrorBoundary.stories.tsx +125 -0
  53. package/src/components/index.ts +45 -4
  54. package/src/components/language-selector/LanguageSelector.stories.tsx +112 -0
  55. package/src/components/theme-selector/ThemeSelector.stories.tsx +77 -0
  56. package/src/components/toaster/Toaster.stories.tsx +67 -0
  57. package/src/components/waka-activity-feed/WakaActivityFeed.stories.tsx +116 -0
  58. package/src/components/waka-ad-banner/WakaAdBanner.stories.tsx +102 -0
  59. package/src/components/waka-ad-fallback/WakaAdFallback.stories.tsx +117 -0
  60. package/src/components/waka-ad-inline/WakaAdInline.stories.tsx +105 -0
  61. package/src/components/waka-ad-interstitial/WakaAdInterstitial.stories.tsx +92 -0
  62. package/src/components/waka-ad-placeholder/WakaAdPlaceholder.stories.tsx +89 -0
  63. package/src/components/waka-ad-provider/WakaAdProvider.stories.tsx +110 -0
  64. package/src/components/waka-ad-sidebar/WakaAdSidebar.stories.tsx +89 -0
  65. package/src/components/waka-ad-sidebar/index.tsx +3 -2
  66. package/src/components/waka-ad-sticky-footer/WakaAdStickyFooter.stories.tsx +88 -0
  67. package/src/components/waka-address-autocomplete/WakaAddressAutocomplete.stories.tsx +46 -0
  68. package/src/components/waka-admincrumb/WakaAdmincrumb.stories.tsx +166 -0
  69. package/src/components/waka-alert-panel/WakaAlertPanel.stories.tsx +45 -0
  70. package/src/components/waka-alert-stack/WakaAlertStack.stories.tsx +62 -0
  71. package/src/components/waka-allocation-matrix/WakaAllocationMatrix.stories.tsx +68 -0
  72. package/src/components/waka-approval-chain/WakaApprovalChain.stories.tsx +63 -0
  73. package/src/components/waka-audit-log/WakaAuditLog.stories.tsx +73 -0
  74. package/src/components/waka-autocomplete/WakaAutocomplete.stories.tsx +132 -172
  75. package/src/components/waka-biometric-prompt/WakaBiometricPrompt.stories.tsx +48 -0
  76. package/src/components/waka-breadcrumb/WakaBreadcrumb.stories.tsx +74 -191
  77. package/src/components/waka-breadcrumb-path/WakaBreadcrumbPath.stories.tsx +40 -0
  78. package/src/components/waka-budget-burn/WakaBudgetBurn.stories.tsx +86 -0
  79. package/src/components/waka-capacity-planner/WakaCapacityPlanner.stories.tsx +273 -0
  80. package/src/components/waka-cart-summary/WakaCartSummary.stories.tsx +176 -0
  81. package/src/components/waka-cart-summary/index.tsx +19 -10
  82. package/src/components/waka-challenge-timer/WakaChallengeTimer.stories.tsx +98 -0
  83. package/src/components/waka-chat-bubble/WakaChatBubble.stories.tsx +118 -0
  84. package/src/components/waka-checklist/WakaChecklist.stories.tsx +71 -0
  85. package/src/components/waka-checkout-stepper/WakaCheckoutStepper.stories.tsx +102 -0
  86. package/src/components/waka-cohort-table/WakaCohortTable.stories.tsx +56 -0
  87. package/src/components/waka-color-picker/WakaColorPicker.stories.tsx +99 -155
  88. package/src/components/waka-combo-counter/WakaComboCounter.stories.tsx +128 -0
  89. package/src/components/waka-command-bar/WakaCommandBar.stories.tsx +45 -0
  90. package/src/components/waka-compare-period/WakaComparePeriod.stories.tsx +76 -0
  91. package/src/components/waka-config-comparator/WakaConfigComparator.stories.tsx +143 -0
  92. package/src/components/waka-connection-matrix/WakaConnectionMatrix.stories.tsx +52 -0
  93. package/src/components/waka-content-recommendation/WakaContentRecommendation.stories.tsx +41 -0
  94. package/src/components/waka-coupon-input/WakaCouponInput.stories.tsx +126 -0
  95. package/src/components/waka-credit-card-input/WakaCreditCardInput.stories.tsx +120 -0
  96. package/src/components/waka-datetime-picker.form-integration/WakaDateTimePickerForm.stories.tsx +79 -0
  97. package/src/components/waka-dependency-tree/WakaDependencyTree.stories.tsx +72 -0
  98. package/src/components/waka-device-trust/WakaDeviceTrust.stories.tsx +109 -0
  99. package/src/components/waka-empty-state/WakaEmptyState.stories.tsx +87 -0
  100. package/src/components/waka-feature-announcement/WakaFeatureAnnouncement.stories.tsx +47 -0
  101. package/src/components/waka-feature-flag-row/WakaFeatureFlagRow.stories.tsx +188 -0
  102. package/src/components/waka-file-upload/WakaFileUpload.stories.tsx +118 -174
  103. package/src/components/waka-floating-nav/WakaFloatingNav.stories.tsx +53 -0
  104. package/src/components/waka-goal-progress/WakaGoalProgress.stories.tsx +137 -0
  105. package/src/components/waka-hotspot/WakaHotspot.stories.tsx +56 -0
  106. package/src/components/waka-invoice-preview/WakaInvoicePreview.stories.tsx +169 -0
  107. package/src/components/waka-kpi-dashboard/WakaKpiDashboard.stories.tsx +46 -0
  108. package/src/components/waka-level-progress/WakaLevelProgress.stories.tsx +94 -75
  109. package/src/components/waka-liquid-button/WakaLiquidButton.stories.tsx +45 -0
  110. package/src/components/waka-magic-link/WakaMagicLink.stories.tsx +61 -0
  111. package/src/components/waka-magnetic-button/WakaMagneticButton.stories.tsx +40 -0
  112. package/src/components/waka-mention-input/WakaMentionInput.stories.tsx +140 -0
  113. package/src/components/waka-milestone-road/WakaMilestoneRoad.stories.tsx +143 -0
  114. package/src/components/waka-orbital-menu/WakaOrbitalMenu.stories.tsx +54 -0
  115. package/src/components/waka-order-tracker/WakaOrderTracker.stories.tsx +163 -0
  116. package/src/components/waka-outstream-video/WakaOutstreamVideo.stories.tsx +94 -0
  117. package/src/components/waka-pagination/WakaPagination.stories.tsx +110 -280
  118. package/src/components/waka-password-strength/WakaPasswordStrength.stories.tsx +132 -268
  119. package/src/components/waka-payment-method-picker/WakaPaymentMethodPicker.stories.tsx +141 -0
  120. package/src/components/waka-permission-matrix/WakaPermissionMatrix.stories.tsx +124 -0
  121. package/src/components/waka-phone-input/WakaPhoneInput.stories.tsx +56 -0
  122. package/src/components/waka-points-popup/WakaPointsPopup.stories.tsx +96 -0
  123. package/src/components/waka-power-up/WakaPowerUp.stories.tsx +121 -0
  124. package/src/components/waka-presence-indicator/WakaPresenceIndicator.stories.tsx +49 -0
  125. package/src/components/waka-pricing-table/WakaPricingTable.stories.tsx +159 -0
  126. package/src/components/waka-product-card/WakaProductCard.stories.tsx +202 -0
  127. package/src/components/waka-progress-onboarding/WakaProgressOnboarding.stories.tsx +57 -0
  128. package/src/components/waka-pull-to-refresh/WakaPullToRefresh.stories.tsx +51 -0
  129. package/src/components/waka-rank-badge/WakaRankBadge.stories.tsx +108 -0
  130. package/src/components/waka-rating-input/WakaRatingInput.stories.tsx +51 -0
  131. package/src/components/waka-reaction-picker/WakaReactionPicker.stories.tsx +52 -0
  132. package/src/components/waka-region-map/WakaRegionMap.stories.tsx +181 -0
  133. package/src/components/waka-resource-pool/WakaResourcePool.stories.tsx +70 -0
  134. package/src/components/waka-rich-text-editor/WakaRichTextEditor.stories.tsx +108 -197
  135. package/src/components/waka-rollback-slider/WakaRollbackSlider.stories.tsx +41 -0
  136. package/src/components/waka-schedule-picker/WakaSchedulePicker.stories.tsx +64 -0
  137. package/src/components/waka-season-pass/WakaSeasonPass.stories.tsx +107 -0
  138. package/src/components/waka-security-scan-result/WakaSecurityScanResult.stories.tsx +146 -0
  139. package/src/components/waka-security-score/WakaSecurityScore.stories.tsx +63 -0
  140. package/src/components/waka-session-manager/WakaSessionManager.stories.tsx +68 -0
  141. package/src/components/waka-signature-pad/WakaSignaturePad.stories.tsx +159 -0
  142. package/src/components/waka-signature-pad/index.tsx +5 -3
  143. package/src/components/waka-sla-tracker/WakaSlaTracker.stories.tsx +65 -0
  144. package/src/components/waka-slider-range/WakaSliderRange.stories.tsx +66 -0
  145. package/src/components/waka-sponsored-badge/WakaSponsoredBadge.stories.tsx +60 -0
  146. package/src/components/waka-sponsored-card/WakaSponsoredCard.stories.tsx +64 -0
  147. package/src/components/waka-sponsored-feed/WakaSponsoredFeed.stories.tsx +58 -0
  148. package/src/components/waka-spotlight/WakaSpotlight.stories.tsx +53 -0
  149. package/src/components/waka-stats-hexagon/WakaStatsHexagon.stories.tsx +161 -0
  150. package/src/components/waka-stepper/WakaStepper.stories.tsx +137 -410
  151. package/src/components/waka-swipe-card/WakaSwipeCard.stories.tsx +51 -0
  152. package/src/components/waka-tag-input/WakaTagInput.stories.tsx +224 -0
  153. package/src/components/waka-team-banner/WakaTeamBanner.stories.tsx +50 -0
  154. package/src/components/waka-theme-creator/WakaThemeCreator.stories.tsx +58 -0
  155. package/src/components/waka-theme-manager/WakaThemeManager.stories.tsx +298 -0
  156. package/src/components/waka-theme-manager/index.tsx +6 -11
  157. package/src/components/waka-thread-view/WakaThreadView.stories.tsx +143 -0
  158. package/src/components/waka-timeline/WakaTimeline.stories.tsx +171 -324
  159. package/src/components/waka-tooltip-tour/WakaTooltipTour.stories.tsx +92 -0
  160. package/src/components/waka-tour-guide/WakaTourGuide.stories.tsx +89 -0
  161. package/src/components/waka-treemap-chart/WakaTreemapChart.stories.tsx +234 -129
  162. package/src/components/waka-treemap-chart/index.tsx +2 -2
  163. package/src/components/waka-two-factor-setup/WakaTwoFactorSetup.stories.tsx +142 -0
  164. package/src/components/waka-typing-indicator/WakaTypingIndicator.stories.tsx +134 -0
  165. package/src/components/waka-video-ad/WakaVideoAd.stories.tsx +138 -0
  166. package/src/components/waka-video-call/WakaVideoCall.stories.tsx +186 -0
  167. package/src/components/waka-video-overlay/WakaVideoOverlay.stories.tsx +100 -0
  168. package/src/components/waka-voice-message/WakaVoiceMessage.stories.tsx +190 -0
  169. package/src/components/waka-welcome-modal/WakaWelcomeModal.stories.tsx +87 -0
  170. package/src/components/waka-xp-bar/WakaXPBar.stories.tsx +29 -29
  171. package/dist/useDataTableImport-D8R2HQl6.mjs +0 -229
  172. package/dist/useDataTableImport-S_hhA5Wo.js +0 -9
  173. package/src/components/DataTable/README.md +0 -446
@@ -0,0 +1,435 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { AlertTriangle, Download, RefreshCw, RotateCcw, Search, Trash2, X } from "lucide-react"
5
+ import { Badge } from "../../components/badge"
6
+ import { Button } from "../../components/button"
7
+ import { Card } from "../../components/card"
8
+ import { Input } from "../../components/input"
9
+ import { ScrollArea } from "../../components/scroll-area"
10
+ import { Select } from "../../components/select"
11
+ import { cn } from "../../utils/cn"
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from "../../components/dialog"
20
+
21
+ export type ThreatSeverity = "critical" | "high" | "medium" | "low"
22
+
23
+ export interface QuarantinedFile {
24
+ id: string
25
+ originalName: string
26
+ originalPath: string
27
+ threatName: string
28
+ severity: ThreatSeverity
29
+ quarantinedAt: Date
30
+ fileSize: number
31
+ fileHash: string
32
+ uploadedBy?: string
33
+ scanEngine: string
34
+ signatureVersion: string
35
+ }
36
+
37
+ export interface QuarantineManagerProps {
38
+ files?: QuarantinedFile[]
39
+ totalCount?: number
40
+ onDelete?: (fileId: string) => void
41
+ onRestore?: (fileId: string) => void
42
+ onRescan?: (fileId: string) => void
43
+ onPurgeAll?: () => void
44
+ onDownloadReport?: (fileId: string) => void
45
+ className?: string
46
+ }
47
+
48
+ const severityConfig = {
49
+ critical: { label: "Critique", color: "bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20" },
50
+ high: { label: "Élevée", color: "bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-500/20" },
51
+ medium: { label: "Moyenne", color: "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-500/20" },
52
+ low: { label: "Faible", color: "bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20" },
53
+ }
54
+
55
+ const formatFileSize = (bytes: number): string => {
56
+ if (bytes < 1024) return `${bytes} B`
57
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
58
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
59
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
60
+ }
61
+
62
+ const formatDate = (date: Date): string => {
63
+ return new Intl.DateTimeFormat("fr-FR", {
64
+ dateStyle: "short",
65
+ timeStyle: "short",
66
+ }).format(date)
67
+ }
68
+
69
+ export const QuarantineManager: React.FC<QuarantineManagerProps> = ({
70
+ files = [],
71
+ totalCount = 0,
72
+ onDelete,
73
+ onRestore,
74
+ onRescan,
75
+ onPurgeAll,
76
+ onDownloadReport,
77
+ className,
78
+ }) => {
79
+ const [searchQuery, setSearchQuery] = React.useState("")
80
+ const [severityFilter, setSeverityFilter] = React.useState<ThreatSeverity | "all">("all")
81
+ const [dateFilter, setDateFilter] = React.useState<string>("all")
82
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
83
+ const [restoreDialogOpen, setRestoreDialogOpen] = React.useState(false)
84
+ const [purgeDialogOpen, setPurgeDialogOpen] = React.useState(false)
85
+ const [selectedFile, setSelectedFile] = React.useState<QuarantinedFile | null>(null)
86
+
87
+ const filteredFiles = React.useMemo(() => {
88
+ let result = files
89
+
90
+ if (searchQuery) {
91
+ const query = searchQuery.toLowerCase()
92
+ result = result.filter(
93
+ (file) =>
94
+ file.originalName.toLowerCase().includes(query) ||
95
+ file.threatName.toLowerCase().includes(query) ||
96
+ file.originalPath.toLowerCase().includes(query)
97
+ )
98
+ }
99
+
100
+ if (severityFilter !== "all") {
101
+ result = result.filter((file) => file.severity === severityFilter)
102
+ }
103
+
104
+ if (dateFilter !== "all") {
105
+ const now = new Date()
106
+ const filterDate = new Date()
107
+
108
+ switch (dateFilter) {
109
+ case "today":
110
+ filterDate.setHours(0, 0, 0, 0)
111
+ break
112
+ case "week":
113
+ filterDate.setDate(now.getDate() - 7)
114
+ break
115
+ case "month":
116
+ filterDate.setMonth(now.getMonth() - 1)
117
+ break
118
+ }
119
+
120
+ if (dateFilter !== "all") {
121
+ result = result.filter((file) => new Date(file.quarantinedAt) >= filterDate)
122
+ }
123
+ }
124
+
125
+ return result
126
+ }, [files, searchQuery, severityFilter, dateFilter])
127
+
128
+ const stats = React.useMemo(() => {
129
+ const counts = {
130
+ critical: 0,
131
+ high: 0,
132
+ medium: 0,
133
+ low: 0,
134
+ }
135
+
136
+ files.forEach((file) => {
137
+ counts[file.severity]++
138
+ })
139
+
140
+ return counts
141
+ }, [files])
142
+
143
+ const handleDelete = (file: QuarantinedFile) => {
144
+ setSelectedFile(file)
145
+ setDeleteDialogOpen(true)
146
+ }
147
+
148
+ const handleRestore = (file: QuarantinedFile) => {
149
+ setSelectedFile(file)
150
+ setRestoreDialogOpen(true)
151
+ }
152
+
153
+ const confirmDelete = () => {
154
+ if (selectedFile && onDelete) {
155
+ onDelete(selectedFile.id)
156
+ }
157
+ setDeleteDialogOpen(false)
158
+ setSelectedFile(null)
159
+ }
160
+
161
+ const confirmRestore = () => {
162
+ if (selectedFile && onRestore) {
163
+ onRestore(selectedFile.id)
164
+ }
165
+ setRestoreDialogOpen(false)
166
+ setSelectedFile(null)
167
+ }
168
+
169
+ const confirmPurge = () => {
170
+ if (onPurgeAll) {
171
+ onPurgeAll()
172
+ }
173
+ setPurgeDialogOpen(false)
174
+ }
175
+
176
+ return (
177
+ <div className={cn("space-y-6", className)}>
178
+ {/* Header */}
179
+ <div className="flex items-center justify-between">
180
+ <div className="space-y-1">
181
+ <h2 className="text-2xl font-semibold tracking-tight">Quarantaine</h2>
182
+ <p className="text-sm text-muted-foreground">
183
+ {totalCount || files.length} fichier{(totalCount || files.length) > 1 ? "s" : ""} en quarantaine
184
+ </p>
185
+ </div>
186
+ <Button
187
+ variant="destructive"
188
+ onClick={() => setPurgeDialogOpen(true)}
189
+ disabled={files.length === 0}
190
+ >
191
+ <Trash2 className="mr-2 h-4 w-4" />
192
+ Vider la quarantaine
193
+ </Button>
194
+ </div>
195
+
196
+ {/* Stats */}
197
+ <div className="grid gap-4 md:grid-cols-4">
198
+ {(["critical", "high", "medium", "low"] as ThreatSeverity[]).map((severity) => (
199
+ <Card key={severity} className="p-4">
200
+ <div className="flex items-center justify-between">
201
+ <div>
202
+ <p className="text-sm font-medium text-muted-foreground">
203
+ {severityConfig[severity].label}
204
+ </p>
205
+ <p className="text-2xl font-bold">{stats[severity]}</p>
206
+ </div>
207
+ <AlertTriangle
208
+ className={cn(
209
+ "h-5 w-5",
210
+ severity === "critical" && "text-red-500",
211
+ severity === "high" && "text-orange-500",
212
+ severity === "medium" && "text-yellow-500",
213
+ severity === "low" && "text-blue-500"
214
+ )}
215
+ />
216
+ </div>
217
+ </Card>
218
+ ))}
219
+ </div>
220
+
221
+ {/* Filters */}
222
+ <Card className="p-4">
223
+ <div className="grid gap-4 md:grid-cols-3">
224
+ <div className="relative">
225
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
226
+ <Input
227
+ placeholder="Rechercher par nom, menace ou chemin..."
228
+ value={searchQuery}
229
+ onChange={(e) => setSearchQuery(e.target.value)}
230
+ className="pl-9"
231
+ />
232
+ </div>
233
+
234
+ <Select
235
+ value={severityFilter}
236
+ onValueChange={(value) => setSeverityFilter(value as ThreatSeverity | "all")}
237
+ >
238
+ <option value="all">Toutes les sévérités</option>
239
+ <option value="critical">Critique</option>
240
+ <option value="high">Élevée</option>
241
+ <option value="medium">Moyenne</option>
242
+ <option value="low">Faible</option>
243
+ </Select>
244
+
245
+ <Select value={dateFilter} onValueChange={setDateFilter}>
246
+ <option value="all">Toutes les dates</option>
247
+ <option value="today">Aujourd'hui</option>
248
+ <option value="week">7 derniers jours</option>
249
+ <option value="month">30 derniers jours</option>
250
+ </Select>
251
+ </div>
252
+ </Card>
253
+
254
+ {/* Files Table */}
255
+ <Card>
256
+ <ScrollArea className="h-[600px]">
257
+ <div className="p-4">
258
+ {filteredFiles.length === 0 ? (
259
+ <div className="flex flex-col items-center justify-center py-12 text-center">
260
+ <AlertTriangle className="mb-4 h-12 w-12 text-muted-foreground" />
261
+ <h3 className="mb-1 text-lg font-semibold">Aucun fichier trouvé</h3>
262
+ <p className="text-sm text-muted-foreground">
263
+ {files.length === 0
264
+ ? "La quarantaine est vide"
265
+ : "Aucun fichier ne correspond aux filtres"}
266
+ </p>
267
+ </div>
268
+ ) : (
269
+ <div className="space-y-4">
270
+ {filteredFiles.map((file) => (
271
+ <Card key={file.id} className="p-4">
272
+ <div className="space-y-3">
273
+ {/* File Header */}
274
+ <div className="flex items-start justify-between">
275
+ <div className="flex-1 space-y-1">
276
+ <div className="flex items-center gap-2">
277
+ <h4 className="font-medium">{file.originalName}</h4>
278
+ <Badge className={severityConfig[file.severity].color}>
279
+ {severityConfig[file.severity].label}
280
+ </Badge>
281
+ </div>
282
+ <p className="text-sm text-muted-foreground">{file.originalPath}</p>
283
+ </div>
284
+ </div>
285
+
286
+ {/* File Details */}
287
+ <div className="grid gap-3 text-sm md:grid-cols-2">
288
+ <div>
289
+ <span className="font-medium">Menace détectée:</span>{" "}
290
+ <span className="text-muted-foreground">{file.threatName}</span>
291
+ </div>
292
+ <div>
293
+ <span className="font-medium">Date quarantaine:</span>{" "}
294
+ <span className="text-muted-foreground">
295
+ {formatDate(file.quarantinedAt)}
296
+ </span>
297
+ </div>
298
+ <div>
299
+ <span className="font-medium">Taille:</span>{" "}
300
+ <span className="text-muted-foreground">{formatFileSize(file.fileSize)}</span>
301
+ </div>
302
+ {file.uploadedBy && (
303
+ <div>
304
+ <span className="font-medium">Uploadé par:</span>{" "}
305
+ <span className="text-muted-foreground">{file.uploadedBy}</span>
306
+ </div>
307
+ )}
308
+ <div>
309
+ <span className="font-medium">Hash:</span>{" "}
310
+ <span className="font-mono text-xs text-muted-foreground">
311
+ {file.fileHash.substring(0, 16)}...
312
+ </span>
313
+ </div>
314
+ <div>
315
+ <span className="font-medium">Moteur:</span>{" "}
316
+ <span className="text-muted-foreground">
317
+ {file.scanEngine} ({file.signatureVersion})
318
+ </span>
319
+ </div>
320
+ </div>
321
+
322
+ {/* Actions */}
323
+ <div className="flex gap-2 border-t pt-3">
324
+ <Button
325
+ variant="outline"
326
+ size="sm"
327
+ onClick={() => onRescan?.(file.id)}
328
+ >
329
+ <RefreshCw className="mr-2 h-3 w-3" />
330
+ Re-scanner
331
+ </Button>
332
+ <Button
333
+ variant="outline"
334
+ size="sm"
335
+ onClick={() => handleRestore(file)}
336
+ >
337
+ <RotateCcw className="mr-2 h-3 w-3" />
338
+ Restaurer
339
+ </Button>
340
+ <Button
341
+ variant="outline"
342
+ size="sm"
343
+ onClick={() => onDownloadReport?.(file.id)}
344
+ >
345
+ <Download className="mr-2 h-3 w-3" />
346
+ Rapport
347
+ </Button>
348
+ <Button
349
+ variant="destructive"
350
+ size="sm"
351
+ onClick={() => handleDelete(file)}
352
+ >
353
+ <Trash2 className="mr-2 h-3 w-3" />
354
+ Supprimer
355
+ </Button>
356
+ </div>
357
+ </div>
358
+ </Card>
359
+ ))}
360
+ </div>
361
+ )}
362
+ </div>
363
+ </ScrollArea>
364
+ </Card>
365
+
366
+ {/* Delete Dialog */}
367
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
368
+ <DialogContent>
369
+ <DialogHeader>
370
+ <DialogTitle>Supprimer définitivement</DialogTitle>
371
+ <DialogDescription>
372
+ Êtes-vous sûr de vouloir supprimer définitivement le fichier{" "}
373
+ <span className="font-medium">{selectedFile?.originalName}</span> ?<br />
374
+ Cette action est irréversible.
375
+ </DialogDescription>
376
+ </DialogHeader>
377
+ <DialogFooter>
378
+ <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
379
+ Annuler
380
+ </Button>
381
+ <Button variant="destructive" onClick={confirmDelete}>
382
+ Supprimer
383
+ </Button>
384
+ </DialogFooter>
385
+ </DialogContent>
386
+ </Dialog>
387
+
388
+ {/* Restore Dialog */}
389
+ <Dialog open={restoreDialogOpen} onOpenChange={setRestoreDialogOpen}>
390
+ <DialogContent>
391
+ <DialogHeader>
392
+ <DialogTitle>Restaurer le fichier</DialogTitle>
393
+ <DialogDescription>
394
+ Êtes-vous sûr de vouloir restaurer le fichier{" "}
395
+ <span className="font-medium">{selectedFile?.originalName}</span> ?<br />
396
+ Le fichier sera replacé à son emplacement d'origine :{" "}
397
+ <span className="font-mono text-xs">{selectedFile?.originalPath}</span>
398
+ </DialogDescription>
399
+ </DialogHeader>
400
+ <DialogFooter>
401
+ <Button variant="outline" onClick={() => setRestoreDialogOpen(false)}>
402
+ Annuler
403
+ </Button>
404
+ <Button onClick={confirmRestore}>Restaurer</Button>
405
+ </DialogFooter>
406
+ </DialogContent>
407
+ </Dialog>
408
+
409
+ {/* Purge Dialog */}
410
+ <Dialog open={purgeDialogOpen} onOpenChange={setPurgeDialogOpen}>
411
+ <DialogContent>
412
+ <DialogHeader>
413
+ <DialogTitle>Vider la quarantaine</DialogTitle>
414
+ <DialogDescription>
415
+ Êtes-vous sûr de vouloir supprimer définitivement tous les fichiers en quarantaine ?
416
+ <br />
417
+ {files.length} fichier{files.length > 1 ? "s" : ""} ser{files.length > 1 ? "ont" : "a"}{" "}
418
+ supprimé{files.length > 1 ? "s" : ""}. Cette action est irréversible.
419
+ </DialogDescription>
420
+ </DialogHeader>
421
+ <DialogFooter>
422
+ <Button variant="outline" onClick={() => setPurgeDialogOpen(false)}>
423
+ Annuler
424
+ </Button>
425
+ <Button variant="destructive" onClick={confirmPurge}>
426
+ Vider la quarantaine
427
+ </Button>
428
+ </DialogFooter>
429
+ </DialogContent>
430
+ </Dialog>
431
+ </div>
432
+ )
433
+ }
434
+
435
+ export default QuarantineManager
@@ -0,0 +1,231 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+ import { ScanHistoryLog, type ScanLogEntry } from "./index"
3
+
4
+ const meta = {
5
+ title: "Blocks/Antivirus/ScanHistoryLog",
6
+ component: ScanHistoryLog,
7
+ parameters: {
8
+ layout: "padded",
9
+ },
10
+ tags: ["autodocs"],
11
+ } satisfies Meta<typeof ScanHistoryLog>
12
+
13
+ export default meta
14
+ type Story = StoryObj<typeof meta>
15
+
16
+ const generateMockEntries = (): ScanLogEntry[] => {
17
+ const now = new Date()
18
+ const users = [
19
+ { id: "user-1", name: "Marie Dupont" },
20
+ { id: "user-2", name: "Jean Martin" },
21
+ { id: "user-3", name: "Sophie Bernard" },
22
+ { id: "user-4", name: "Luc Moreau" },
23
+ ]
24
+
25
+ const cleanFiles = [
26
+ "rapport-mensuel.pdf",
27
+ "presentation.pptx",
28
+ "document.docx",
29
+ "image.jpg",
30
+ "archive.zip",
31
+ "script.js",
32
+ "style.css",
33
+ "index.html",
34
+ "data.json",
35
+ "config.yaml",
36
+ ]
37
+
38
+ const infectedFiles = [
39
+ "virus.exe",
40
+ "trojan-downloader.zip",
41
+ "malware.pdf",
42
+ "ransomware.docx",
43
+ ]
44
+
45
+ const errorFiles = [
46
+ "corrupted-archive.zip",
47
+ "encrypted-file.bin",
48
+ "large-file.iso",
49
+ ]
50
+
51
+ const threats = [
52
+ "Trojan.Generic.KD.12345678",
53
+ "Win32.Malware.Agent",
54
+ "EICAR-Test-File",
55
+ "Ransom.WannaCry.Variant",
56
+ "Adware.Generic.BundleInstaller",
57
+ ]
58
+
59
+ const entries: ScanLogEntry[] = []
60
+ let idCounter = 1
61
+
62
+ // Clean files (10 entries)
63
+ for (let i = 0; i < 10; i++) {
64
+ const daysAgo = Math.floor(Math.random() * 7)
65
+ const hoursAgo = Math.floor(Math.random() * 24)
66
+ const scanDate = new Date(now)
67
+ scanDate.setDate(scanDate.getDate() - daysAgo)
68
+ scanDate.setHours(scanDate.getHours() - hoursAgo)
69
+
70
+ const user = users[Math.floor(Math.random() * users.length)]
71
+ const filename = cleanFiles[Math.floor(Math.random() * cleanFiles.length)]
72
+ const fileSize = Math.floor(Math.random() * 10000000) + 1000
73
+
74
+ entries.push({
75
+ id: `scan-${idCounter++}`,
76
+ filename,
77
+ fileSize,
78
+ fileHash: Array.from({ length: 64 }, () =>
79
+ Math.floor(Math.random() * 16).toString(16)
80
+ ).join(""),
81
+ result: "clean",
82
+ scanDate,
83
+ duration: Math.floor(Math.random() * 2000) + 100,
84
+ userId: user.id,
85
+ userName: user.name,
86
+ engineVersion: "ClamAV 1.2.1",
87
+ signatureVersion: `${Math.floor(Math.random() * 30000) + 25000}`,
88
+ })
89
+ }
90
+
91
+ // Infected files (5 entries)
92
+ for (let i = 0; i < 5; i++) {
93
+ const daysAgo = Math.floor(Math.random() * 7)
94
+ const hoursAgo = Math.floor(Math.random() * 24)
95
+ const scanDate = new Date(now)
96
+ scanDate.setDate(scanDate.getDate() - daysAgo)
97
+ scanDate.setHours(scanDate.getHours() - hoursAgo)
98
+
99
+ const user = users[Math.floor(Math.random() * users.length)]
100
+ const filename = infectedFiles[Math.floor(Math.random() * infectedFiles.length)]
101
+ const fileSize = Math.floor(Math.random() * 5000000) + 1000
102
+ const threatName = threats[Math.floor(Math.random() * threats.length)]
103
+
104
+ entries.push({
105
+ id: `scan-${idCounter++}`,
106
+ filename,
107
+ fileSize,
108
+ fileHash: Array.from({ length: 64 }, () =>
109
+ Math.floor(Math.random() * 16).toString(16)
110
+ ).join(""),
111
+ result: "infected",
112
+ threatName,
113
+ scanDate,
114
+ duration: Math.floor(Math.random() * 3000) + 500,
115
+ userId: user.id,
116
+ userName: user.name,
117
+ engineVersion: "ClamAV 1.2.1",
118
+ signatureVersion: `${Math.floor(Math.random() * 30000) + 25000}`,
119
+ })
120
+ }
121
+
122
+ // Error files (3 entries)
123
+ for (let i = 0; i < 3; i++) {
124
+ const daysAgo = Math.floor(Math.random() * 7)
125
+ const hoursAgo = Math.floor(Math.random() * 24)
126
+ const scanDate = new Date(now)
127
+ scanDate.setDate(scanDate.getDate() - daysAgo)
128
+ scanDate.setHours(scanDate.getHours() - hoursAgo)
129
+
130
+ const user = users[Math.floor(Math.random() * users.length)]
131
+ const filename = errorFiles[Math.floor(Math.random() * errorFiles.length)]
132
+ const fileSize = Math.floor(Math.random() * 50000000) + 10000000
133
+
134
+ entries.push({
135
+ id: `scan-${idCounter++}`,
136
+ filename,
137
+ fileSize,
138
+ fileHash: Array.from({ length: 64 }, () =>
139
+ Math.floor(Math.random() * 16).toString(16)
140
+ ).join(""),
141
+ result: "error",
142
+ scanDate,
143
+ duration: Math.floor(Math.random() * 5000) + 1000,
144
+ userId: user.id,
145
+ userName: user.name,
146
+ engineVersion: "ClamAV 1.2.1",
147
+ signatureVersion: `${Math.floor(Math.random() * 30000) + 25000}`,
148
+ })
149
+ }
150
+
151
+ // Sort by date (most recent first)
152
+ return entries.sort((a, b) => b.scanDate.getTime() - a.scanDate.getTime())
153
+ }
154
+
155
+ export const Default: Story = {
156
+ args: {
157
+ entries: generateMockEntries(),
158
+ totalEntries: 18,
159
+ currentPage: 1,
160
+ pageSize: 10,
161
+ periodFilter: "7d",
162
+ onPageChange: (page: number) => console.log("Page changed:", page),
163
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
164
+ onExport: () => console.log("Export triggered"),
165
+ },
166
+ }
167
+
168
+ export const Empty: Story = {
169
+ args: {
170
+ entries: [],
171
+ totalEntries: 0,
172
+ currentPage: 1,
173
+ pageSize: 10,
174
+ periodFilter: "7d",
175
+ onPageChange: (page: number) => console.log("Page changed:", page),
176
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
177
+ onExport: () => console.log("Export triggered"),
178
+ },
179
+ }
180
+
181
+ export const OnlyCleanScans: Story = {
182
+ args: {
183
+ entries: generateMockEntries().filter((e) => e.result === "clean"),
184
+ totalEntries: 10,
185
+ currentPage: 1,
186
+ pageSize: 10,
187
+ periodFilter: "7d",
188
+ onPageChange: (page: number) => console.log("Page changed:", page),
189
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
190
+ onExport: () => console.log("Export triggered"),
191
+ },
192
+ }
193
+
194
+ export const WithInfections: Story = {
195
+ args: {
196
+ entries: generateMockEntries().filter((e) => e.result === "infected"),
197
+ totalEntries: 5,
198
+ currentPage: 1,
199
+ pageSize: 10,
200
+ periodFilter: "7d",
201
+ onPageChange: (page: number) => console.log("Page changed:", page),
202
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
203
+ onExport: () => console.log("Export triggered"),
204
+ },
205
+ }
206
+
207
+ export const WithErrors: Story = {
208
+ args: {
209
+ entries: generateMockEntries().filter((e) => e.result === "error"),
210
+ totalEntries: 3,
211
+ currentPage: 1,
212
+ pageSize: 10,
213
+ periodFilter: "7d",
214
+ onPageChange: (page: number) => console.log("Page changed:", page),
215
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
216
+ onExport: () => console.log("Export triggered"),
217
+ },
218
+ }
219
+
220
+ export const WithPagination: Story = {
221
+ args: {
222
+ entries: generateMockEntries(),
223
+ totalEntries: 50,
224
+ currentPage: 2,
225
+ pageSize: 5,
226
+ periodFilter: "30d",
227
+ onPageChange: (page: number) => console.log("Page changed:", page),
228
+ onFilterChange: (filters: any) => console.log("Filters changed:", filters),
229
+ onExport: () => console.log("Export triggered"),
230
+ },
231
+ }