@arch-cadre/panel 1.0.7 → 1.0.10
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/ui/activity-log/pages/log-list.cjs +2 -2
- package/dist/ui/components/profile/components.cjs +2 -2
- package/dist/ui/components/profile/components.d.ts +1 -1
- package/dist/ui/components/profile/components.mjs +1 -1
- package/dist/ui/rbac/pages/rbac-admin.cjs +14 -14
- package/dist/ui/session-manager/components/sessions-list.cjs +5 -5
- package/dist/ui/session-manager/components/sessions-list.mjs +1 -1
- package/dist/ui/session-manager/pages/sessions-page.cjs +3 -3
- package/dist/ui/session-manager/pages/sessions-page.mjs +1 -1
- package/package.json +7 -6
- package/src/actions/actions.ts +17 -0
- package/src/actions/activity-log/index.ts +17 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/manager.ts +168 -0
- package/src/actions/profile.ts +173 -0
- package/src/actions/rbac/index.ts +131 -0
- package/src/actions/session-manager/index.ts +87 -0
- package/src/actions/settings.ts +34 -0
- package/src/index.ts +135 -0
- package/src/intl.d.ts +9 -0
- package/src/navigation.ts +57 -0
- package/src/routes.ts +107 -0
- package/src/schema/activity-log.ts +16 -0
- package/src/schema.ts +1 -0
- package/src/types.ts +18 -0
- package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
- package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
- package/src/ui/activity-log/pages/log-list.tsx +91 -0
- package/src/ui/components/app-content.tsx +51 -0
- package/src/ui/components/app-header.tsx +65 -0
- package/src/ui/components/app-sidebar.tsx +249 -0
- package/src/ui/components/app-user.tsx +126 -0
- package/src/ui/components/breadcrumb-slot.tsx +52 -0
- package/src/ui/components/manager/module-card.tsx +327 -0
- package/src/ui/components/manager/module-list.tsx +59 -0
- package/src/ui/components/manager/module-upload.tsx +84 -0
- package/src/ui/components/profile/components.tsx +311 -0
- package/src/ui/components/profile/link.tsx +36 -0
- package/src/ui/components/profile/page.tsx +45 -0
- package/src/ui/components/sidebar-slot.tsx +47 -0
- package/src/ui/dashboard/page.tsx +17 -0
- package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
- package/src/ui/error.tsx +82 -0
- package/src/ui/layout.tsx +54 -0
- package/src/ui/modules/docs/page.tsx +105 -0
- package/src/ui/modules/page.tsx +30 -0
- package/src/ui/page.tsx +15 -0
- package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
- package/src/ui/router.tsx +69 -0
- package/src/ui/session-manager/components/sessions-list.tsx +303 -0
- package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
- package/src/ui/settings/page.tsx +73 -0
- package/src/ui/settings-page.tsx +97 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbItem,
|
|
7
|
+
BreadcrumbList,
|
|
8
|
+
BreadcrumbPage,
|
|
9
|
+
BreadcrumbSeparator,
|
|
10
|
+
} from "@arch-cadre/ui/components/breadcrumb";
|
|
11
|
+
import {
|
|
12
|
+
Tooltip,
|
|
13
|
+
TooltipContent,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "@arch-cadre/ui/components/tooltip";
|
|
16
|
+
import Image from "next/image";
|
|
17
|
+
import Link from "next/link";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
|
|
20
|
+
export default function BreadcrumbSlot() {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Breadcrumb className="hidden sm:block">
|
|
25
|
+
<BreadcrumbList>
|
|
26
|
+
<BreadcrumbItem>
|
|
27
|
+
<Tooltip>
|
|
28
|
+
<TooltipTrigger asChild>
|
|
29
|
+
<Link href="/kryo">
|
|
30
|
+
<Image
|
|
31
|
+
width={16}
|
|
32
|
+
height={16}
|
|
33
|
+
src="/favicon.svg"
|
|
34
|
+
alt="logo"
|
|
35
|
+
className="size-4"
|
|
36
|
+
aria-hidden={true}
|
|
37
|
+
/>
|
|
38
|
+
</Link>
|
|
39
|
+
</TooltipTrigger>
|
|
40
|
+
<TooltipContent>
|
|
41
|
+
<p>{t("Back to Dashboard")}</p>
|
|
42
|
+
</TooltipContent>
|
|
43
|
+
</Tooltip>
|
|
44
|
+
</BreadcrumbItem>
|
|
45
|
+
<BreadcrumbSeparator>•</BreadcrumbSeparator>
|
|
46
|
+
<BreadcrumbItem>
|
|
47
|
+
<BreadcrumbPage>{t("Dashboard")}</BreadcrumbPage>
|
|
48
|
+
</BreadcrumbItem>
|
|
49
|
+
</BreadcrumbList>
|
|
50
|
+
</Breadcrumb>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { eventBus } from "@arch-cadre/core";
|
|
4
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
5
|
+
import type { ModuleManifest } from "@arch-cadre/modules";
|
|
6
|
+
import { Badge } from "@arch-cadre/ui/components/badge";
|
|
7
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@arch-cadre/ui/components/card";
|
|
14
|
+
import { Switch } from "@arch-cadre/ui/components/switch";
|
|
15
|
+
import { Loader } from "@arch-cadre/ui/shared/loader";
|
|
16
|
+
import { AlertCircle, FileText, ShieldCheck } from "lucide-react";
|
|
17
|
+
import Link from "next/link";
|
|
18
|
+
import { useRouter } from "next/navigation";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { toast } from "sonner";
|
|
22
|
+
import { toggleModuleAction } from "../../../actions/manager";
|
|
23
|
+
|
|
24
|
+
interface ModuleStatusCheckEvent {
|
|
25
|
+
triggerSource: string;
|
|
26
|
+
dependencies: string[];
|
|
27
|
+
action: "enable" | "disable";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ModuleCardProps {
|
|
31
|
+
module: ModuleManifest & {
|
|
32
|
+
installed?: boolean;
|
|
33
|
+
hasDocs?: boolean;
|
|
34
|
+
lastStep?: string | null;
|
|
35
|
+
isNpm?: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ModuleCard({ module }: ModuleCardProps) {
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const { t } = useTranslation();
|
|
42
|
+
const [isEnabled, setIsEnabled] = useState(module.enabled);
|
|
43
|
+
const [isProcessing, setIsProcessing] = useState(!!module.lastStep);
|
|
44
|
+
const [localStep, setLocalStep] = useState<string | null>(
|
|
45
|
+
module.lastStep || null,
|
|
46
|
+
);
|
|
47
|
+
const pollerRef = useRef<NodeJS.Timeout | null>(null);
|
|
48
|
+
|
|
49
|
+
const stopPolling = useCallback(() => {
|
|
50
|
+
if (pollerRef.current) {
|
|
51
|
+
clearInterval(pollerRef.current);
|
|
52
|
+
pollerRef.current = null;
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const startPolling = useCallback(() => {
|
|
57
|
+
if (pollerRef.current) return;
|
|
58
|
+
|
|
59
|
+
pollerRef.current = setInterval(async () => {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(`/api/system/modules/${module.id}/status`);
|
|
62
|
+
const status = await res.json();
|
|
63
|
+
|
|
64
|
+
// Jeśli serwer zwrócił konkretny krok, ustawiamy go
|
|
65
|
+
if (status.lastStep) {
|
|
66
|
+
setLocalStep(status.lastStep);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Jeśli lastStep zniknął, proces się zakończył
|
|
70
|
+
if (!status.lastStep) {
|
|
71
|
+
setIsProcessing(false);
|
|
72
|
+
setLocalStep(null);
|
|
73
|
+
stopPolling();
|
|
74
|
+
router.refresh();
|
|
75
|
+
} else if (status.lastStep.toLowerCase().includes("error")) {
|
|
76
|
+
setIsProcessing(false);
|
|
77
|
+
stopPolling();
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error("Polling error", e);
|
|
81
|
+
}
|
|
82
|
+
}, 1000);
|
|
83
|
+
}, [module.id, router, stopPolling]);
|
|
84
|
+
|
|
85
|
+
// Subskrypcja EventBus
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const eventType = "module:status:check";
|
|
88
|
+
const subscriberId = `card-${module.id}`;
|
|
89
|
+
|
|
90
|
+
eventBus.subscribe<ModuleStatusCheckEvent>(
|
|
91
|
+
eventType,
|
|
92
|
+
subscriberId,
|
|
93
|
+
(event) => {
|
|
94
|
+
const { triggerSource, dependencies, action } = event.payload;
|
|
95
|
+
// Ignorujemy jeśli to my wywołaliśmy akcję LUB jeśli jesteśmy modułem systemowym
|
|
96
|
+
if (triggerSource === module.id || module.system) return;
|
|
97
|
+
|
|
98
|
+
let shouldCheck = false;
|
|
99
|
+
|
|
100
|
+
if (action === "enable") {
|
|
101
|
+
// Jeśli ktoś włącza moduł, który zależy ode mnie, ja muszę zostać sprawdzony/zaktualizowany pierwszy
|
|
102
|
+
if (dependencies.includes(module.id)) {
|
|
103
|
+
shouldCheck = true;
|
|
104
|
+
}
|
|
105
|
+
} else if (action === "disable") {
|
|
106
|
+
// Jeśli ktoś wyłącza moduł, od którego JA zależę, ja też zostanę wyłączony
|
|
107
|
+
if (module.dependencies?.includes(triggerSource)) {
|
|
108
|
+
shouldCheck = true;
|
|
109
|
+
}
|
|
110
|
+
// Jeśli ktoś wyłącza moduł, który zależał ode mnie, ja MOGĘ zostać wyłączony (jeśli nikt inny mnie nie chce)
|
|
111
|
+
if (dependencies.includes(module.id)) {
|
|
112
|
+
shouldCheck = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (shouldCheck && !isProcessing) {
|
|
117
|
+
setIsProcessing(true);
|
|
118
|
+
setLocalStep(t("Waiting..."));
|
|
119
|
+
startPolling();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return () => eventBus.unsubscribe(eventType, subscriberId);
|
|
125
|
+
}, [
|
|
126
|
+
module.id,
|
|
127
|
+
module.dependencies,
|
|
128
|
+
isProcessing,
|
|
129
|
+
module.system,
|
|
130
|
+
startPolling,
|
|
131
|
+
t,
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Uruchom polling jeśli moduł przyszedł z serwera jako "w trakcie"
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (module.lastStep) {
|
|
137
|
+
setIsProcessing(true);
|
|
138
|
+
setLocalStep(module.lastStep);
|
|
139
|
+
startPolling();
|
|
140
|
+
}
|
|
141
|
+
return () => stopPolling();
|
|
142
|
+
}, [module.lastStep, startPolling, stopPolling]);
|
|
143
|
+
|
|
144
|
+
// Synchronizacja isEnabled przy zmianie propsów (np. po refresh)
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setIsEnabled(module.enabled);
|
|
147
|
+
}, [module.enabled]);
|
|
148
|
+
|
|
149
|
+
const handleToggle = async (checked: boolean) => {
|
|
150
|
+
setIsProcessing(true);
|
|
151
|
+
setLocalStep(t("Starting..."));
|
|
152
|
+
setIsEnabled(checked);
|
|
153
|
+
|
|
154
|
+
// Publikowanie zdarzenia przez EventBus
|
|
155
|
+
eventBus.publish(
|
|
156
|
+
"module:status:check",
|
|
157
|
+
{
|
|
158
|
+
triggerSource: module.id,
|
|
159
|
+
dependencies: module.dependencies || [],
|
|
160
|
+
action: checked ? "enable" : "disable",
|
|
161
|
+
},
|
|
162
|
+
`card-${module.id}`,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await toggleModuleAction(module.id, checked);
|
|
167
|
+
startPolling();
|
|
168
|
+
} catch (_error) {
|
|
169
|
+
setIsEnabled(!checked);
|
|
170
|
+
setIsProcessing(false);
|
|
171
|
+
setLocalStep(null);
|
|
172
|
+
toast.error(t("error_occurred"));
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Card
|
|
178
|
+
className={`relative overflow-hidden transition-all duration-300 ${!isEnabled && !module.system ? "opacity-75 shadow-none border-dashed" : "shadow-md"}`}
|
|
179
|
+
>
|
|
180
|
+
{isProcessing && (
|
|
181
|
+
<div className="absolute top-0 left-0 w-full h-1 bg-primary/20 overflow-hidden">
|
|
182
|
+
<div
|
|
183
|
+
className={`h-full bg-primary ${localStep?.startsWith("Waiting") ? "animate-pulse opacity-50" : "animate-progress-fast"}`}
|
|
184
|
+
style={{ width: localStep?.startsWith("Waiting") ? "100%" : "40%" }}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
190
|
+
<CardTitle className="text-base font-medium flex items-center gap-2">
|
|
191
|
+
{module.name}
|
|
192
|
+
{module.system && <ShieldCheck className="size-4 text-primary" />}
|
|
193
|
+
</CardTitle>
|
|
194
|
+
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
{module.hasDocs && (
|
|
197
|
+
<Button
|
|
198
|
+
asChild
|
|
199
|
+
variant="ghost"
|
|
200
|
+
size="icon"
|
|
201
|
+
className="h-8 w-8"
|
|
202
|
+
title={t("View documentation")}
|
|
203
|
+
>
|
|
204
|
+
<Link href={`/kryo/modules/docs/${module.id}`}>
|
|
205
|
+
<FileText className="size-4" />
|
|
206
|
+
</Link>
|
|
207
|
+
</Button>
|
|
208
|
+
)}
|
|
209
|
+
{!module.system && (
|
|
210
|
+
<Switch
|
|
211
|
+
checked={isEnabled}
|
|
212
|
+
onCheckedChange={handleToggle}
|
|
213
|
+
disabled={isProcessing}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</CardHeader>
|
|
218
|
+
<CardContent>
|
|
219
|
+
<div className="flex items-center gap-2 mb-4">
|
|
220
|
+
<Badge variant={isEnabled || module.system ? "default" : "secondary"}>
|
|
221
|
+
{isProcessing ? (
|
|
222
|
+
<span className="flex items-center gap-1">
|
|
223
|
+
<Loader
|
|
224
|
+
variant={isEnabled || module.system ? "dark" : "default"}
|
|
225
|
+
className="size-3"
|
|
226
|
+
/>
|
|
227
|
+
{localStep?.startsWith("Waiting")
|
|
228
|
+
? t("Please wait...")
|
|
229
|
+
: t("Working...")}
|
|
230
|
+
</span>
|
|
231
|
+
) : isEnabled || module.system ? (
|
|
232
|
+
t("Enabled")
|
|
233
|
+
) : (
|
|
234
|
+
t("Disabled")
|
|
235
|
+
)}
|
|
236
|
+
</Badge>
|
|
237
|
+
{module.installed && (
|
|
238
|
+
<Badge variant="outline" className="text-[10px] h-5 font-mono">
|
|
239
|
+
{t("Installed")}
|
|
240
|
+
</Badge>
|
|
241
|
+
)}
|
|
242
|
+
{module.isNpm && (
|
|
243
|
+
<Badge
|
|
244
|
+
variant="secondary"
|
|
245
|
+
className="text-[10px] h-5 font-mono bg-blue-500/10 text-blue-600 border-blue-500/20"
|
|
246
|
+
>
|
|
247
|
+
NPM
|
|
248
|
+
</Badge>
|
|
249
|
+
)}
|
|
250
|
+
<span className="text-xs text-muted-foreground ml-auto">
|
|
251
|
+
v{module.version}
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{isProcessing || localStep ? (
|
|
256
|
+
<div
|
|
257
|
+
className={`p-2 rounded-lg border text-[11px] font-mono mb-2 flex items-center gap-2 ${localStep?.toLowerCase().includes("error") ? "bg-destructive/10 text-destructive border-destructive/20" : localStep?.startsWith("Waiting") ? "bg-amber-500/10 text-amber-600 border-amber-500/20" : "bg-muted/50 text-muted-foreground"}`}
|
|
258
|
+
>
|
|
259
|
+
{localStep?.toLowerCase().includes("error") ? (
|
|
260
|
+
<AlertCircle className="size-3" />
|
|
261
|
+
) : localStep?.startsWith("Waiting") ? (
|
|
262
|
+
<Loader className="size-3 animate-spin opacity-50" />
|
|
263
|
+
) : (
|
|
264
|
+
<Loader className="size-3 animate-spin" />
|
|
265
|
+
)}
|
|
266
|
+
<span className="truncate">{localStep || t("Processing")}</span>
|
|
267
|
+
</div>
|
|
268
|
+
) : (
|
|
269
|
+
<div className="space-y-3">
|
|
270
|
+
<p className="text-sm text-muted-foreground line-clamp-2 h-[40px]">
|
|
271
|
+
{t(module.description!)}
|
|
272
|
+
</p>
|
|
273
|
+
|
|
274
|
+
<div className="flex flex-wrap gap-y-2 gap-x-4 pt-2 border-t border-muted/50">
|
|
275
|
+
{(!module.extends || module.extends.length === 0) &&
|
|
276
|
+
(!module.dependencies || module.dependencies.length === 0) ? (
|
|
277
|
+
<div className="flex items-center gap-1.5">
|
|
278
|
+
<span className="text-[10px] font-black uppercase tracking-tight text-muted-foreground/40 italic">
|
|
279
|
+
{t("No dependencies")}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
) : (
|
|
283
|
+
<>
|
|
284
|
+
{module.extends && module.extends.length > 0 && (
|
|
285
|
+
<div className="flex items-center gap-1.5">
|
|
286
|
+
<span className="text-[10px] font-black uppercase tracking-tight text-primary/70">
|
|
287
|
+
{t("Extends")}
|
|
288
|
+
</span>
|
|
289
|
+
<div className="flex gap-1">
|
|
290
|
+
{module.extends.map((ext) => (
|
|
291
|
+
<span
|
|
292
|
+
key={ext}
|
|
293
|
+
className="text-[10px] font-mono bg-primary/5 text-primary px-1 rounded border border-primary/10"
|
|
294
|
+
>
|
|
295
|
+
{ext}
|
|
296
|
+
</span>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{module.dependencies && module.dependencies.length > 0 && (
|
|
303
|
+
<div className="flex items-center gap-1.5">
|
|
304
|
+
<span className="text-[10px] font-black uppercase tracking-tight text-muted-foreground">
|
|
305
|
+
{t("Requires")}
|
|
306
|
+
</span>
|
|
307
|
+
<div className="flex gap-1">
|
|
308
|
+
{module.dependencies.map((dep) => (
|
|
309
|
+
<span
|
|
310
|
+
key={dep}
|
|
311
|
+
className="text-[10px] font-mono bg-muted text-muted-foreground px-1 rounded border"
|
|
312
|
+
>
|
|
313
|
+
{dep}
|
|
314
|
+
</span>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</CardContent>
|
|
325
|
+
</Card>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
|
|
5
|
+
import { Separator } from "@arch-cadre/ui/components/separator";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { ModuleCard } from "./module-card";
|
|
8
|
+
|
|
9
|
+
export function ModuleMarketplaceList({
|
|
10
|
+
initialModules,
|
|
11
|
+
}: {
|
|
12
|
+
initialModules: any[];
|
|
13
|
+
}) {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
// Sortowanie i filtrowanie
|
|
16
|
+
const sortedModules = [...initialModules].sort((a, b) =>
|
|
17
|
+
a.name.localeCompare(b.name),
|
|
18
|
+
);
|
|
19
|
+
const coreModules = sortedModules.filter((m) => m.system);
|
|
20
|
+
const publicModules = sortedModules.filter((m) => !m.system);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-10">
|
|
24
|
+
{/* Sekcja Public */}
|
|
25
|
+
{publicModules.length > 0 && (
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
<div className="flex items-center gap-4">
|
|
28
|
+
<h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground shrink-0">
|
|
29
|
+
{t("Public Modules")}
|
|
30
|
+
</h3>
|
|
31
|
+
<Separator className="flex-1" />
|
|
32
|
+
</div>
|
|
33
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
34
|
+
{publicModules.map((module) => (
|
|
35
|
+
<ModuleCard key={module.id} module={module} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{/* Sekcja Core */}
|
|
42
|
+
{coreModules.length > 0 && (
|
|
43
|
+
<div className="space-y-6">
|
|
44
|
+
<div className="flex items-center gap-4">
|
|
45
|
+
<h3 className="text-sm font-bold uppercase tracking-widest text-primary shrink-0">
|
|
46
|
+
{t("Core Modules")}
|
|
47
|
+
</h3>
|
|
48
|
+
<Separator className="flex-1" />
|
|
49
|
+
</div>
|
|
50
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
51
|
+
{coreModules.map((module) => (
|
|
52
|
+
<ModuleCard key={module.id} module={module} />
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
6
|
+
import { Loader2, Lock, Upload } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import {
|
|
11
|
+
checkDiskWriteAccess,
|
|
12
|
+
uploadModuleAction,
|
|
13
|
+
} from "../../../actions/manager";
|
|
14
|
+
|
|
15
|
+
export function ModuleUpload() {
|
|
16
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
17
|
+
const [canWrite, setCanWrite] = useState<boolean | null>(null);
|
|
18
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
checkDiskWriteAccess().then((res) => setCanWrite(res.canWrite));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
26
|
+
const file = e.target.files?.[0];
|
|
27
|
+
if (!file) return;
|
|
28
|
+
|
|
29
|
+
if (!file.name.endsWith(".zip")) {
|
|
30
|
+
toast.error(t("Invalid file"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const formData = new FormData();
|
|
35
|
+
formData.append("file", file);
|
|
36
|
+
|
|
37
|
+
setIsUploading(true);
|
|
38
|
+
const id = toast.loading(t("Uploading..."));
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await uploadModuleAction(formData);
|
|
42
|
+
if (result.success) {
|
|
43
|
+
toast.success(t("Upload successful"), { id });
|
|
44
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
45
|
+
} else {
|
|
46
|
+
toast.error(result.error || t("Upload error"), { id });
|
|
47
|
+
}
|
|
48
|
+
} catch (_error) {
|
|
49
|
+
toast.error(t("error_occurred"), { id });
|
|
50
|
+
} finally {
|
|
51
|
+
setIsUploading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex items-center gap-4">
|
|
57
|
+
<input
|
|
58
|
+
type="file"
|
|
59
|
+
accept=".zip"
|
|
60
|
+
className="hidden"
|
|
61
|
+
ref={fileInputRef}
|
|
62
|
+
onChange={handleUpload}
|
|
63
|
+
disabled={isUploading}
|
|
64
|
+
/>
|
|
65
|
+
<Button
|
|
66
|
+
onClick={() => fileInputRef.current?.click()}
|
|
67
|
+
disabled={isUploading || canWrite === false}
|
|
68
|
+
variant="outline"
|
|
69
|
+
size="sm"
|
|
70
|
+
className="gap-2 w-full sm:w-auto"
|
|
71
|
+
title={canWrite === false ? t("No write access") : undefined}
|
|
72
|
+
>
|
|
73
|
+
{isUploading ? (
|
|
74
|
+
<Loader2 className="size-4 animate-spin" />
|
|
75
|
+
) : canWrite === false ? (
|
|
76
|
+
<Lock className="size-4 text-destructive" />
|
|
77
|
+
) : (
|
|
78
|
+
<Upload className="size-4" />
|
|
79
|
+
)}
|
|
80
|
+
{canWrite === false ? t("Upload disabled") : t("Upload module")}
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|