@hed-hog/developer-mode 0.0.194 → 0.0.197
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/developermode.controller.d.ts.map +1 -1
- package/dist/developermode.controller.js +0 -2
- package/dist/developermode.controller.js.map +1 -1
- package/hedhog/frontend/app/components/data.tsx.ejs +557 -0
- package/hedhog/frontend/app/components/icons.tsx.ejs +159 -0
- package/hedhog/frontend/app/components/sections.tsx.ejs +1251 -0
- package/hedhog/frontend/app/components/types.ts.ejs +86 -0
- package/hedhog/frontend/app/layout.tsx.ejs +10 -0
- package/hedhog/frontend/app/page.tsx.ejs +247 -0
- package/hedhog/frontend/app/provider.tsx.ejs +95 -0
- package/hedhog/frontend/messages/en.json +374 -0
- package/hedhog/frontend/messages/pt.json +374 -0
- package/package.json +3 -3
- package/src/developermode.controller.ts +0 -1
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
2
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from '@/components/ui/card';
|
|
11
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
} from '@/components/ui/dialog';
|
|
19
|
+
import {
|
|
20
|
+
DropdownMenu,
|
|
21
|
+
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuLabel,
|
|
24
|
+
DropdownMenuSeparator,
|
|
25
|
+
DropdownMenuTrigger,
|
|
26
|
+
} from '@/components/ui/dropdown-menu';
|
|
27
|
+
import { Input } from '@/components/ui/input';
|
|
28
|
+
import { Label } from '@/components/ui/label';
|
|
29
|
+
import { Progress } from '@/components/ui/progress';
|
|
30
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
31
|
+
import { cn } from '@/lib/utils';
|
|
32
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
33
|
+
import {
|
|
34
|
+
AlertCircle,
|
|
35
|
+
ArrowLeft,
|
|
36
|
+
ArrowRight,
|
|
37
|
+
ArrowUp,
|
|
38
|
+
Check,
|
|
39
|
+
CheckCircle2,
|
|
40
|
+
Clock,
|
|
41
|
+
Database,
|
|
42
|
+
Download,
|
|
43
|
+
ExternalLink,
|
|
44
|
+
FolderGit2,
|
|
45
|
+
GitBranch,
|
|
46
|
+
GitCommit,
|
|
47
|
+
GitPullRequest,
|
|
48
|
+
HardDrive,
|
|
49
|
+
Loader2,
|
|
50
|
+
MoreVertical,
|
|
51
|
+
Package,
|
|
52
|
+
Play,
|
|
53
|
+
Plus,
|
|
54
|
+
Table2,
|
|
55
|
+
Terminal,
|
|
56
|
+
Zap,
|
|
57
|
+
} from 'lucide-react';
|
|
58
|
+
import { useTranslations } from 'next-intl';
|
|
59
|
+
import * as React from 'react';
|
|
60
|
+
import {
|
|
61
|
+
frameworkIcons,
|
|
62
|
+
getFrameworkLabel,
|
|
63
|
+
getQuickActions,
|
|
64
|
+
getStatusLabel,
|
|
65
|
+
mockStats,
|
|
66
|
+
statusColors,
|
|
67
|
+
variantStyles,
|
|
68
|
+
} from './data';
|
|
69
|
+
import { GitIcon, HedHogIcon, NodeJSIcon, PostgreSQLIcon } from './icons';
|
|
70
|
+
import {
|
|
71
|
+
AppInfo,
|
|
72
|
+
CommitInfo,
|
|
73
|
+
LibraryInfo,
|
|
74
|
+
QuickAction,
|
|
75
|
+
WizardConfig,
|
|
76
|
+
} from './types';
|
|
77
|
+
|
|
78
|
+
// ─── Stat Item ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function StatItem({
|
|
81
|
+
title,
|
|
82
|
+
value,
|
|
83
|
+
icon,
|
|
84
|
+
}: {
|
|
85
|
+
title: string;
|
|
86
|
+
value: string | number;
|
|
87
|
+
icon: React.ReactNode;
|
|
88
|
+
}) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30 border border-border">
|
|
91
|
+
<div className="text-muted-foreground">{icon}</div>
|
|
92
|
+
<div className="min-w-0">
|
|
93
|
+
<p className="text-xs text-muted-foreground truncate">{title}</p>
|
|
94
|
+
<p className="text-sm font-medium text-foreground truncate">{value}</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Stats Cards ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export function StatsCards({ stats }: { stats: typeof mockStats }) {
|
|
103
|
+
const t = useTranslations('developer-mode.Sections');
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Card className="bg-card border-border">
|
|
107
|
+
<CardContent className="p-4">
|
|
108
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-2">
|
|
109
|
+
<StatItem
|
|
110
|
+
title={t('stats.node')}
|
|
111
|
+
value={stats.nodeVersion}
|
|
112
|
+
icon={<NodeJSIcon className="h-4 w-4" />}
|
|
113
|
+
/>
|
|
114
|
+
<StatItem
|
|
115
|
+
title={t('stats.disk')}
|
|
116
|
+
value={stats.diskUsage}
|
|
117
|
+
icon={<HardDrive className="h-4 w-4" />}
|
|
118
|
+
/>
|
|
119
|
+
<StatItem
|
|
120
|
+
title={t('stats.apps')}
|
|
121
|
+
value={stats.totalApps}
|
|
122
|
+
icon={<Package className="h-4 w-4" />}
|
|
123
|
+
/>
|
|
124
|
+
<StatItem
|
|
125
|
+
title={t('stats.libraries')}
|
|
126
|
+
value={stats.totalLibraries}
|
|
127
|
+
icon={<FolderGit2 className="h-4 w-4" />}
|
|
128
|
+
/>
|
|
129
|
+
<StatItem
|
|
130
|
+
title={t('stats.dbConnections')}
|
|
131
|
+
value={stats.dbConnections}
|
|
132
|
+
icon={<Database className="h-4 w-4" />}
|
|
133
|
+
/>
|
|
134
|
+
<StatItem
|
|
135
|
+
title={t('stats.branch')}
|
|
136
|
+
value={stats.gitBranch}
|
|
137
|
+
icon={<GitBranch className="h-4 w-4" />}
|
|
138
|
+
/>
|
|
139
|
+
<StatItem
|
|
140
|
+
title={t('stats.commit')}
|
|
141
|
+
value={stats.lastCommit}
|
|
142
|
+
icon={<GitCommit className="h-4 w-4" />}
|
|
143
|
+
/>
|
|
144
|
+
<StatItem
|
|
145
|
+
title={t('stats.uptime')}
|
|
146
|
+
value={stats.uptime}
|
|
147
|
+
icon={<Clock className="h-4 w-4" />}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Quick Actions Bar ──────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export function QuickActionsBar({
|
|
158
|
+
onAction,
|
|
159
|
+
}: {
|
|
160
|
+
onAction: (action: QuickAction) => void;
|
|
161
|
+
}) {
|
|
162
|
+
const t = useTranslations('developer-mode.Sections');
|
|
163
|
+
const quickActions = React.useMemo(
|
|
164
|
+
() => getQuickActions((key, values) => t(key, values)),
|
|
165
|
+
[t]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Card className="bg-card border-border">
|
|
170
|
+
<CardHeader className="pb-3">
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<Zap className="h-4 w-4 text-accent" />
|
|
174
|
+
<CardTitle className="text-sm text-foreground">
|
|
175
|
+
{t('quickActions.title')}
|
|
176
|
+
</CardTitle>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent className="pt-0">
|
|
181
|
+
<div className="flex flex-wrap gap-2">
|
|
182
|
+
{quickActions.map((action) => (
|
|
183
|
+
<Button
|
|
184
|
+
key={action.id}
|
|
185
|
+
variant="outline"
|
|
186
|
+
size="sm"
|
|
187
|
+
className={cn(
|
|
188
|
+
'gap-2 text-xs bg-transparent border-border transition-all',
|
|
189
|
+
variantStyles[action.variant]
|
|
190
|
+
)}
|
|
191
|
+
onClick={() => onAction(action)}
|
|
192
|
+
title={action.description}
|
|
193
|
+
>
|
|
194
|
+
{action.icon}
|
|
195
|
+
{action.label}
|
|
196
|
+
</Button>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Apps Grid ──────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export function AppsGrid({
|
|
207
|
+
apps,
|
|
208
|
+
onRunScript,
|
|
209
|
+
}: {
|
|
210
|
+
apps: AppInfo[];
|
|
211
|
+
onRunScript: (app: AppInfo, script: string) => void;
|
|
212
|
+
}) {
|
|
213
|
+
const t = useTranslations('developer-mode.Sections');
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Card className="bg-card border-border">
|
|
217
|
+
<CardHeader>
|
|
218
|
+
<div className="flex items-center justify-between">
|
|
219
|
+
<div>
|
|
220
|
+
<CardTitle className="text-foreground">{t('apps.title')}</CardTitle>
|
|
221
|
+
<CardDescription className="text-muted-foreground">
|
|
222
|
+
{t('apps.description')}
|
|
223
|
+
</CardDescription>
|
|
224
|
+
</div>
|
|
225
|
+
<Badge variant="outline" className="font-mono">
|
|
226
|
+
{t('apps.total', { count: apps.length })}
|
|
227
|
+
</Badge>
|
|
228
|
+
</div>
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardContent>
|
|
231
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
232
|
+
{apps.map((app) => (
|
|
233
|
+
<div
|
|
234
|
+
key={app.name}
|
|
235
|
+
className="flex items-start gap-4 p-4 rounded-lg border border-border bg-secondary/30 hover:bg-secondary/50 transition-colors"
|
|
236
|
+
>
|
|
237
|
+
<div className="shrink-0 p-2 rounded-lg bg-background">
|
|
238
|
+
{frameworkIcons[app.framework]}
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex-1 min-w-0">
|
|
241
|
+
<div className="flex items-center gap-2 mb-1">
|
|
242
|
+
<h3 className="font-semibold text-foreground truncate">
|
|
243
|
+
{app.name}
|
|
244
|
+
</h3>
|
|
245
|
+
<Badge
|
|
246
|
+
variant="outline"
|
|
247
|
+
className={cn('text-xs', statusColors[app.status])}
|
|
248
|
+
>
|
|
249
|
+
{getStatusLabel(app.status, (key, values) =>
|
|
250
|
+
t(key, values)
|
|
251
|
+
)}
|
|
252
|
+
</Badge>
|
|
253
|
+
</div>
|
|
254
|
+
<p className="text-sm text-muted-foreground mb-2">
|
|
255
|
+
{getFrameworkLabel(app.framework, (key, values) =>
|
|
256
|
+
t(key, values)
|
|
257
|
+
)}
|
|
258
|
+
{app.database && (
|
|
259
|
+
<span className="inline-flex items-center gap-1 ml-2">
|
|
260
|
+
<PostgreSQLIcon className="h-3 w-3" />
|
|
261
|
+
{app.database.toUpperCase()}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</p>
|
|
265
|
+
<p className="text-xs text-muted-foreground mb-3 line-clamp-2">
|
|
266
|
+
{app.description}
|
|
267
|
+
</p>
|
|
268
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
269
|
+
{app.port && (
|
|
270
|
+
<Button
|
|
271
|
+
variant="outline"
|
|
272
|
+
size="sm"
|
|
273
|
+
className="h-7 text-xs bg-transparent"
|
|
274
|
+
asChild
|
|
275
|
+
>
|
|
276
|
+
<a
|
|
277
|
+
href={`http://localhost:${app.port}`}
|
|
278
|
+
target="_blank"
|
|
279
|
+
rel="noreferrer"
|
|
280
|
+
>
|
|
281
|
+
<ExternalLink className="h-3 w-3 mr-1" />:{app.port}
|
|
282
|
+
</a>
|
|
283
|
+
</Button>
|
|
284
|
+
)}
|
|
285
|
+
<Button
|
|
286
|
+
variant="outline"
|
|
287
|
+
size="sm"
|
|
288
|
+
className="h-7 text-xs bg-transparent"
|
|
289
|
+
onClick={() => onRunScript(app, 'dev')}
|
|
290
|
+
>
|
|
291
|
+
<Play className="h-3 w-3 mr-1" />
|
|
292
|
+
{t('apps.actions.dev')}
|
|
293
|
+
</Button>
|
|
294
|
+
<Button
|
|
295
|
+
variant="outline"
|
|
296
|
+
size="sm"
|
|
297
|
+
className="h-7 text-xs bg-transparent"
|
|
298
|
+
onClick={() => onRunScript(app, 'build')}
|
|
299
|
+
>
|
|
300
|
+
<Terminal className="h-3 w-3 mr-1" />
|
|
301
|
+
{t('apps.actions.build')}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</CardContent>
|
|
309
|
+
</Card>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Libraries List ─────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
export function LibrariesList({
|
|
316
|
+
libraries,
|
|
317
|
+
onRunScript,
|
|
318
|
+
onInstall,
|
|
319
|
+
onUpdate,
|
|
320
|
+
onCreateLibrary,
|
|
321
|
+
}: {
|
|
322
|
+
libraries: LibraryInfo[];
|
|
323
|
+
onRunScript: (library: LibraryInfo, script: string) => void;
|
|
324
|
+
onInstall: (library: LibraryInfo) => void;
|
|
325
|
+
onUpdate: (library: LibraryInfo) => void;
|
|
326
|
+
onCreateLibrary: () => void;
|
|
327
|
+
}) {
|
|
328
|
+
const t = useTranslations('developer-mode.Sections');
|
|
329
|
+
const installedCount = libraries.filter((l) => l.installed).length;
|
|
330
|
+
const updatesAvailable = libraries.filter(
|
|
331
|
+
(l) => l.installed && l.latestVersion && l.version !== l.latestVersion
|
|
332
|
+
).length;
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<Card className="bg-card border-border h-full flex flex-col">
|
|
336
|
+
<CardHeader className="pb-4">
|
|
337
|
+
<div className="flex items-center justify-between">
|
|
338
|
+
<div className="flex items-center gap-3">
|
|
339
|
+
<div className="p-2.5 rounded-xl bg-accent/10 border border-accent/20">
|
|
340
|
+
<HedHogIcon className="h-6 w-6 text-accent" />
|
|
341
|
+
</div>
|
|
342
|
+
<div>
|
|
343
|
+
<CardTitle className="text-lg text-foreground">
|
|
344
|
+
{t('libraries.title')}
|
|
345
|
+
</CardTitle>
|
|
346
|
+
<CardDescription className="text-muted-foreground">
|
|
347
|
+
{t('libraries.installedCount', {
|
|
348
|
+
installed: installedCount,
|
|
349
|
+
total: libraries.length,
|
|
350
|
+
})}
|
|
351
|
+
{updatesAvailable > 0 && (
|
|
352
|
+
<span className="text-warning ml-2">
|
|
353
|
+
{t('libraries.updates', { count: updatesAvailable })}
|
|
354
|
+
</span>
|
|
355
|
+
)}
|
|
356
|
+
</CardDescription>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<Button
|
|
360
|
+
variant="outline"
|
|
361
|
+
size="sm"
|
|
362
|
+
className="gap-2 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
|
|
363
|
+
onClick={onCreateLibrary}
|
|
364
|
+
>
|
|
365
|
+
<Plus className="h-4 w-4" />
|
|
366
|
+
{t('libraries.actions.newLibrary')}
|
|
367
|
+
</Button>
|
|
368
|
+
</div>
|
|
369
|
+
</CardHeader>
|
|
370
|
+
<CardContent className="flex-1 p-0">
|
|
371
|
+
<ScrollArea className="h-[500px]">
|
|
372
|
+
<div className="px-6 pb-6 space-y-3">
|
|
373
|
+
{libraries.map((lib) => {
|
|
374
|
+
const hasUpdate =
|
|
375
|
+
lib.installed &&
|
|
376
|
+
lib.latestVersion &&
|
|
377
|
+
lib.version !== lib.latestVersion;
|
|
378
|
+
return (
|
|
379
|
+
<div
|
|
380
|
+
key={lib.name}
|
|
381
|
+
className={cn(
|
|
382
|
+
'rounded-xl border overflow-hidden transition-all duration-200',
|
|
383
|
+
lib.installed
|
|
384
|
+
? 'border-border bg-secondary/20 hover:bg-secondary/40'
|
|
385
|
+
: 'border-dashed border-border/50 bg-background/30 opacity-70 hover:opacity-100'
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
388
|
+
<div className="p-4">
|
|
389
|
+
<div className="flex items-start gap-4">
|
|
390
|
+
<div
|
|
391
|
+
className={cn(
|
|
392
|
+
'p-2.5 rounded-lg shrink-0',
|
|
393
|
+
lib.installed ? 'bg-accent/10' : 'bg-muted/50'
|
|
394
|
+
)}
|
|
395
|
+
>
|
|
396
|
+
<Package
|
|
397
|
+
className={cn(
|
|
398
|
+
'h-5 w-5',
|
|
399
|
+
lib.installed
|
|
400
|
+
? 'text-accent'
|
|
401
|
+
: 'text-muted-foreground'
|
|
402
|
+
)}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
<div className="flex-1 min-w-0">
|
|
406
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
407
|
+
<span className="font-mono text-sm font-semibold text-foreground">
|
|
408
|
+
@hedhog/{lib.name}
|
|
409
|
+
</span>
|
|
410
|
+
{lib.installed && (
|
|
411
|
+
<Badge
|
|
412
|
+
variant="outline"
|
|
413
|
+
className="text-xs font-mono bg-secondary/50"
|
|
414
|
+
>
|
|
415
|
+
v{lib.version}
|
|
416
|
+
</Badge>
|
|
417
|
+
)}
|
|
418
|
+
{hasUpdate && (
|
|
419
|
+
<Badge className="text-xs bg-warning/20 text-warning border-warning/30 gap-1">
|
|
420
|
+
<ArrowUp className="h-3 w-3" />v
|
|
421
|
+
{lib.latestVersion}
|
|
422
|
+
</Badge>
|
|
423
|
+
)}
|
|
424
|
+
{lib.installed && (
|
|
425
|
+
<Check className="h-4 w-4 text-success ml-auto shrink-0" />
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
|
429
|
+
{lib.description}
|
|
430
|
+
</p>
|
|
431
|
+
<div className="flex items-center gap-2 mt-3">
|
|
432
|
+
{lib.installed ? (
|
|
433
|
+
<>
|
|
434
|
+
{lib.scripts.length > 0 && (
|
|
435
|
+
<DropdownMenu>
|
|
436
|
+
<DropdownMenuTrigger asChild>
|
|
437
|
+
<Button
|
|
438
|
+
variant="outline"
|
|
439
|
+
size="sm"
|
|
440
|
+
className="h-7 text-xs gap-1.5 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
|
|
441
|
+
>
|
|
442
|
+
<Terminal className="h-3 w-3" />
|
|
443
|
+
{t('libraries.actions.scripts')}
|
|
444
|
+
<Badge
|
|
445
|
+
variant="secondary"
|
|
446
|
+
className="ml-1 h-4 px-1 text-[10px]"
|
|
447
|
+
>
|
|
448
|
+
{lib.scripts.length}
|
|
449
|
+
</Badge>
|
|
450
|
+
</Button>
|
|
451
|
+
</DropdownMenuTrigger>
|
|
452
|
+
<DropdownMenuContent
|
|
453
|
+
align="start"
|
|
454
|
+
className="w-56"
|
|
455
|
+
>
|
|
456
|
+
<DropdownMenuLabel className="text-xs">
|
|
457
|
+
{t('libraries.availableScripts')}
|
|
458
|
+
</DropdownMenuLabel>
|
|
459
|
+
<DropdownMenuSeparator />
|
|
460
|
+
{lib.scripts.map((script) => (
|
|
461
|
+
<DropdownMenuItem
|
|
462
|
+
key={script.name}
|
|
463
|
+
className="gap-2 cursor-pointer font-mono text-xs"
|
|
464
|
+
onClick={() =>
|
|
465
|
+
onRunScript(lib, script.name)
|
|
466
|
+
}
|
|
467
|
+
>
|
|
468
|
+
<Play className="h-3 w-3 text-accent" />
|
|
469
|
+
npm run {script.name}
|
|
470
|
+
</DropdownMenuItem>
|
|
471
|
+
))}
|
|
472
|
+
</DropdownMenuContent>
|
|
473
|
+
</DropdownMenu>
|
|
474
|
+
)}
|
|
475
|
+
{hasUpdate && (
|
|
476
|
+
<Button
|
|
477
|
+
variant="outline"
|
|
478
|
+
size="sm"
|
|
479
|
+
className="h-7 text-xs gap-1.5 bg-transparent hover:bg-warning/10 hover:text-warning hover:border-warning/50"
|
|
480
|
+
onClick={() => onUpdate(lib)}
|
|
481
|
+
>
|
|
482
|
+
<ArrowUp className="h-3 w-3" />
|
|
483
|
+
{t('libraries.actions.update')}
|
|
484
|
+
</Button>
|
|
485
|
+
)}
|
|
486
|
+
<DropdownMenu>
|
|
487
|
+
<DropdownMenuTrigger asChild>
|
|
488
|
+
<Button
|
|
489
|
+
variant="ghost"
|
|
490
|
+
size="sm"
|
|
491
|
+
className="h-7 w-7 p-0 ml-auto"
|
|
492
|
+
>
|
|
493
|
+
<MoreVertical className="h-4 w-4" />
|
|
494
|
+
</Button>
|
|
495
|
+
</DropdownMenuTrigger>
|
|
496
|
+
<DropdownMenuContent align="end">
|
|
497
|
+
<DropdownMenuItem className="gap-2 cursor-pointer text-xs">
|
|
498
|
+
<ExternalLink className="h-3 w-3" />
|
|
499
|
+
{t('libraries.menu.viewNpm')}
|
|
500
|
+
</DropdownMenuItem>
|
|
501
|
+
<DropdownMenuItem className="gap-2 cursor-pointer text-xs">
|
|
502
|
+
<Package className="h-3 w-3" />
|
|
503
|
+
{t('libraries.menu.configure')}
|
|
504
|
+
</DropdownMenuItem>
|
|
505
|
+
<DropdownMenuSeparator />
|
|
506
|
+
<DropdownMenuItem className="gap-2 cursor-pointer text-xs text-destructive">
|
|
507
|
+
<Package className="h-3 w-3" />
|
|
508
|
+
{t('libraries.menu.uninstall')}
|
|
509
|
+
</DropdownMenuItem>
|
|
510
|
+
</DropdownMenuContent>
|
|
511
|
+
</DropdownMenu>
|
|
512
|
+
</>
|
|
513
|
+
) : (
|
|
514
|
+
<Button
|
|
515
|
+
variant="outline"
|
|
516
|
+
size="sm"
|
|
517
|
+
className="h-7 text-xs gap-1.5 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
|
|
518
|
+
onClick={() => onInstall(lib)}
|
|
519
|
+
>
|
|
520
|
+
<Download className="h-3 w-3" />
|
|
521
|
+
{t('libraries.actions.install')}
|
|
522
|
+
</Button>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
})}
|
|
531
|
+
</div>
|
|
532
|
+
</ScrollArea>
|
|
533
|
+
</CardContent>
|
|
534
|
+
</Card>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ─── Git Info ───────────────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
export function GitInfo({
|
|
541
|
+
repoName,
|
|
542
|
+
currentBranch,
|
|
543
|
+
totalBranches,
|
|
544
|
+
openPRs,
|
|
545
|
+
recentCommits,
|
|
546
|
+
}: {
|
|
547
|
+
repoName: string;
|
|
548
|
+
currentBranch: string;
|
|
549
|
+
totalBranches: number;
|
|
550
|
+
openPRs: number;
|
|
551
|
+
recentCommits: CommitInfo[];
|
|
552
|
+
}) {
|
|
553
|
+
const t = useTranslations('developer-mode.Sections');
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<Card className="bg-card border-border h-full">
|
|
557
|
+
<CardHeader>
|
|
558
|
+
<div className="flex items-center gap-3">
|
|
559
|
+
<div className="p-2 rounded-lg bg-[#F05032]/10">
|
|
560
|
+
<GitIcon className="h-5 w-5" />
|
|
561
|
+
</div>
|
|
562
|
+
<div>
|
|
563
|
+
<CardTitle className="text-foreground">{repoName}</CardTitle>
|
|
564
|
+
<CardDescription className="text-muted-foreground">
|
|
565
|
+
{t('git.repository')}
|
|
566
|
+
</CardDescription>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</CardHeader>
|
|
570
|
+
<CardContent className="space-y-4">
|
|
571
|
+
<div className="flex items-center gap-3">
|
|
572
|
+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary/50 border border-border">
|
|
573
|
+
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
|
574
|
+
<span className="font-mono text-sm text-foreground">
|
|
575
|
+
{currentBranch}
|
|
576
|
+
</span>
|
|
577
|
+
</div>
|
|
578
|
+
<Badge variant="outline" className="font-mono text-xs">
|
|
579
|
+
{t('git.branches', { count: totalBranches })}
|
|
580
|
+
</Badge>
|
|
581
|
+
{openPRs > 0 && (
|
|
582
|
+
<Badge className="bg-info/20 text-info border-info/30">
|
|
583
|
+
<GitPullRequest className="h-3 w-3 mr-1" />
|
|
584
|
+
{t('git.openPrs', { count: openPRs })}
|
|
585
|
+
</Badge>
|
|
586
|
+
)}
|
|
587
|
+
</div>
|
|
588
|
+
<div>
|
|
589
|
+
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
|
590
|
+
{t('git.recentCommits')}
|
|
591
|
+
</h4>
|
|
592
|
+
<ScrollArea className="h-[180px]">
|
|
593
|
+
<div className="space-y-3 pr-4">
|
|
594
|
+
{recentCommits.map((commit) => (
|
|
595
|
+
<div
|
|
596
|
+
key={commit.hash}
|
|
597
|
+
className="flex items-start gap-3 p-2 rounded-lg hover:bg-secondary/30 transition-colors"
|
|
598
|
+
>
|
|
599
|
+
<Avatar className="h-8 w-8">
|
|
600
|
+
<AvatarImage src={commit.authorAvatar} />
|
|
601
|
+
<AvatarFallback className="bg-secondary text-xs">
|
|
602
|
+
{commit.author
|
|
603
|
+
.split(' ')
|
|
604
|
+
.map((n) => n[0])
|
|
605
|
+
.join('')
|
|
606
|
+
.toUpperCase()}
|
|
607
|
+
</AvatarFallback>
|
|
608
|
+
</Avatar>
|
|
609
|
+
<div className="flex-1 min-w-0">
|
|
610
|
+
<p className="text-sm text-foreground line-clamp-1">
|
|
611
|
+
{commit.message}
|
|
612
|
+
</p>
|
|
613
|
+
<div className="flex items-center gap-2 mt-1">
|
|
614
|
+
<span className="font-mono text-xs text-accent">
|
|
615
|
+
{commit.hash.substring(0, 7)}
|
|
616
|
+
</span>
|
|
617
|
+
<span className="text-xs text-muted-foreground">
|
|
618
|
+
{commit.author}
|
|
619
|
+
</span>
|
|
620
|
+
<span className="text-xs text-muted-foreground">
|
|
621
|
+
{commit.date}
|
|
622
|
+
</span>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
</ScrollArea>
|
|
629
|
+
</div>
|
|
630
|
+
</CardContent>
|
|
631
|
+
</Card>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ─── Database Info Card ─────────────────────────────────────────────────────
|
|
636
|
+
|
|
637
|
+
export function DatabaseInfoCard({
|
|
638
|
+
name,
|
|
639
|
+
host,
|
|
640
|
+
tables,
|
|
641
|
+
size,
|
|
642
|
+
connections,
|
|
643
|
+
lastMigration,
|
|
644
|
+
}: {
|
|
645
|
+
name: string;
|
|
646
|
+
host: string;
|
|
647
|
+
tables: number;
|
|
648
|
+
size: string;
|
|
649
|
+
connections: { active: number; max: number };
|
|
650
|
+
lastMigration?: {
|
|
651
|
+
name: string;
|
|
652
|
+
date: string;
|
|
653
|
+
status: 'success' | 'pending' | 'failed';
|
|
654
|
+
};
|
|
655
|
+
}) {
|
|
656
|
+
const t = useTranslations('developer-mode.Sections');
|
|
657
|
+
const connectionPercentage = (connections.active / connections.max) * 100;
|
|
658
|
+
const migrationStatusColors: Record<string, string> = {
|
|
659
|
+
success: 'bg-success/20 text-success border-success/30',
|
|
660
|
+
pending: 'bg-warning/20 text-warning border-warning/30',
|
|
661
|
+
failed: 'bg-destructive/20 text-destructive border-destructive/30',
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
return (
|
|
665
|
+
<Card className="bg-card border-border">
|
|
666
|
+
<CardHeader>
|
|
667
|
+
<div className="flex items-center justify-between">
|
|
668
|
+
<div className="flex items-center gap-3">
|
|
669
|
+
<div className="p-2 rounded-lg bg-[#336791]/20">
|
|
670
|
+
<PostgreSQLIcon className="h-5 w-5" />
|
|
671
|
+
</div>
|
|
672
|
+
<div>
|
|
673
|
+
<CardTitle className="text-foreground">{name}</CardTitle>
|
|
674
|
+
<CardDescription className="text-muted-foreground font-mono text-xs">
|
|
675
|
+
{host}
|
|
676
|
+
</CardDescription>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
<Badge variant="outline" className="font-mono text-xs uppercase">
|
|
680
|
+
{t('database.type')}
|
|
681
|
+
</Badge>
|
|
682
|
+
</div>
|
|
683
|
+
</CardHeader>
|
|
684
|
+
<CardContent className="space-y-4">
|
|
685
|
+
<div className="grid grid-cols-2 gap-4">
|
|
686
|
+
<div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30">
|
|
687
|
+
<Table2 className="h-4 w-4 text-muted-foreground" />
|
|
688
|
+
<div>
|
|
689
|
+
<p className="text-lg font-semibold text-foreground">{tables}</p>
|
|
690
|
+
<p className="text-xs text-muted-foreground">
|
|
691
|
+
{t('database.tables')}
|
|
692
|
+
</p>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
<div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30">
|
|
696
|
+
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
697
|
+
<div>
|
|
698
|
+
<p className="text-lg font-semibold text-foreground">{size}</p>
|
|
699
|
+
<p className="text-xs text-muted-foreground">
|
|
700
|
+
{t('database.size')}
|
|
701
|
+
</p>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
<div className="space-y-2">
|
|
706
|
+
<div className="flex items-center justify-between text-sm">
|
|
707
|
+
<span className="text-muted-foreground">
|
|
708
|
+
{t('database.connections')}
|
|
709
|
+
</span>
|
|
710
|
+
<span className="font-mono text-foreground">
|
|
711
|
+
{connections.active}/{connections.max}
|
|
712
|
+
</span>
|
|
713
|
+
</div>
|
|
714
|
+
<Progress value={connectionPercentage} className="h-2" />
|
|
715
|
+
</div>
|
|
716
|
+
{lastMigration && (
|
|
717
|
+
<div className="pt-3 border-t border-border">
|
|
718
|
+
<div className="flex items-center justify-between">
|
|
719
|
+
<div className="flex items-center gap-2">
|
|
720
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
721
|
+
<span className="text-sm text-muted-foreground">
|
|
722
|
+
{t('database.lastMigration')}
|
|
723
|
+
</span>
|
|
724
|
+
</div>
|
|
725
|
+
<Badge
|
|
726
|
+
variant="outline"
|
|
727
|
+
className={migrationStatusColors[lastMigration.status]}
|
|
728
|
+
>
|
|
729
|
+
{lastMigration.status}
|
|
730
|
+
</Badge>
|
|
731
|
+
</div>
|
|
732
|
+
<p className="text-sm text-foreground mt-2 font-mono">
|
|
733
|
+
{lastMigration.name}
|
|
734
|
+
</p>
|
|
735
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
736
|
+
{lastMigration.date}
|
|
737
|
+
</p>
|
|
738
|
+
</div>
|
|
739
|
+
)}
|
|
740
|
+
</CardContent>
|
|
741
|
+
</Card>
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ─── Wizard Dialog ──────────────────────────────────────────────────────────
|
|
746
|
+
|
|
747
|
+
export function WizardDialog({
|
|
748
|
+
open,
|
|
749
|
+
onOpenChange,
|
|
750
|
+
config,
|
|
751
|
+
}: {
|
|
752
|
+
open: boolean;
|
|
753
|
+
onOpenChange: (open: boolean) => void;
|
|
754
|
+
config: WizardConfig;
|
|
755
|
+
}) {
|
|
756
|
+
const t = useTranslations('developer-mode.Sections');
|
|
757
|
+
const { accessToken, currentLocaleCode } = useApp();
|
|
758
|
+
const [currentStep, setCurrentStep] = React.useState(0);
|
|
759
|
+
const [formData, setFormData] = React.useState<Record<string, string>>({});
|
|
760
|
+
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
|
|
761
|
+
const [isRunning, setIsRunning] = React.useState(false);
|
|
762
|
+
const [output, setOutput] = React.useState<string[]>([]);
|
|
763
|
+
const [completed, setCompleted] = React.useState(false);
|
|
764
|
+
const [hasError, setHasError] = React.useState(false);
|
|
765
|
+
const outputRef = React.useRef<HTMLDivElement>(null);
|
|
766
|
+
const streamAbortRef = React.useRef<AbortController | null>(null);
|
|
767
|
+
const executedProgressStepRef = React.useRef<string | null>(null);
|
|
768
|
+
const step = config.steps[currentStep] ?? config.steps[0]!;
|
|
769
|
+
const apiBaseUrl =
|
|
770
|
+
process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_API_URL;
|
|
771
|
+
|
|
772
|
+
React.useEffect(() => {
|
|
773
|
+
if (outputRef.current)
|
|
774
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
775
|
+
}, [output]);
|
|
776
|
+
|
|
777
|
+
const simulateOutput = React.useCallback(() => {
|
|
778
|
+
const lines = [
|
|
779
|
+
`$ hedhog ${config.id}`,
|
|
780
|
+
``,
|
|
781
|
+
t('wizard.output.starting', {
|
|
782
|
+
title: config.title.toLowerCase(),
|
|
783
|
+
}),
|
|
784
|
+
t('wizard.output.checkingPrerequisites'),
|
|
785
|
+
t('wizard.output.validatingConfiguration'),
|
|
786
|
+
...(Object.keys(formData).length > 0
|
|
787
|
+
? Object.entries(formData).map(([k, v]) =>
|
|
788
|
+
t('wizard.output.setting', { key: k, value: v })
|
|
789
|
+
)
|
|
790
|
+
: []),
|
|
791
|
+
...(selectedOptions.length > 0
|
|
792
|
+
? [
|
|
793
|
+
t('wizard.output.selectedModules', {
|
|
794
|
+
modules: selectedOptions.join(', '),
|
|
795
|
+
}),
|
|
796
|
+
]
|
|
797
|
+
: []),
|
|
798
|
+
t('wizard.output.processing'),
|
|
799
|
+
t('wizard.output.creatingDirectories'),
|
|
800
|
+
t('wizard.output.generatingFiles'),
|
|
801
|
+
t('wizard.output.installingDependencies'),
|
|
802
|
+
t('wizard.output.runningPostInstallHooks'),
|
|
803
|
+
``,
|
|
804
|
+
t('wizard.output.completedSuccessfully'),
|
|
805
|
+
];
|
|
806
|
+
setHasError(false);
|
|
807
|
+
setCompleted(false);
|
|
808
|
+
setIsRunning(true);
|
|
809
|
+
setOutput([]);
|
|
810
|
+
lines.forEach((line, i) => {
|
|
811
|
+
setTimeout(
|
|
812
|
+
() => {
|
|
813
|
+
setOutput((prev) => [...prev, line]);
|
|
814
|
+
if (i === lines.length - 1) {
|
|
815
|
+
setIsRunning(false);
|
|
816
|
+
setCompleted(true);
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
(i + 1) * 300
|
|
820
|
+
);
|
|
821
|
+
});
|
|
822
|
+
}, [config.id, config.title, formData, selectedOptions, t]);
|
|
823
|
+
|
|
824
|
+
const runScriptOutputStream = React.useCallback(async () => {
|
|
825
|
+
if (!config.scriptExecution || !apiBaseUrl) {
|
|
826
|
+
simulateOutput();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const abortController = new AbortController();
|
|
831
|
+
streamAbortRef.current = abortController;
|
|
832
|
+
|
|
833
|
+
setHasError(false);
|
|
834
|
+
setCompleted(false);
|
|
835
|
+
setIsRunning(true);
|
|
836
|
+
setOutput([]);
|
|
837
|
+
|
|
838
|
+
const pushOutput = (line: string) => {
|
|
839
|
+
setOutput((prev) => [...prev, line]);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
pushOutput(t('wizard.output.connectingStream'));
|
|
843
|
+
|
|
844
|
+
let streamHasError = false;
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
const response = await fetch(
|
|
848
|
+
`${apiBaseUrl}/developer-mode/scripts/stream`,
|
|
849
|
+
{
|
|
850
|
+
method: 'POST',
|
|
851
|
+
headers: {
|
|
852
|
+
'Content-Type': 'application/json',
|
|
853
|
+
Accept: 'text/event-stream',
|
|
854
|
+
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
855
|
+
...(currentLocaleCode
|
|
856
|
+
? { 'Accept-Language': currentLocaleCode }
|
|
857
|
+
: {}),
|
|
858
|
+
},
|
|
859
|
+
body: JSON.stringify(config.scriptExecution),
|
|
860
|
+
signal: abortController.signal,
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
if (!response.ok || !response.body) {
|
|
865
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
pushOutput(t('wizard.output.streamConnected'));
|
|
869
|
+
|
|
870
|
+
const reader = response.body.getReader();
|
|
871
|
+
const decoder = new TextDecoder();
|
|
872
|
+
let buffer = '';
|
|
873
|
+
|
|
874
|
+
const processChunk = (rawChunk: string) => {
|
|
875
|
+
if (!rawChunk.trim()) return;
|
|
876
|
+
|
|
877
|
+
let event = 'message';
|
|
878
|
+
let data = '';
|
|
879
|
+
|
|
880
|
+
for (const line of rawChunk.split(/\r?\n/)) {
|
|
881
|
+
if (line.startsWith('event:')) {
|
|
882
|
+
event = line.replace('event:', '').trim();
|
|
883
|
+
}
|
|
884
|
+
if (line.startsWith('data:')) {
|
|
885
|
+
data += `${line.replace('data:', '').trim()}\n`;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const payloadText = data.trim();
|
|
890
|
+
if (!payloadText) return;
|
|
891
|
+
|
|
892
|
+
let payloadMessage = payloadText;
|
|
893
|
+
try {
|
|
894
|
+
const parsed = JSON.parse(payloadText) as { message?: string };
|
|
895
|
+
payloadMessage = parsed.message ?? payloadText;
|
|
896
|
+
} catch {
|
|
897
|
+
payloadMessage = payloadText;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (event === 'error') {
|
|
901
|
+
streamHasError = true;
|
|
902
|
+
setHasError(true);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (payloadMessage) {
|
|
906
|
+
pushOutput(payloadMessage);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
while (true) {
|
|
911
|
+
const { done, value } = await reader.read();
|
|
912
|
+
if (done) {
|
|
913
|
+
processChunk(buffer);
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
buffer += decoder.decode(value, { stream: true });
|
|
918
|
+
const chunks = buffer.split(/\r?\n\r?\n/);
|
|
919
|
+
buffer = chunks.pop() ?? '';
|
|
920
|
+
|
|
921
|
+
for (const chunk of chunks) {
|
|
922
|
+
processChunk(chunk);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
setCompleted(!streamHasError);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (!abortController.signal.aborted) {
|
|
929
|
+
const message =
|
|
930
|
+
error instanceof Error
|
|
931
|
+
? error.message
|
|
932
|
+
: t('wizard.output.streamFailed');
|
|
933
|
+
setHasError(true);
|
|
934
|
+
pushOutput(message);
|
|
935
|
+
}
|
|
936
|
+
} finally {
|
|
937
|
+
if (streamAbortRef.current === abortController) {
|
|
938
|
+
streamAbortRef.current = null;
|
|
939
|
+
}
|
|
940
|
+
setIsRunning(false);
|
|
941
|
+
}
|
|
942
|
+
}, [
|
|
943
|
+
accessToken,
|
|
944
|
+
apiBaseUrl,
|
|
945
|
+
config.scriptExecution,
|
|
946
|
+
currentLocaleCode,
|
|
947
|
+
simulateOutput,
|
|
948
|
+
t,
|
|
949
|
+
]);
|
|
950
|
+
|
|
951
|
+
React.useEffect(() => {
|
|
952
|
+
if (!open) return;
|
|
953
|
+
if (step.type !== 'progress') return;
|
|
954
|
+
if (executedProgressStepRef.current === step.id) return;
|
|
955
|
+
|
|
956
|
+
executedProgressStepRef.current = step.id;
|
|
957
|
+
runScriptOutputStream();
|
|
958
|
+
}, [open, runScriptOutputStream, step.id, step.type]);
|
|
959
|
+
|
|
960
|
+
React.useEffect(
|
|
961
|
+
() => () => {
|
|
962
|
+
streamAbortRef.current?.abort();
|
|
963
|
+
},
|
|
964
|
+
[]
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
const handleNext = () => {
|
|
968
|
+
if (currentStep < config.steps.length - 1) {
|
|
969
|
+
const next = currentStep + 1;
|
|
970
|
+
setCurrentStep(next);
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const handleClose = () => {
|
|
975
|
+
streamAbortRef.current?.abort();
|
|
976
|
+
streamAbortRef.current = null;
|
|
977
|
+
executedProgressStepRef.current = null;
|
|
978
|
+
setCurrentStep(0);
|
|
979
|
+
setFormData({});
|
|
980
|
+
setSelectedOptions([]);
|
|
981
|
+
setOutput([]);
|
|
982
|
+
setIsRunning(false);
|
|
983
|
+
setCompleted(false);
|
|
984
|
+
setHasError(false);
|
|
985
|
+
onOpenChange(false);
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
const handleDialogOpenChange = (nextOpen: boolean) => {
|
|
989
|
+
if (!nextOpen) {
|
|
990
|
+
handleClose();
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const toggleOption = (value: string) => {
|
|
995
|
+
if (config.selectMultiple)
|
|
996
|
+
setSelectedOptions((p) =>
|
|
997
|
+
p.includes(value) ? p.filter((v) => v !== value) : [...p, value]
|
|
998
|
+
);
|
|
999
|
+
else setSelectedOptions([value]);
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
const canProceed = () => {
|
|
1003
|
+
if (step.type === 'form' && config.formFields)
|
|
1004
|
+
return config.formFields
|
|
1005
|
+
.filter((f) => f.required)
|
|
1006
|
+
.every((f) => formData[f.name]?.trim());
|
|
1007
|
+
if (step.type === 'select') return selectedOptions.length > 0;
|
|
1008
|
+
return true;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const renderStep = () => {
|
|
1012
|
+
switch (step.type) {
|
|
1013
|
+
case 'info':
|
|
1014
|
+
return (
|
|
1015
|
+
<div className="space-y-4 py-4">
|
|
1016
|
+
<div className="rounded-lg border border-border bg-secondary/30 p-4">
|
|
1017
|
+
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-line">
|
|
1018
|
+
{step.description}
|
|
1019
|
+
</p>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
);
|
|
1023
|
+
case 'form':
|
|
1024
|
+
return (
|
|
1025
|
+
<div className="space-y-4 py-4">
|
|
1026
|
+
{config.formFields?.map((field) => (
|
|
1027
|
+
<div key={field.name} className="space-y-2">
|
|
1028
|
+
<Label htmlFor={field.name} className="text-foreground">
|
|
1029
|
+
{field.label}
|
|
1030
|
+
{field.required && (
|
|
1031
|
+
<span className="text-destructive ml-1">*</span>
|
|
1032
|
+
)}
|
|
1033
|
+
</Label>
|
|
1034
|
+
<Input
|
|
1035
|
+
id={field.name}
|
|
1036
|
+
type={field.type || 'text'}
|
|
1037
|
+
placeholder={field.placeholder}
|
|
1038
|
+
value={formData[field.name] || ''}
|
|
1039
|
+
onChange={(e) =>
|
|
1040
|
+
setFormData((p) => ({ ...p, [field.name]: e.target.value }))
|
|
1041
|
+
}
|
|
1042
|
+
className="bg-input border-border font-mono"
|
|
1043
|
+
/>
|
|
1044
|
+
</div>
|
|
1045
|
+
))}
|
|
1046
|
+
</div>
|
|
1047
|
+
);
|
|
1048
|
+
case 'select':
|
|
1049
|
+
return (
|
|
1050
|
+
<div className="space-y-3 py-4">
|
|
1051
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
1052
|
+
{step.description}
|
|
1053
|
+
</p>
|
|
1054
|
+
<ScrollArea className="h-[250px] pr-4">
|
|
1055
|
+
<div className="space-y-2">
|
|
1056
|
+
{(config.selectOptions ?? []).map((option) => (
|
|
1057
|
+
<div
|
|
1058
|
+
key={option.value}
|
|
1059
|
+
className={cn(
|
|
1060
|
+
'flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer transition-colors',
|
|
1061
|
+
selectedOptions.includes(option.value)
|
|
1062
|
+
? 'bg-accent/10 border-accent'
|
|
1063
|
+
: 'hover:bg-secondary/50'
|
|
1064
|
+
)}
|
|
1065
|
+
onClick={() => toggleOption(option.value)}
|
|
1066
|
+
>
|
|
1067
|
+
<Checkbox
|
|
1068
|
+
checked={selectedOptions.includes(option.value)}
|
|
1069
|
+
onCheckedChange={() => toggleOption(option.value)}
|
|
1070
|
+
className="mt-0.5 border-muted-foreground data-[state=checked]:bg-accent data-[state=checked]:border-accent"
|
|
1071
|
+
/>
|
|
1072
|
+
<div>
|
|
1073
|
+
<span className="font-mono text-sm text-foreground">
|
|
1074
|
+
@hedhog/{option.label ?? option.value}
|
|
1075
|
+
</span>
|
|
1076
|
+
{option.description && (
|
|
1077
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
1078
|
+
{option.description}
|
|
1079
|
+
</p>
|
|
1080
|
+
)}
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
))}
|
|
1084
|
+
</div>
|
|
1085
|
+
</ScrollArea>
|
|
1086
|
+
</div>
|
|
1087
|
+
);
|
|
1088
|
+
case 'progress':
|
|
1089
|
+
return (
|
|
1090
|
+
<div className="space-y-4 py-4">
|
|
1091
|
+
<div className="rounded-lg border border-border bg-black overflow-hidden">
|
|
1092
|
+
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-secondary/30">
|
|
1093
|
+
<div className="flex gap-1.5">
|
|
1094
|
+
<div className="w-3 h-3 rounded-full bg-destructive/50" />
|
|
1095
|
+
<div className="w-3 h-3 rounded-full bg-warning/50" />
|
|
1096
|
+
<div className="w-3 h-3 rounded-full bg-success/50" />
|
|
1097
|
+
</div>
|
|
1098
|
+
<Terminal className="h-3.5 w-3.5 text-muted-foreground ml-2" />
|
|
1099
|
+
<span className="text-xs text-muted-foreground font-mono">
|
|
1100
|
+
{t('wizard.terminal')}
|
|
1101
|
+
</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
<ScrollArea className="h-[220px] p-4" ref={outputRef}>
|
|
1104
|
+
<div className="space-y-0.5 font-mono">
|
|
1105
|
+
{output.map((line, i) => (
|
|
1106
|
+
<div
|
|
1107
|
+
key={i}
|
|
1108
|
+
className={cn(
|
|
1109
|
+
'text-xs',
|
|
1110
|
+
line
|
|
1111
|
+
.toLowerCase()
|
|
1112
|
+
.includes(
|
|
1113
|
+
t('wizard.output.successKeyword').toLowerCase()
|
|
1114
|
+
) ||
|
|
1115
|
+
line
|
|
1116
|
+
.toLowerCase()
|
|
1117
|
+
.includes(
|
|
1118
|
+
t('wizard.output.doneKeyword').toLowerCase()
|
|
1119
|
+
)
|
|
1120
|
+
? 'text-green-400'
|
|
1121
|
+
: line
|
|
1122
|
+
.toLowerCase()
|
|
1123
|
+
.includes(
|
|
1124
|
+
t('wizard.output.errorKeyword').toLowerCase()
|
|
1125
|
+
) ||
|
|
1126
|
+
line
|
|
1127
|
+
.toLowerCase()
|
|
1128
|
+
.includes(
|
|
1129
|
+
t('wizard.output.failedKeyword').toLowerCase()
|
|
1130
|
+
)
|
|
1131
|
+
? 'text-red-400'
|
|
1132
|
+
: line.startsWith('$')
|
|
1133
|
+
? 'text-cyan-300'
|
|
1134
|
+
: line.startsWith('[HedHog]')
|
|
1135
|
+
? 'text-zinc-300'
|
|
1136
|
+
: 'text-zinc-100'
|
|
1137
|
+
)}
|
|
1138
|
+
>
|
|
1139
|
+
{line || '\u00A0'}
|
|
1140
|
+
</div>
|
|
1141
|
+
))}
|
|
1142
|
+
{isRunning && (
|
|
1143
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
1144
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
1145
|
+
<span className="text-xs">{t('wizard.processing')}</span>
|
|
1146
|
+
</div>
|
|
1147
|
+
)}
|
|
1148
|
+
</div>
|
|
1149
|
+
</ScrollArea>
|
|
1150
|
+
</div>
|
|
1151
|
+
{completed && !hasError && (
|
|
1152
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-success/10 border border-success/30">
|
|
1153
|
+
<CheckCircle2 className="h-4 w-4 text-success" />
|
|
1154
|
+
<span className="text-sm text-success">
|
|
1155
|
+
{t('wizard.success')}
|
|
1156
|
+
</span>
|
|
1157
|
+
</div>
|
|
1158
|
+
)}
|
|
1159
|
+
{hasError && (
|
|
1160
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-destructive/10 border border-destructive/30">
|
|
1161
|
+
<AlertCircle className="h-4 w-4 text-destructive" />
|
|
1162
|
+
<span className="text-sm text-destructive">
|
|
1163
|
+
{t('wizard.error')}
|
|
1164
|
+
</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
)}
|
|
1167
|
+
</div>
|
|
1168
|
+
);
|
|
1169
|
+
default:
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
|
1176
|
+
<DialogContent className="sm:max-w-[550px] bg-card border-border">
|
|
1177
|
+
<DialogHeader>
|
|
1178
|
+
<div className="flex items-center gap-3">
|
|
1179
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/20 text-accent">
|
|
1180
|
+
{config.icon}
|
|
1181
|
+
</div>
|
|
1182
|
+
<div>
|
|
1183
|
+
<DialogTitle className="text-foreground">
|
|
1184
|
+
{config.title}
|
|
1185
|
+
</DialogTitle>
|
|
1186
|
+
<DialogDescription className="text-muted-foreground">
|
|
1187
|
+
{step.title}
|
|
1188
|
+
</DialogDescription>
|
|
1189
|
+
</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
</DialogHeader>
|
|
1192
|
+
<div className="flex items-center justify-center gap-2 py-2">
|
|
1193
|
+
{config.steps.map((s, i) => (
|
|
1194
|
+
<div key={s.id} className="flex items-center">
|
|
1195
|
+
<div
|
|
1196
|
+
className={cn(
|
|
1197
|
+
'flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium transition-colors',
|
|
1198
|
+
i < currentStep
|
|
1199
|
+
? 'bg-accent text-accent-foreground'
|
|
1200
|
+
: i === currentStep
|
|
1201
|
+
? 'bg-accent text-accent-foreground ring-2 ring-accent/30'
|
|
1202
|
+
: 'bg-secondary text-muted-foreground'
|
|
1203
|
+
)}
|
|
1204
|
+
>
|
|
1205
|
+
{i < currentStep ? <CheckCircle2 className="h-4 w-4" /> : i + 1}
|
|
1206
|
+
</div>
|
|
1207
|
+
{i < config.steps.length - 1 && (
|
|
1208
|
+
<div
|
|
1209
|
+
className={cn(
|
|
1210
|
+
'w-8 h-0.5 mx-1',
|
|
1211
|
+
i < currentStep ? 'bg-accent' : 'bg-secondary'
|
|
1212
|
+
)}
|
|
1213
|
+
/>
|
|
1214
|
+
)}
|
|
1215
|
+
</div>
|
|
1216
|
+
))}
|
|
1217
|
+
</div>
|
|
1218
|
+
{renderStep()}
|
|
1219
|
+
<div className="flex justify-between pt-4 border-t border-border">
|
|
1220
|
+
<Button
|
|
1221
|
+
variant="outline"
|
|
1222
|
+
onClick={() => currentStep > 0 && setCurrentStep(currentStep - 1)}
|
|
1223
|
+
disabled={currentStep === 0 || isRunning}
|
|
1224
|
+
className="border-border bg-transparent"
|
|
1225
|
+
>
|
|
1226
|
+
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
1227
|
+
{t('wizard.actions.back')}
|
|
1228
|
+
</Button>
|
|
1229
|
+
{currentStep === config.steps.length - 1 ? (
|
|
1230
|
+
<Button
|
|
1231
|
+
onClick={handleClose}
|
|
1232
|
+
disabled={isRunning}
|
|
1233
|
+
className="bg-accent text-accent-foreground hover:bg-accent/90"
|
|
1234
|
+
>
|
|
1235
|
+
{completed ? t('wizard.actions.done') : t('wizard.actions.close')}
|
|
1236
|
+
</Button>
|
|
1237
|
+
) : (
|
|
1238
|
+
<Button
|
|
1239
|
+
onClick={handleNext}
|
|
1240
|
+
disabled={isRunning || !canProceed()}
|
|
1241
|
+
className="bg-accent text-accent-foreground hover:bg-accent/90"
|
|
1242
|
+
>
|
|
1243
|
+
{t('wizard.actions.next')}
|
|
1244
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
1245
|
+
</Button>
|
|
1246
|
+
)}
|
|
1247
|
+
</div>
|
|
1248
|
+
</DialogContent>
|
|
1249
|
+
</Dialog>
|
|
1250
|
+
);
|
|
1251
|
+
}
|