@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,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
|
+
}
|