@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,470 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Badge } from "../../components/badge"
|
|
4
|
+
import { Button } from "../../components/button"
|
|
5
|
+
import { Card } from "../../components/card"
|
|
6
|
+
import { Progress } from "../../components/progress"
|
|
7
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
8
|
+
import {
|
|
9
|
+
Database,
|
|
10
|
+
RefreshCw,
|
|
11
|
+
Calendar,
|
|
12
|
+
FileText,
|
|
13
|
+
HardDrive,
|
|
14
|
+
Clock,
|
|
15
|
+
Download,
|
|
16
|
+
CheckCircle2,
|
|
17
|
+
XCircle,
|
|
18
|
+
Loader2,
|
|
19
|
+
AlertCircle,
|
|
20
|
+
Globe,
|
|
21
|
+
} from "lucide-react"
|
|
22
|
+
import { cn } from "../../utils/cn"
|
|
23
|
+
|
|
24
|
+
export type UpdateStatus = "success" | "failed" | "in_progress" | "scheduled"
|
|
25
|
+
|
|
26
|
+
export interface SignatureDatabase {
|
|
27
|
+
name: string
|
|
28
|
+
version: string
|
|
29
|
+
signatureCount: number
|
|
30
|
+
size: number
|
|
31
|
+
lastUpdated: Date
|
|
32
|
+
buildTime: Date
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SignatureUpdate {
|
|
36
|
+
id: string
|
|
37
|
+
date: Date
|
|
38
|
+
fromVersion: string
|
|
39
|
+
toVersion: string
|
|
40
|
+
newSignatures: number
|
|
41
|
+
removedSignatures: number
|
|
42
|
+
duration: number
|
|
43
|
+
status: UpdateStatus
|
|
44
|
+
source: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FreshclamConfig {
|
|
48
|
+
updateFrequency: number // hours
|
|
49
|
+
mirror: string
|
|
50
|
+
lastCheck: Date
|
|
51
|
+
checksToday: number
|
|
52
|
+
proxyEnabled: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SignatureDatabaseManagerProps {
|
|
56
|
+
databases?: SignatureDatabase[]
|
|
57
|
+
updates?: SignatureUpdate[]
|
|
58
|
+
freshclamConfig?: FreshclamConfig
|
|
59
|
+
totalSignatures?: number
|
|
60
|
+
isUpdating?: boolean
|
|
61
|
+
onUpdateNow?: () => void
|
|
62
|
+
onScheduleUpdate?: (frequency: number) => void
|
|
63
|
+
onViewChangelog?: (updateId: string) => void
|
|
64
|
+
className?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const defaultDatabases: SignatureDatabase[] = [
|
|
68
|
+
{
|
|
69
|
+
name: "main.cvd",
|
|
70
|
+
version: "62",
|
|
71
|
+
signatureCount: 6647427,
|
|
72
|
+
size: 162.5 * 1024 * 1024,
|
|
73
|
+
lastUpdated: new Date("2024-12-15T10:30:00"),
|
|
74
|
+
buildTime: new Date("2024-12-15T08:00:00"),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "daily.cvd",
|
|
78
|
+
version: "27458",
|
|
79
|
+
signatureCount: 2145789,
|
|
80
|
+
size: 89.3 * 1024 * 1024,
|
|
81
|
+
lastUpdated: new Date("2025-01-15T14:20:00"),
|
|
82
|
+
buildTime: new Date("2025-01-15T12:00:00"),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "bytecode.cvd",
|
|
86
|
+
version: "334",
|
|
87
|
+
signatureCount: 52438,
|
|
88
|
+
size: 2.8 * 1024 * 1024,
|
|
89
|
+
lastUpdated: new Date("2025-01-10T09:15:00"),
|
|
90
|
+
buildTime: new Date("2025-01-10T06:00:00"),
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const defaultUpdates: SignatureUpdate[] = [
|
|
95
|
+
{
|
|
96
|
+
id: "upd-1",
|
|
97
|
+
date: new Date("2025-01-15T14:20:00"),
|
|
98
|
+
fromVersion: "27457",
|
|
99
|
+
toVersion: "27458",
|
|
100
|
+
newSignatures: 1247,
|
|
101
|
+
removedSignatures: 89,
|
|
102
|
+
duration: 45,
|
|
103
|
+
status: "success",
|
|
104
|
+
source: "daily.cvd",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "upd-2",
|
|
108
|
+
date: new Date("2025-01-14T14:15:00"),
|
|
109
|
+
fromVersion: "27456",
|
|
110
|
+
toVersion: "27457",
|
|
111
|
+
newSignatures: 2134,
|
|
112
|
+
removedSignatures: 156,
|
|
113
|
+
duration: 52,
|
|
114
|
+
status: "success",
|
|
115
|
+
source: "daily.cvd",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "upd-3",
|
|
119
|
+
date: new Date("2025-01-13T14:10:00"),
|
|
120
|
+
fromVersion: "27455",
|
|
121
|
+
toVersion: "27456",
|
|
122
|
+
newSignatures: 1876,
|
|
123
|
+
removedSignatures: 203,
|
|
124
|
+
duration: 48,
|
|
125
|
+
status: "success",
|
|
126
|
+
source: "daily.cvd",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "upd-4",
|
|
130
|
+
date: new Date("2025-01-12T14:05:00"),
|
|
131
|
+
fromVersion: "27454",
|
|
132
|
+
toVersion: "27455",
|
|
133
|
+
newSignatures: 0,
|
|
134
|
+
removedSignatures: 0,
|
|
135
|
+
duration: 12,
|
|
136
|
+
status: "failed",
|
|
137
|
+
source: "daily.cvd",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "upd-5",
|
|
141
|
+
date: new Date("2025-01-11T14:00:00"),
|
|
142
|
+
fromVersion: "27453",
|
|
143
|
+
toVersion: "27454",
|
|
144
|
+
newSignatures: 3245,
|
|
145
|
+
removedSignatures: 178,
|
|
146
|
+
duration: 67,
|
|
147
|
+
status: "success",
|
|
148
|
+
source: "daily.cvd",
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
const defaultFreshclamConfig: FreshclamConfig = {
|
|
153
|
+
updateFrequency: 24,
|
|
154
|
+
mirror: "database.clamav.net",
|
|
155
|
+
lastCheck: new Date("2025-01-15T14:20:00"),
|
|
156
|
+
checksToday: 12,
|
|
157
|
+
proxyEnabled: false,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const formatBytes = (bytes: number): string => {
|
|
161
|
+
if (bytes < 1024) return bytes + " B"
|
|
162
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"
|
|
163
|
+
return (bytes / (1024 * 1024)).toFixed(2) + " MB"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const formatDate = (date: Date): string => {
|
|
167
|
+
return new Intl.DateTimeFormat("fr-FR", {
|
|
168
|
+
year: "numeric",
|
|
169
|
+
month: "short",
|
|
170
|
+
day: "numeric",
|
|
171
|
+
hour: "2-digit",
|
|
172
|
+
minute: "2-digit",
|
|
173
|
+
}).format(date)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const formatNumber = (num: number): string => {
|
|
177
|
+
return new Intl.NumberFormat("fr-FR").format(num)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const getStatusIcon = (status: UpdateStatus) => {
|
|
181
|
+
switch (status) {
|
|
182
|
+
case "success":
|
|
183
|
+
return <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" />
|
|
184
|
+
case "failed":
|
|
185
|
+
return <XCircle className="size-4 text-red-600 dark:text-red-400" />
|
|
186
|
+
case "in_progress":
|
|
187
|
+
return <Loader2 className="size-4 text-blue-600 dark:text-blue-400 animate-spin" />
|
|
188
|
+
case "scheduled":
|
|
189
|
+
return <Clock className="size-4 text-amber-600 dark:text-amber-400" />
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const getStatusBadge = (status: UpdateStatus) => {
|
|
194
|
+
switch (status) {
|
|
195
|
+
case "success":
|
|
196
|
+
return <Badge variant="secondary" className="bg-green-500/10 text-green-600 border-green-500/20">Réussie</Badge>
|
|
197
|
+
case "failed":
|
|
198
|
+
return <Badge variant="destructive">Échouée</Badge>
|
|
199
|
+
case "in_progress":
|
|
200
|
+
return <Badge variant="default">En cours</Badge>
|
|
201
|
+
case "scheduled":
|
|
202
|
+
return <Badge variant="secondary" className="bg-yellow-500/10 text-yellow-600 border-yellow-500/20">Planifiée</Badge>
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function SignatureDatabaseManager({
|
|
207
|
+
databases = defaultDatabases,
|
|
208
|
+
updates = defaultUpdates,
|
|
209
|
+
freshclamConfig = defaultFreshclamConfig,
|
|
210
|
+
totalSignatures = databases.reduce((sum, db) => sum + db.signatureCount, 0),
|
|
211
|
+
isUpdating = false,
|
|
212
|
+
onUpdateNow,
|
|
213
|
+
onScheduleUpdate,
|
|
214
|
+
onViewChangelog,
|
|
215
|
+
className,
|
|
216
|
+
}: SignatureDatabaseManagerProps) {
|
|
217
|
+
const latestUpdate = updates[0]
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className={cn("space-y-6", className)}>
|
|
221
|
+
{/* Status actuel */}
|
|
222
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
223
|
+
<Card className="p-6">
|
|
224
|
+
<div className="flex items-center justify-between">
|
|
225
|
+
<div>
|
|
226
|
+
<p className="text-sm font-medium text-muted-foreground">Version DB</p>
|
|
227
|
+
<h3 className="text-2xl font-bold">{latestUpdate?.toVersion || "N/A"}</h3>
|
|
228
|
+
</div>
|
|
229
|
+
<Database className="size-8 text-blue-600 dark:text-blue-400" />
|
|
230
|
+
</div>
|
|
231
|
+
</Card>
|
|
232
|
+
|
|
233
|
+
<Card className="p-6">
|
|
234
|
+
<div className="flex items-center justify-between">
|
|
235
|
+
<div>
|
|
236
|
+
<p className="text-sm font-medium text-muted-foreground">Total Signatures</p>
|
|
237
|
+
<h3 className="text-2xl font-bold">{formatNumber(totalSignatures)}</h3>
|
|
238
|
+
</div>
|
|
239
|
+
<FileText className="size-8 text-purple-600 dark:text-purple-400" />
|
|
240
|
+
</div>
|
|
241
|
+
</Card>
|
|
242
|
+
|
|
243
|
+
<Card className="p-6">
|
|
244
|
+
<div className="flex items-center justify-between">
|
|
245
|
+
<div>
|
|
246
|
+
<p className="text-sm font-medium text-muted-foreground">Dernière MAJ</p>
|
|
247
|
+
<h3 className="text-sm font-bold">
|
|
248
|
+
{latestUpdate ? formatDate(latestUpdate.date) : "N/A"}
|
|
249
|
+
</h3>
|
|
250
|
+
</div>
|
|
251
|
+
<Clock className="size-8 text-green-600 dark:text-green-400" />
|
|
252
|
+
</div>
|
|
253
|
+
</Card>
|
|
254
|
+
|
|
255
|
+
<Card className="p-6">
|
|
256
|
+
<div className="flex items-center justify-between">
|
|
257
|
+
<div>
|
|
258
|
+
<p className="text-sm font-medium text-muted-foreground">Taille Totale</p>
|
|
259
|
+
<h3 className="text-2xl font-bold">
|
|
260
|
+
{formatBytes(databases.reduce((sum, db) => sum + db.size, 0))}
|
|
261
|
+
</h3>
|
|
262
|
+
</div>
|
|
263
|
+
<HardDrive className="size-8 text-amber-600 dark:text-amber-400" />
|
|
264
|
+
</div>
|
|
265
|
+
</Card>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
269
|
+
{/* Bases de données */}
|
|
270
|
+
<Card className="p-6">
|
|
271
|
+
<div className="mb-4 flex items-center justify-between">
|
|
272
|
+
<h3 className="text-lg font-semibold">Bases de données</h3>
|
|
273
|
+
<Badge variant="outline">{databases.length} bases</Badge>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div className="space-y-4">
|
|
277
|
+
{databases.map((db) => (
|
|
278
|
+
<div
|
|
279
|
+
key={db.name}
|
|
280
|
+
className="rounded-lg border border-border bg-muted/50 p-4 space-y-2"
|
|
281
|
+
>
|
|
282
|
+
<div className="flex items-center justify-between">
|
|
283
|
+
<div className="flex items-center gap-2">
|
|
284
|
+
<Database className="size-4 text-muted-foreground" />
|
|
285
|
+
<span className="font-medium">{db.name}</span>
|
|
286
|
+
</div>
|
|
287
|
+
<Badge variant="secondary">v{db.version}</Badge>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
291
|
+
<div>
|
|
292
|
+
<p className="text-muted-foreground">Signatures</p>
|
|
293
|
+
<p className="font-medium">{formatNumber(db.signatureCount)}</p>
|
|
294
|
+
</div>
|
|
295
|
+
<div>
|
|
296
|
+
<p className="text-muted-foreground">Taille</p>
|
|
297
|
+
<p className="font-medium">{formatBytes(db.size)}</p>
|
|
298
|
+
</div>
|
|
299
|
+
<div className="col-span-2">
|
|
300
|
+
<p className="text-muted-foreground">Dernière mise à jour</p>
|
|
301
|
+
<p className="font-medium">{formatDate(db.lastUpdated)}</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
</Card>
|
|
308
|
+
|
|
309
|
+
{/* Freshclam Config */}
|
|
310
|
+
<Card className="p-6">
|
|
311
|
+
<div className="mb-4 flex items-center justify-between">
|
|
312
|
+
<h3 className="text-lg font-semibold">Configuration Freshclam</h3>
|
|
313
|
+
<Button
|
|
314
|
+
size="sm"
|
|
315
|
+
variant="outline"
|
|
316
|
+
onClick={() => onScheduleUpdate?.(freshclamConfig.updateFrequency)}
|
|
317
|
+
>
|
|
318
|
+
<Calendar className="mr-2 size-4" />
|
|
319
|
+
Planifier
|
|
320
|
+
</Button>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div className="space-y-4">
|
|
324
|
+
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/50 p-4">
|
|
325
|
+
<div>
|
|
326
|
+
<p className="text-sm text-muted-foreground">Fréquence de MAJ</p>
|
|
327
|
+
<p className="text-lg font-semibold">
|
|
328
|
+
Toutes les {freshclamConfig.updateFrequency}h
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
<Clock className="size-6 text-blue-600 dark:text-blue-400" />
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/50 p-4">
|
|
335
|
+
<div>
|
|
336
|
+
<p className="text-sm text-muted-foreground">Mirror utilisé</p>
|
|
337
|
+
<p className="text-lg font-semibold">{freshclamConfig.mirror}</p>
|
|
338
|
+
</div>
|
|
339
|
+
<Globe className="size-6 text-purple-600 dark:text-purple-400" />
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/50 p-4">
|
|
343
|
+
<div>
|
|
344
|
+
<p className="text-sm text-muted-foreground">Dernier check</p>
|
|
345
|
+
<p className="text-lg font-semibold">{formatDate(freshclamConfig.lastCheck)}</p>
|
|
346
|
+
</div>
|
|
347
|
+
<RefreshCw className="size-6 text-green-600 dark:text-green-400" />
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/50 p-4">
|
|
351
|
+
<div>
|
|
352
|
+
<p className="text-sm text-muted-foreground">Checks aujourd'hui</p>
|
|
353
|
+
<p className="text-lg font-semibold">{freshclamConfig.checksToday}</p>
|
|
354
|
+
</div>
|
|
355
|
+
<AlertCircle className="size-6 text-amber-600 dark:text-amber-400" />
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{freshclamConfig.proxyEnabled && (
|
|
359
|
+
<div className="rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/50 p-3">
|
|
360
|
+
<div className="flex items-center gap-2 text-sm text-amber-800 dark:text-amber-200">
|
|
361
|
+
<AlertCircle className="size-4" />
|
|
362
|
+
<span>Proxy activé</span>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
<Button
|
|
368
|
+
className="w-full"
|
|
369
|
+
onClick={onUpdateNow}
|
|
370
|
+
disabled={isUpdating}
|
|
371
|
+
>
|
|
372
|
+
{isUpdating ? (
|
|
373
|
+
<>
|
|
374
|
+
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
375
|
+
Mise à jour en cours...
|
|
376
|
+
</>
|
|
377
|
+
) : (
|
|
378
|
+
<>
|
|
379
|
+
<Download className="mr-2 size-4" />
|
|
380
|
+
Mettre à jour maintenant
|
|
381
|
+
</>
|
|
382
|
+
)}
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
</Card>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{/* Historique des MAJ */}
|
|
389
|
+
<Card className="p-6">
|
|
390
|
+
<div className="mb-4 flex items-center justify-between">
|
|
391
|
+
<h3 className="text-lg font-semibold">Historique des mises à jour</h3>
|
|
392
|
+
<Badge variant="outline">{updates.length} mises à jour</Badge>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<ScrollArea className="h-[400px]">
|
|
396
|
+
<div className="space-y-3">
|
|
397
|
+
{updates.map((update, index) => (
|
|
398
|
+
<div
|
|
399
|
+
key={update.id}
|
|
400
|
+
className="relative rounded-lg border border-border bg-muted/50 p-4"
|
|
401
|
+
>
|
|
402
|
+
{index < updates.length - 1 && (
|
|
403
|
+
<div className="absolute left-6 top-12 h-full w-px bg-border" />
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
<div className="flex items-start gap-3">
|
|
407
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-background">
|
|
408
|
+
{getStatusIcon(update.status)}
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div className="flex-1 space-y-2">
|
|
412
|
+
<div className="flex items-center justify-between">
|
|
413
|
+
<div>
|
|
414
|
+
<p className="font-medium">
|
|
415
|
+
{update.source} - v{update.fromVersion} → v{update.toVersion}
|
|
416
|
+
</p>
|
|
417
|
+
<p className="text-sm text-muted-foreground">
|
|
418
|
+
{formatDate(update.date)}
|
|
419
|
+
</p>
|
|
420
|
+
</div>
|
|
421
|
+
{getStatusBadge(update.status)}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{update.status === "success" && (
|
|
425
|
+
<div className="grid grid-cols-3 gap-3 text-sm">
|
|
426
|
+
<div>
|
|
427
|
+
<p className="text-muted-foreground">Ajoutées</p>
|
|
428
|
+
<p className="font-medium text-green-600 dark:text-green-400">
|
|
429
|
+
+{formatNumber(update.newSignatures)}
|
|
430
|
+
</p>
|
|
431
|
+
</div>
|
|
432
|
+
<div>
|
|
433
|
+
<p className="text-muted-foreground">Supprimées</p>
|
|
434
|
+
<p className="font-medium text-red-600 dark:text-red-400">
|
|
435
|
+
-{formatNumber(update.removedSignatures)}
|
|
436
|
+
</p>
|
|
437
|
+
</div>
|
|
438
|
+
<div>
|
|
439
|
+
<p className="text-muted-foreground">Durée</p>
|
|
440
|
+
<p className="font-medium">{update.duration}s</p>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{update.status === "failed" && (
|
|
446
|
+
<div className="rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/50 p-2">
|
|
447
|
+
<p className="text-sm text-red-800 dark:text-red-200">
|
|
448
|
+
Échec de la mise à jour
|
|
449
|
+
</p>
|
|
450
|
+
</div>
|
|
451
|
+
)}
|
|
452
|
+
|
|
453
|
+
<Button
|
|
454
|
+
size="sm"
|
|
455
|
+
variant="ghost"
|
|
456
|
+
onClick={() => onViewChangelog?.(update.id)}
|
|
457
|
+
>
|
|
458
|
+
<FileText className="mr-2 size-3" />
|
|
459
|
+
Voir le changelog
|
|
460
|
+
</Button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
))}
|
|
465
|
+
</div>
|
|
466
|
+
</ScrollArea>
|
|
467
|
+
</Card>
|
|
468
|
+
</div>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
@@ -8,8 +8,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
|
|
8
8
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../components/collapsible"
|
|
9
9
|
import { ScrollArea } from "../../components/scroll-area"
|
|
10
10
|
import { useToast } from "../../hooks/useToast"
|
|
11
|
-
import { useTheme, ShadcnRegistryItem } from "../../context/theme-provider"
|
|
11
|
+
import { useTheme, type ShadcnRegistryItem } from "../../context/theme-provider"
|
|
12
12
|
import { Login, type LoginConfig } from "../login"
|
|
13
|
+
|
|
14
|
+
// Hook sécurisé pour gérer le contexte de thème (optionnel)
|
|
15
|
+
function useThemeSafe() {
|
|
16
|
+
try {
|
|
17
|
+
return useTheme()
|
|
18
|
+
} catch {
|
|
19
|
+
// Le contexte n'est pas disponible, retourner des valeurs par défaut
|
|
20
|
+
return {
|
|
21
|
+
loadThemeFromJSON: async () => {},
|
|
22
|
+
isDarkMode: false,
|
|
23
|
+
toggleDarkMode: () => {},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
import { WakaDashboard, defaultDashboardStats } from "../dashboard"
|
|
14
28
|
import { WakaChat, defaultChatUser, defaultChatConversations, defaultChatMessages } from "../chat"
|
|
15
29
|
import {
|
|
@@ -513,7 +527,7 @@ export function WakaThemeCreatorBlock({
|
|
|
513
527
|
className,
|
|
514
528
|
}: WakaThemeCreatorBlockProps) {
|
|
515
529
|
const { toast } = useToast()
|
|
516
|
-
const { loadThemeFromJSON, isDarkMode, toggleDarkMode } =
|
|
530
|
+
const { loadThemeFromJSON, isDarkMode, toggleDarkMode } = useThemeSafe()
|
|
517
531
|
|
|
518
532
|
// Theme state
|
|
519
533
|
const [currentTheme, setCurrentTheme] = React.useState<ThemeCreatorBlockTheme>(
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { ThreatAlertBanner, ThreatAlert } from "./index"
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Blocks/Antivirus/ThreatAlertBanner",
|
|
6
|
+
component: ThreatAlertBanner,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "padded",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
} satisfies Meta<typeof ThreatAlertBanner>
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof meta>
|
|
15
|
+
|
|
16
|
+
const criticalAlert: ThreatAlert = {
|
|
17
|
+
id: "1",
|
|
18
|
+
threatName: "Ransomware.WannaCry.Variant",
|
|
19
|
+
filename: "important_document.pdf.exe",
|
|
20
|
+
filePath: "/home/user/Documents/important_document.pdf.exe",
|
|
21
|
+
severity: "critical",
|
|
22
|
+
action: "quarantined",
|
|
23
|
+
detectedAt: new Date(Date.now() - 2 * 60000),
|
|
24
|
+
userId: "john.doe@company.com",
|
|
25
|
+
engineVersion: "ClamAV 1.4.1 / 2024-02-12",
|
|
26
|
+
dismissed: false,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const highAlert: ThreatAlert = {
|
|
30
|
+
id: "2",
|
|
31
|
+
threatName: "Trojan.GenericKD.87654321",
|
|
32
|
+
filename: "update.exe",
|
|
33
|
+
filePath: "/var/www/uploads/suspicious/update.exe",
|
|
34
|
+
severity: "high",
|
|
35
|
+
action: "blocked",
|
|
36
|
+
detectedAt: new Date(Date.now() - 10 * 60000),
|
|
37
|
+
userId: "admin@company.com",
|
|
38
|
+
engineVersion: "ClamAV 1.4.1 / 2024-02-12",
|
|
39
|
+
dismissed: false,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mediumAlert: ThreatAlert = {
|
|
43
|
+
id: "3",
|
|
44
|
+
threatName: "PUA.Win.Adware.BundleInstaller",
|
|
45
|
+
filename: "freeware_installer.exe",
|
|
46
|
+
filePath: "/tmp/downloads/freeware_installer.exe",
|
|
47
|
+
severity: "medium",
|
|
48
|
+
action: "deleted",
|
|
49
|
+
detectedAt: new Date(Date.now() - 30 * 60000),
|
|
50
|
+
userId: "user@company.com",
|
|
51
|
+
engineVersion: "ClamAV 1.4.1 / 2024-02-12",
|
|
52
|
+
dismissed: false,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lowAlert: ThreatAlert = {
|
|
56
|
+
id: "4",
|
|
57
|
+
threatName: "Test.EICAR.Signature",
|
|
58
|
+
filename: "eicar.com",
|
|
59
|
+
filePath: "/home/user/test/eicar.com",
|
|
60
|
+
severity: "low",
|
|
61
|
+
action: "allowed",
|
|
62
|
+
detectedAt: new Date(Date.now() - 60 * 60000),
|
|
63
|
+
userId: "security-team@company.com",
|
|
64
|
+
engineVersion: "ClamAV 1.4.1 / 2024-02-12",
|
|
65
|
+
dismissed: false,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Critical: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
alerts: [criticalAlert],
|
|
71
|
+
onDismiss: (alertId: string) => {
|
|
72
|
+
console.log("Dismiss alert:", alertId)
|
|
73
|
+
},
|
|
74
|
+
onViewDetails: (alertId: string) => {
|
|
75
|
+
console.log("View details:", alertId)
|
|
76
|
+
},
|
|
77
|
+
onViewQuarantine: () => {
|
|
78
|
+
console.log("View quarantine")
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const MultipleAlerts: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
alerts: [criticalAlert, highAlert, mediumAlert, lowAlert],
|
|
86
|
+
maxVisible: 3,
|
|
87
|
+
onDismiss: (alertId: string) => {
|
|
88
|
+
console.log("Dismiss alert:", alertId)
|
|
89
|
+
},
|
|
90
|
+
onDismissAll: () => {
|
|
91
|
+
console.log("Dismiss all alerts")
|
|
92
|
+
},
|
|
93
|
+
onViewDetails: (alertId: string) => {
|
|
94
|
+
console.log("View details:", alertId)
|
|
95
|
+
},
|
|
96
|
+
onViewQuarantine: () => {
|
|
97
|
+
console.log("View quarantine")
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const LowSeverity: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
alerts: [lowAlert],
|
|
105
|
+
onDismiss: (alertId: string) => {
|
|
106
|
+
console.log("Dismiss alert:", alertId)
|
|
107
|
+
},
|
|
108
|
+
onViewDetails: (alertId: string) => {
|
|
109
|
+
console.log("View details:", alertId)
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const AllDismissed: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
alerts: [
|
|
117
|
+
{ ...criticalAlert, dismissed: true },
|
|
118
|
+
{ ...highAlert, dismissed: true },
|
|
119
|
+
{ ...mediumAlert, dismissed: true },
|
|
120
|
+
{ ...lowAlert, dismissed: true },
|
|
121
|
+
],
|
|
122
|
+
onDismiss: (alertId: string) => {
|
|
123
|
+
console.log("Dismiss alert:", alertId)
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const ManyAlerts: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
alerts: [
|
|
131
|
+
criticalAlert,
|
|
132
|
+
{ ...highAlert, id: "5", detectedAt: new Date(Date.now() - 5 * 60000) },
|
|
133
|
+
{ ...highAlert, id: "6", filename: "malware.zip", detectedAt: new Date(Date.now() - 8 * 60000) },
|
|
134
|
+
mediumAlert,
|
|
135
|
+
{ ...mediumAlert, id: "7", filename: "suspicious.js", detectedAt: new Date(Date.now() - 45 * 60000) },
|
|
136
|
+
lowAlert,
|
|
137
|
+
],
|
|
138
|
+
maxVisible: 3,
|
|
139
|
+
onDismiss: (alertId: string) => {
|
|
140
|
+
console.log("Dismiss alert:", alertId)
|
|
141
|
+
},
|
|
142
|
+
onDismissAll: () => {
|
|
143
|
+
console.log("Dismiss all alerts")
|
|
144
|
+
},
|
|
145
|
+
onViewDetails: (alertId: string) => {
|
|
146
|
+
console.log("View details:", alertId)
|
|
147
|
+
},
|
|
148
|
+
onViewQuarantine: () => {
|
|
149
|
+
console.log("View quarantine")
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}
|