@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,320 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Badge } from "../../components/badge"
|
|
5
|
+
import { Button } from "../../components/button"
|
|
6
|
+
import { Card } from "../../components/card"
|
|
7
|
+
import { ShieldAlert, AlertTriangle, AlertCircle, Info, X, ChevronDown, ChevronUp, Eye, Archive } from "lucide-react"
|
|
8
|
+
import { cn } from "../../utils/cn"
|
|
9
|
+
|
|
10
|
+
export type AlertSeverity = "critical" | "high" | "medium" | "low"
|
|
11
|
+
export type ThreatAction = "quarantined" | "deleted" | "blocked" | "allowed"
|
|
12
|
+
|
|
13
|
+
export interface ThreatAlert {
|
|
14
|
+
id: string
|
|
15
|
+
threatName: string
|
|
16
|
+
filename: string
|
|
17
|
+
filePath: string
|
|
18
|
+
severity: AlertSeverity
|
|
19
|
+
action: ThreatAction
|
|
20
|
+
detectedAt: Date
|
|
21
|
+
userId?: string
|
|
22
|
+
engineVersion: string
|
|
23
|
+
dismissed: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ThreatAlertBannerProps {
|
|
27
|
+
alerts?: ThreatAlert[]
|
|
28
|
+
maxVisible?: number
|
|
29
|
+
autoHideAfter?: number // ms, 0 = never
|
|
30
|
+
onDismiss?: (alertId: string) => void
|
|
31
|
+
onDismissAll?: () => void
|
|
32
|
+
onViewDetails?: (alertId: string) => void
|
|
33
|
+
onViewQuarantine?: () => void
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const severityConfig = {
|
|
38
|
+
critical: {
|
|
39
|
+
icon: ShieldAlert,
|
|
40
|
+
bgColor: "bg-red-950/50 border-red-800",
|
|
41
|
+
iconColor: "text-red-400",
|
|
42
|
+
badgeVariant: "destructive" as const,
|
|
43
|
+
},
|
|
44
|
+
high: {
|
|
45
|
+
icon: AlertTriangle,
|
|
46
|
+
bgColor: "bg-orange-950/50 border-orange-800",
|
|
47
|
+
iconColor: "text-orange-400",
|
|
48
|
+
badgeVariant: "destructive" as const,
|
|
49
|
+
},
|
|
50
|
+
medium: {
|
|
51
|
+
icon: AlertCircle,
|
|
52
|
+
bgColor: "bg-yellow-950/50 border-yellow-800",
|
|
53
|
+
iconColor: "text-yellow-400",
|
|
54
|
+
badgeVariant: "secondary" as const,
|
|
55
|
+
},
|
|
56
|
+
low: {
|
|
57
|
+
icon: Info,
|
|
58
|
+
bgColor: "bg-blue-950/50 border-blue-800",
|
|
59
|
+
iconColor: "text-blue-400",
|
|
60
|
+
badgeVariant: "default" as const,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const actionLabels: Record<ThreatAction, string> = {
|
|
65
|
+
quarantined: "Mis en quarantaine",
|
|
66
|
+
deleted: "Supprimé",
|
|
67
|
+
blocked: "Bloqué",
|
|
68
|
+
allowed: "Autorisé",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const actionColors: Record<ThreatAction, string> = {
|
|
72
|
+
quarantined: "text-orange-400",
|
|
73
|
+
deleted: "text-red-400",
|
|
74
|
+
blocked: "text-red-400",
|
|
75
|
+
allowed: "text-green-400",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const ThreatAlertBanner = ({
|
|
79
|
+
alerts = [],
|
|
80
|
+
maxVisible = 3,
|
|
81
|
+
autoHideAfter = 0,
|
|
82
|
+
onDismiss,
|
|
83
|
+
onDismissAll,
|
|
84
|
+
onViewDetails,
|
|
85
|
+
onViewQuarantine,
|
|
86
|
+
className,
|
|
87
|
+
}: ThreatAlertBannerProps) => {
|
|
88
|
+
const [expandedAlertIds, setExpandedAlertIds] = useState<Set<string>>(new Set())
|
|
89
|
+
|
|
90
|
+
const activeAlerts = alerts.filter((alert) => !alert.dismissed)
|
|
91
|
+
|
|
92
|
+
if (activeAlerts.length === 0) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const toggleExpanded = (alertId: string) => {
|
|
97
|
+
setExpandedAlertIds((prev) => {
|
|
98
|
+
const next = new Set(prev)
|
|
99
|
+
if (next.has(alertId)) {
|
|
100
|
+
next.delete(alertId)
|
|
101
|
+
} else {
|
|
102
|
+
next.add(alertId)
|
|
103
|
+
}
|
|
104
|
+
return next
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const visibleAlerts = activeAlerts.slice(0, maxVisible)
|
|
109
|
+
const remainingCount = activeAlerts.length - maxVisible
|
|
110
|
+
|
|
111
|
+
const formatTimestamp = (date: Date) => {
|
|
112
|
+
const now = new Date()
|
|
113
|
+
const diff = now.getTime() - date.getTime()
|
|
114
|
+
const minutes = Math.floor(diff / 60000)
|
|
115
|
+
const hours = Math.floor(diff / 3600000)
|
|
116
|
+
|
|
117
|
+
if (minutes < 1) return "À l'instant"
|
|
118
|
+
if (minutes < 60) return `Il y a ${minutes}m`
|
|
119
|
+
if (hours < 24) return `Il y a ${hours}h`
|
|
120
|
+
return date.toLocaleDateString("fr-FR", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className={cn("space-y-2", className)}>
|
|
125
|
+
{activeAlerts.length > 1 && (
|
|
126
|
+
<div className="flex items-center justify-between bg-red-950/30 border border-red-800/50 rounded-lg px-4 py-2">
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<ShieldAlert className="h-4 w-4 text-red-400" />
|
|
129
|
+
<span className="text-sm font-medium text-red-200">
|
|
130
|
+
{activeAlerts.length} alerte{activeAlerts.length > 1 ? "s" : ""} active{activeAlerts.length > 1 ? "s" : ""}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
{onDismissAll && (
|
|
134
|
+
<Button variant="ghost" size="sm" onClick={onDismissAll} className="text-red-300 hover:text-red-200">
|
|
135
|
+
Tout ignorer
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{visibleAlerts.map((alert) => {
|
|
142
|
+
const config = severityConfig[alert.severity]
|
|
143
|
+
const Icon = config.icon
|
|
144
|
+
const isExpanded = expandedAlertIds.has(alert.id)
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Card
|
|
148
|
+
key={alert.id}
|
|
149
|
+
className={cn(
|
|
150
|
+
"border-2 backdrop-blur-sm transition-all duration-200",
|
|
151
|
+
config.bgColor,
|
|
152
|
+
isExpanded && "shadow-lg"
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<div className="p-4">
|
|
156
|
+
<div className="flex items-start gap-3">
|
|
157
|
+
<div className={cn("mt-0.5 flex-shrink-0", config.iconColor)}>
|
|
158
|
+
<Icon className="h-5 w-5" />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="flex-1 min-w-0">
|
|
162
|
+
<div className="flex items-start justify-between gap-3">
|
|
163
|
+
<div className="flex-1 min-w-0">
|
|
164
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
165
|
+
<h4 className="text-sm font-semibold text-white truncate">{alert.threatName}</h4>
|
|
166
|
+
<Badge variant={config.badgeVariant} className="text-xs">
|
|
167
|
+
{alert.severity.toUpperCase()}
|
|
168
|
+
</Badge>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="mt-1 flex items-center gap-2 text-xs text-gray-400">
|
|
172
|
+
<span className="truncate" title={alert.filePath}>
|
|
173
|
+
{alert.filename}
|
|
174
|
+
</span>
|
|
175
|
+
<span>•</span>
|
|
176
|
+
<span className={actionColors[alert.action]}>{actionLabels[alert.action]}</span>
|
|
177
|
+
<span>•</span>
|
|
178
|
+
<span>{formatTimestamp(alert.detectedAt)}</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
onClick={() => onDismiss?.(alert.id)}
|
|
186
|
+
className="flex-shrink-0 h-8 w-8 p-0 text-gray-400 hover:text-white"
|
|
187
|
+
>
|
|
188
|
+
<X className="h-4 w-4" />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{isExpanded && (
|
|
193
|
+
<div className="mt-3 pt-3 border-t border-white/10 space-y-2 text-xs text-gray-300">
|
|
194
|
+
<div className="grid grid-cols-2 gap-2">
|
|
195
|
+
<div>
|
|
196
|
+
<span className="text-gray-500">Chemin complet:</span>
|
|
197
|
+
<p className="font-mono break-all">{alert.filePath}</p>
|
|
198
|
+
</div>
|
|
199
|
+
<div>
|
|
200
|
+
<span className="text-gray-500">Moteur antivirus:</span>
|
|
201
|
+
<p className="font-mono">{alert.engineVersion}</p>
|
|
202
|
+
</div>
|
|
203
|
+
{alert.userId && (
|
|
204
|
+
<div>
|
|
205
|
+
<span className="text-gray-500">Utilisateur:</span>
|
|
206
|
+
<p className="font-mono">{alert.userId}</p>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
<div>
|
|
210
|
+
<span className="text-gray-500">Détecté le:</span>
|
|
211
|
+
<p>
|
|
212
|
+
{alert.detectedAt.toLocaleString("fr-FR", {
|
|
213
|
+
dateStyle: "medium",
|
|
214
|
+
timeStyle: "short",
|
|
215
|
+
})}
|
|
216
|
+
</p>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<div className="mt-3 flex items-center gap-2">
|
|
223
|
+
<Button
|
|
224
|
+
variant="outline"
|
|
225
|
+
size="sm"
|
|
226
|
+
onClick={() => toggleExpanded(alert.id)}
|
|
227
|
+
className="text-xs"
|
|
228
|
+
>
|
|
229
|
+
{isExpanded ? (
|
|
230
|
+
<>
|
|
231
|
+
<ChevronUp className="h-3 w-3 mr-1" />
|
|
232
|
+
Masquer
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
<>
|
|
236
|
+
<ChevronDown className="h-3 w-3 mr-1" />
|
|
237
|
+
Détails
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
</Button>
|
|
241
|
+
|
|
242
|
+
{onViewDetails && (
|
|
243
|
+
<Button
|
|
244
|
+
variant="outline"
|
|
245
|
+
size="sm"
|
|
246
|
+
onClick={() => onViewDetails(alert.id)}
|
|
247
|
+
className="text-xs"
|
|
248
|
+
>
|
|
249
|
+
<Eye className="h-3 w-3 mr-1" />
|
|
250
|
+
Analyse complète
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{alert.action === "quarantined" && onViewQuarantine && (
|
|
255
|
+
<Button
|
|
256
|
+
variant="outline"
|
|
257
|
+
size="sm"
|
|
258
|
+
onClick={onViewQuarantine}
|
|
259
|
+
className="text-xs"
|
|
260
|
+
>
|
|
261
|
+
<Archive className="h-3 w-3 mr-1" />
|
|
262
|
+
Quarantaine
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</Card>
|
|
270
|
+
)
|
|
271
|
+
})}
|
|
272
|
+
|
|
273
|
+
{remainingCount > 0 && (
|
|
274
|
+
<div className="text-center">
|
|
275
|
+
<Button variant="ghost" size="sm" className="text-xs text-gray-400 hover:text-white">
|
|
276
|
+
+ {remainingCount} autre{remainingCount > 1 ? "s" : ""} alerte{remainingCount > 1 ? "s" : ""}
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const defaultThreatAlerts: ThreatAlert[] = [
|
|
285
|
+
{
|
|
286
|
+
id: "1",
|
|
287
|
+
threatName: "Trojan.GenericKD.12345678",
|
|
288
|
+
filename: "invoice_2024.pdf.exe",
|
|
289
|
+
filePath: "/home/user/Downloads/invoice_2024.pdf.exe",
|
|
290
|
+
severity: "critical",
|
|
291
|
+
action: "quarantined",
|
|
292
|
+
detectedAt: new Date(Date.now() - 5 * 60000),
|
|
293
|
+
userId: "user@example.com",
|
|
294
|
+
engineVersion: "ClamAV 1.4.1",
|
|
295
|
+
dismissed: false,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "2",
|
|
299
|
+
threatName: "Malware.Suspicious.Archive",
|
|
300
|
+
filename: "files.zip",
|
|
301
|
+
filePath: "/var/www/uploads/files.zip",
|
|
302
|
+
severity: "high",
|
|
303
|
+
action: "blocked",
|
|
304
|
+
detectedAt: new Date(Date.now() - 15 * 60000),
|
|
305
|
+
engineVersion: "ClamAV 1.4.1",
|
|
306
|
+
dismissed: false,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "3",
|
|
310
|
+
threatName: "PUA.Win.Adware.Generic",
|
|
311
|
+
filename: "installer.exe",
|
|
312
|
+
filePath: "/tmp/installer.exe",
|
|
313
|
+
severity: "medium",
|
|
314
|
+
action: "deleted",
|
|
315
|
+
detectedAt: new Date(Date.now() - 3600000),
|
|
316
|
+
userId: "admin@example.com",
|
|
317
|
+
engineVersion: "ClamAV 1.4.1",
|
|
318
|
+
dismissed: false,
|
|
319
|
+
},
|
|
320
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { DataTable } from "./index"
|
|
3
|
+
import { ColumnDef } from "@tanstack/react-table"
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
email: string
|
|
9
|
+
role: string
|
|
10
|
+
status: "active" | "inactive" | "pending"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sampleData: User[] = [
|
|
14
|
+
{ id: "1", name: "John Doe", email: "john@example.com", role: "Admin", status: "active" },
|
|
15
|
+
{ id: "2", name: "Jane Smith", email: "jane@example.com", role: "Editor", status: "active" },
|
|
16
|
+
{ id: "3", name: "Bob Johnson", email: "bob@example.com", role: "Viewer", status: "inactive" },
|
|
17
|
+
{ id: "4", name: "Alice Brown", email: "alice@example.com", role: "Editor", status: "pending" },
|
|
18
|
+
{ id: "5", name: "Charlie Wilson", email: "charlie@example.com", role: "Admin", status: "active" },
|
|
19
|
+
{ id: "6", name: "Diana Miller", email: "diana@example.com", role: "Viewer", status: "active" },
|
|
20
|
+
{ id: "7", name: "Edward Davis", email: "edward@example.com", role: "Editor", status: "inactive" },
|
|
21
|
+
{ id: "8", name: "Fiona Garcia", email: "fiona@example.com", role: "Admin", status: "active" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const columns: ColumnDef<User>[] = [
|
|
25
|
+
{
|
|
26
|
+
accessorKey: "name",
|
|
27
|
+
header: "Name",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
accessorKey: "email",
|
|
31
|
+
header: "Email",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
accessorKey: "role",
|
|
35
|
+
header: "Role",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
accessorKey: "status",
|
|
39
|
+
header: "Status",
|
|
40
|
+
cell: ({ row }) => {
|
|
41
|
+
const status = row.getValue("status") as string
|
|
42
|
+
const colors = {
|
|
43
|
+
active: "bg-green-100 text-green-800",
|
|
44
|
+
inactive: "bg-gray-100 text-gray-800",
|
|
45
|
+
pending: "bg-yellow-100 text-yellow-800",
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colors[status as keyof typeof colors]}`}>
|
|
49
|
+
{status}
|
|
50
|
+
</span>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const meta: Meta<typeof DataTable<User>> = {
|
|
57
|
+
title: "Components/Data/DataTable",
|
|
58
|
+
component: DataTable,
|
|
59
|
+
parameters: {
|
|
60
|
+
layout: "padded",
|
|
61
|
+
},
|
|
62
|
+
tags: ["autodocs"],
|
|
63
|
+
argTypes: {
|
|
64
|
+
layout: {
|
|
65
|
+
control: "select",
|
|
66
|
+
options: ["standard", "card", "compact", "split", "infinite"],
|
|
67
|
+
description: "Table layout style",
|
|
68
|
+
},
|
|
69
|
+
variant: {
|
|
70
|
+
control: "select",
|
|
71
|
+
options: ["bordered", "minimal", "striped", "glass"],
|
|
72
|
+
description: "Table visual variant",
|
|
73
|
+
},
|
|
74
|
+
density: {
|
|
75
|
+
control: "select",
|
|
76
|
+
options: ["compact", "comfortable", "spacious"],
|
|
77
|
+
description: "Row density",
|
|
78
|
+
},
|
|
79
|
+
selection: {
|
|
80
|
+
control: "boolean",
|
|
81
|
+
description: "Enable row selection",
|
|
82
|
+
},
|
|
83
|
+
loading: {
|
|
84
|
+
control: "boolean",
|
|
85
|
+
description: "Loading state",
|
|
86
|
+
},
|
|
87
|
+
headerSticky: {
|
|
88
|
+
control: "boolean",
|
|
89
|
+
description: "Sticky header",
|
|
90
|
+
},
|
|
91
|
+
columnVisibilityToggle: {
|
|
92
|
+
control: "boolean",
|
|
93
|
+
description: "Enable column visibility toggle",
|
|
94
|
+
},
|
|
95
|
+
enableExport: {
|
|
96
|
+
control: "boolean",
|
|
97
|
+
description: "Enable export functionality",
|
|
98
|
+
},
|
|
99
|
+
enableImport: {
|
|
100
|
+
control: "boolean",
|
|
101
|
+
description: "Enable import functionality",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default meta
|
|
107
|
+
type Story = StoryObj<typeof DataTable<User>>
|
|
108
|
+
|
|
109
|
+
export const Default: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
data: sampleData,
|
|
112
|
+
columns: columns,
|
|
113
|
+
layout: "standard",
|
|
114
|
+
variant: "bordered",
|
|
115
|
+
density: "comfortable",
|
|
116
|
+
},
|
|
117
|
+
render: (args) => <DataTable {...args} />,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const WithSelection: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
data: sampleData,
|
|
123
|
+
columns: columns,
|
|
124
|
+
selection: true,
|
|
125
|
+
variant: "bordered",
|
|
126
|
+
},
|
|
127
|
+
render: (args) => <DataTable {...args} />,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const WithPagination: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
data: sampleData,
|
|
133
|
+
columns: columns,
|
|
134
|
+
pagination: {
|
|
135
|
+
mode: "client",
|
|
136
|
+
pageSize: 5,
|
|
137
|
+
pageSizeOptions: [5, 10, 20],
|
|
138
|
+
},
|
|
139
|
+
variant: "bordered",
|
|
140
|
+
},
|
|
141
|
+
render: (args) => <DataTable {...args} />,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const Striped: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
data: sampleData,
|
|
147
|
+
columns: columns,
|
|
148
|
+
variant: "striped",
|
|
149
|
+
},
|
|
150
|
+
render: (args) => <DataTable {...args} />,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const Compact: Story = {
|
|
154
|
+
args: {
|
|
155
|
+
data: sampleData,
|
|
156
|
+
columns: columns,
|
|
157
|
+
density: "compact",
|
|
158
|
+
variant: "bordered",
|
|
159
|
+
},
|
|
160
|
+
render: (args) => <DataTable {...args} />,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const Loading: Story = {
|
|
164
|
+
args: {
|
|
165
|
+
data: [],
|
|
166
|
+
columns: columns,
|
|
167
|
+
loading: true,
|
|
168
|
+
},
|
|
169
|
+
render: (args) => <DataTable {...args} />,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const Empty: Story = {
|
|
173
|
+
args: {
|
|
174
|
+
data: [],
|
|
175
|
+
columns: columns,
|
|
176
|
+
},
|
|
177
|
+
render: (args) => <DataTable {...args} />,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const WithError: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
data: [],
|
|
183
|
+
columns: columns,
|
|
184
|
+
error: "Failed to load data. Please try again.",
|
|
185
|
+
},
|
|
186
|
+
render: (args) => <DataTable {...args} />,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const WithToolbar: Story = {
|
|
190
|
+
args: {
|
|
191
|
+
data: sampleData,
|
|
192
|
+
columns: columns,
|
|
193
|
+
toolbar: {
|
|
194
|
+
showGlobalSearch: true,
|
|
195
|
+
showColumnVisibility: true,
|
|
196
|
+
showDensityToggle: true,
|
|
197
|
+
},
|
|
198
|
+
enableExport: true,
|
|
199
|
+
variant: "bordered",
|
|
200
|
+
},
|
|
201
|
+
render: (args) => <DataTable {...args} />,
|
|
202
|
+
}
|
|
203
|
+
|
|
@@ -66,44 +66,47 @@ export function useDataTableExport<TData>({
|
|
|
66
66
|
URL.revokeObjectURL(link.href)
|
|
67
67
|
}, [config.filename])
|
|
68
68
|
|
|
69
|
-
// Fonction d'export XLSX (nécessite
|
|
69
|
+
// Fonction d'export XLSX (nécessite exceljs)
|
|
70
70
|
const exportToXLSX = useCallback(async (data: TData[], filename?: string) => {
|
|
71
71
|
try {
|
|
72
|
-
// Vérifier si la librairie xlsx est disponible
|
|
73
72
|
if (typeof window === 'undefined') {
|
|
74
73
|
throw new Error("XLSX export not available in server environment")
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
// Import dynamique pour éviter d'augmenter la taille du bundle
|
|
75
|
+
|
|
78
76
|
// @ts-ignore - Module importé dynamiquement
|
|
79
|
-
const
|
|
80
|
-
|
|
77
|
+
const ExcelJS = await import("exceljs")
|
|
78
|
+
|
|
79
|
+
const workbook = new ExcelJS.Workbook()
|
|
80
|
+
const worksheet = workbook.addWorksheet(config.options?.xlsx?.sheetName || "Sheet1")
|
|
81
|
+
|
|
81
82
|
const headers = columns
|
|
82
83
|
.filter(col => col.accessorKey || col.id)
|
|
83
84
|
.map(col => col.header || col.accessorKey || col.id)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
worksheet.addRow(headers)
|
|
87
|
+
|
|
88
|
+
data.forEach(row => {
|
|
89
|
+
const values = columns
|
|
90
|
+
.filter(col => col.accessorKey || col.id)
|
|
91
|
+
.map(col => {
|
|
92
|
+
const value = col.accessorKey ? (row as any)[col.accessorKey] : (row as any)[col.id]
|
|
93
|
+
return col.formatter ? col.formatter(value) : value
|
|
94
|
+
})
|
|
95
|
+
worksheet.addRow(values)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const buffer = await workbook.xlsx.writeBuffer()
|
|
99
|
+
const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" })
|
|
100
|
+
const link = document.createElement("a")
|
|
101
|
+
link.href = URL.createObjectURL(blob)
|
|
102
|
+
link.download = `${filename || config.filename}.xlsx`
|
|
103
|
+
link.click()
|
|
104
|
+
URL.revokeObjectURL(link.href)
|
|
102
105
|
} catch (error) {
|
|
103
106
|
console.error("Erreur lors de l'export XLSX:", error)
|
|
104
|
-
throw new Error("La librairie
|
|
107
|
+
throw new Error("La librairie exceljs n'est pas disponible. Installez-la avec: pnpm add exceljs")
|
|
105
108
|
}
|
|
106
|
-
}, [columns, config.filename])
|
|
109
|
+
}, [columns, config.filename, config.options?.xlsx?.sheetName])
|
|
107
110
|
|
|
108
111
|
// Fonction d'export PDF (nécessite une librairie externe)
|
|
109
112
|
const exportToPDF = useCallback(async (data: TData[], filename?: string) => {
|
|
@@ -113,12 +116,15 @@ export function useDataTableExport<TData>({
|
|
|
113
116
|
throw new Error("PDF export not available in server environment")
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// Import dynamique des modules PDF
|
|
120
|
+
const jsPDFModule = await import("jspdf")
|
|
121
|
+
const autoTableModule = await import("jspdf-autotable")
|
|
122
|
+
|
|
123
|
+
const jsPDF = jsPDFModule.default || jsPDFModule.jsPDF
|
|
124
|
+
const doc = new jsPDF()
|
|
120
125
|
|
|
121
|
-
|
|
126
|
+
// Appliquer le plugin autoTable au document
|
|
127
|
+
const autoTable = autoTableModule.default || autoTableModule.autoTable
|
|
122
128
|
|
|
123
129
|
const headers = columns
|
|
124
130
|
.filter(col => col.accessorKey || col.id)
|
|
@@ -133,7 +139,8 @@ export function useDataTableExport<TData>({
|
|
|
133
139
|
})
|
|
134
140
|
)
|
|
135
141
|
|
|
136
|
-
|
|
142
|
+
// Appeler autoTable comme fonction avec le document
|
|
143
|
+
autoTable(doc, {
|
|
137
144
|
head: [headers],
|
|
138
145
|
body: rows,
|
|
139
146
|
startY: 20,
|
|
@@ -62,36 +62,47 @@ export function useDataTableImport<TData>({
|
|
|
62
62
|
}
|
|
63
63
|
}, [])
|
|
64
64
|
|
|
65
|
-
// Fonction de parsing XLSX
|
|
65
|
+
// Fonction de parsing XLSX (nécessite exceljs)
|
|
66
66
|
const parseXLSX = useCallback(async (file: File) => {
|
|
67
67
|
try {
|
|
68
|
-
// Vérifier si on est côté client
|
|
69
68
|
if (typeof window === 'undefined') {
|
|
70
69
|
throw new Error("XLSX parsing not available in server environment")
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
|
|
73
72
|
// @ts-ignore - Module importé dynamiquement
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
const ExcelJS = await import("exceljs")
|
|
74
|
+
const buffer = await file.arrayBuffer()
|
|
75
|
+
|
|
76
|
+
const workbook = new ExcelJS.Workbook()
|
|
77
|
+
await workbook.xlsx.load(buffer)
|
|
78
|
+
|
|
79
|
+
const sheetIndex = config.parseOptions?.xlsx?.sheetIndex ?? 0
|
|
80
|
+
const headerRow = config.parseOptions?.xlsx?.headerRow ?? 1
|
|
81
|
+
const worksheet = workbook.worksheets[sheetIndex]
|
|
82
|
+
|
|
83
|
+
if (!worksheet || worksheet.rowCount === 0) return []
|
|
84
|
+
|
|
85
|
+
const headers: string[] = []
|
|
86
|
+
worksheet.getRow(headerRow).eachCell((cell, colNumber) => {
|
|
87
|
+
headers[colNumber - 1] = String(cell.value || "")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const data: Record<string, unknown>[] = []
|
|
91
|
+
for (let i = headerRow + 1; i <= worksheet.rowCount; i++) {
|
|
92
|
+
const row = worksheet.getRow(i)
|
|
93
|
+
const obj: Record<string, unknown> = {}
|
|
86
94
|
headers.forEach((header, index) => {
|
|
87
|
-
|
|
95
|
+
const cell = row.getCell(index + 1)
|
|
96
|
+
obj[header] = cell.value ?? ""
|
|
88
97
|
})
|
|
89
|
-
|
|
90
|
-
}
|
|
98
|
+
data.push(obj)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return data
|
|
91
102
|
} catch (error) {
|
|
92
|
-
throw new Error("Erreur lors du parsing du fichier XLSX")
|
|
103
|
+
throw new Error("Erreur lors du parsing du fichier XLSX. Verifiez que exceljs est installe: pnpm add exceljs")
|
|
93
104
|
}
|
|
94
|
-
}, [])
|
|
105
|
+
}, [config.parseOptions?.xlsx?.sheetIndex, config.parseOptions?.xlsx?.headerRow])
|
|
95
106
|
|
|
96
107
|
// Fonction de validation des données
|
|
97
108
|
const validateData = useCallback((data: any[]) => {
|