groove-dev 0.26.33 → 0.26.35
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/CHANGELOG.md +55 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-86xvrzfI.js +638 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +10 -7
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +511 -0
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +26 -4
- package/package.json +1 -1
- package/packages/daemon/src/providers/claude-code.js +4 -2
- package/packages/gui/dist/assets/index-86xvrzfI.js +638 -0
- package/packages/gui/dist/assets/index-CEFKgLGB.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +10 -7
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +511 -0
- package/packages/gui/src/views/dashboard.jsx +3 -1
- package/packages/gui/src/views/marketplace.jsx +26 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BnNZzcsd.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CPF9iasK.js +0 -638
- package/packages/gui/dist/assets/index-BnNZzcsd.css +0 -1
- package/packages/gui/dist/assets/index-CPF9iasK.js +0 -638
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Dialog, DialogContent } from '../ui/dialog';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { Input } from '../ui/input';
|
|
6
|
+
import { Badge } from '../ui/badge';
|
|
7
|
+
import { api } from '../../lib/api';
|
|
8
|
+
import { useToast } from '../../lib/hooks/use-toast';
|
|
9
|
+
import {
|
|
10
|
+
Check, CheckCircle, ExternalLink, Loader2, Eye, EyeOff,
|
|
11
|
+
Key, Shield, Trash2, ChevronRight, X,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
// Reuse integration logos from marketplace-card
|
|
15
|
+
const INTEGRATION_LOGOS = {
|
|
16
|
+
slack: 'https://cdn.simpleicons.org/slack/E01E5A',
|
|
17
|
+
github: 'https://cdn.simpleicons.org/github/white',
|
|
18
|
+
stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
|
|
19
|
+
gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
|
|
20
|
+
'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
|
|
21
|
+
'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
|
|
22
|
+
'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
|
|
23
|
+
postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
|
|
24
|
+
notion: 'https://cdn.simpleicons.org/notion/white',
|
|
25
|
+
discord: 'https://cdn.simpleicons.org/discord/5865F2',
|
|
26
|
+
linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
|
|
27
|
+
'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
|
|
28
|
+
'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function IntegrationIcon({ item, size = 48 }) {
|
|
32
|
+
const logoUrl = INTEGRATION_LOGOS[item.id];
|
|
33
|
+
if (logoUrl) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="rounded-lg bg-surface-4 flex items-center justify-center flex-shrink-0 overflow-hidden" style={{ width: size, height: size }}>
|
|
36
|
+
<img src={logoUrl} alt={item.name} className="w-6 h-6" onError={(e) => { e.target.style.display = 'none'; }} />
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const initial = (item.name || '?')[0].toUpperCase();
|
|
41
|
+
const hue = item.name ? item.name.charCodeAt(0) * 37 % 360 : 200;
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className="rounded-lg flex items-center justify-center flex-shrink-0 text-xl font-bold font-sans"
|
|
45
|
+
style={{ width: size, height: size, background: `hsl(${hue}, 40%, 18%)`, color: `hsl(${hue}, 60%, 65%)` }}
|
|
46
|
+
>
|
|
47
|
+
{initial}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Password input with show/hide toggle ────────────────
|
|
53
|
+
function SecretInput({ value, onChange, placeholder, disabled }) {
|
|
54
|
+
const [visible, setVisible] = useState(false);
|
|
55
|
+
return (
|
|
56
|
+
<div className="relative">
|
|
57
|
+
<Input
|
|
58
|
+
type={visible ? 'text' : 'password'}
|
|
59
|
+
value={value}
|
|
60
|
+
onChange={(e) => onChange(e.target.value)}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
mono
|
|
64
|
+
className="pr-9"
|
|
65
|
+
/>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => setVisible((v) => !v)}
|
|
69
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded text-text-4 hover:text-text-1 transition-colors cursor-pointer"
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
>
|
|
72
|
+
{visible ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Credential row for api-key auth type ────────────────
|
|
79
|
+
function CredentialRow({ integrationId, envKey, onSaved }) {
|
|
80
|
+
const toast = useToast();
|
|
81
|
+
const [value, setValue] = useState('');
|
|
82
|
+
const [saving, setSaving] = useState(false);
|
|
83
|
+
const [saved, setSaved] = useState(envKey.set);
|
|
84
|
+
const [deleting, setDeleting] = useState(false);
|
|
85
|
+
|
|
86
|
+
async function handleSave() {
|
|
87
|
+
if (!value.trim()) return;
|
|
88
|
+
setSaving(true);
|
|
89
|
+
try {
|
|
90
|
+
await api.post(`/integrations/${integrationId}/credentials`, { key: envKey.key, value: value.trim() });
|
|
91
|
+
setSaved(true);
|
|
92
|
+
setValue('');
|
|
93
|
+
toast.success(`${envKey.label} saved`);
|
|
94
|
+
onSaved?.();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
toast.error('Failed to save', err.message);
|
|
97
|
+
}
|
|
98
|
+
setSaving(false);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleDelete() {
|
|
102
|
+
setDeleting(true);
|
|
103
|
+
try {
|
|
104
|
+
await api.delete(`/integrations/${integrationId}/credentials/${envKey.key}`);
|
|
105
|
+
setSaved(false);
|
|
106
|
+
toast.success(`${envKey.label} removed`);
|
|
107
|
+
onSaved?.();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
toast.error('Failed to remove', err.message);
|
|
110
|
+
}
|
|
111
|
+
setDeleting(false);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="space-y-1.5">
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
<label className="text-xs font-medium text-text-2 font-sans flex items-center gap-1.5">
|
|
118
|
+
<Key size={11} className="text-text-4" />
|
|
119
|
+
{envKey.label}
|
|
120
|
+
{envKey.required && <span className="text-danger">*</span>}
|
|
121
|
+
</label>
|
|
122
|
+
{saved && (
|
|
123
|
+
<span className="flex items-center gap-1 text-2xs text-success font-sans">
|
|
124
|
+
<Check size={10} /> Set
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{saved ? (
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<div className="flex-1 h-8 rounded-md px-3 bg-surface-2 border border-border-subtle flex items-center">
|
|
132
|
+
<span className="text-xs text-text-4 font-mono tracking-widest">{'*'.repeat(16)}</span>
|
|
133
|
+
</div>
|
|
134
|
+
<Button variant="ghost" size="sm" onClick={handleDelete} disabled={deleting} className="text-text-3 hover:text-danger">
|
|
135
|
+
{deleting ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
<div className="flex-1">
|
|
141
|
+
<SecretInput
|
|
142
|
+
value={value}
|
|
143
|
+
onChange={setValue}
|
|
144
|
+
placeholder={envKey.placeholder || `Enter ${envKey.label.toLowerCase()}...`}
|
|
145
|
+
disabled={saving}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
<Button variant="primary" size="sm" onClick={handleSave} disabled={saving || !value.trim()}>
|
|
149
|
+
{saving ? <Loader2 size={12} className="animate-spin" /> : 'Save'}
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Step: Overview ──────────────────────────────────────
|
|
158
|
+
function OverviewStep({ item, status, installing, onInstall, onUninstall, onNext }) {
|
|
159
|
+
const isInstalled = status?.installed;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="px-5 py-5 space-y-5">
|
|
163
|
+
{/* Header */}
|
|
164
|
+
<div className="flex items-start gap-4">
|
|
165
|
+
<IntegrationIcon item={item} size={52} />
|
|
166
|
+
<div className="flex-1 min-w-0">
|
|
167
|
+
<div className="flex items-center gap-2">
|
|
168
|
+
<h2 className="text-base font-bold text-text-0 font-sans">{item.name}</h2>
|
|
169
|
+
{(item.verified === 'mcp-official' || item.verified === 'verified') && (
|
|
170
|
+
<Badge variant="accent" className="text-2xs gap-1">
|
|
171
|
+
<Shield size={9} /> Verified
|
|
172
|
+
</Badge>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<p className="text-xs text-text-3 font-sans mt-0.5">{item.author || 'Community'}</p>
|
|
176
|
+
{item.category && (
|
|
177
|
+
<Badge variant="default" className="text-2xs mt-2">{item.category}</Badge>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Description */}
|
|
183
|
+
<p className="text-sm text-text-2 font-sans leading-relaxed">{item.description}</p>
|
|
184
|
+
|
|
185
|
+
{/* Tags */}
|
|
186
|
+
{item.tags?.length > 0 && (
|
|
187
|
+
<div className="flex flex-wrap gap-1.5">
|
|
188
|
+
{item.tags.map((tag) => (
|
|
189
|
+
<span key={tag} className="text-2xs text-text-3 font-sans px-2 py-0.5 rounded bg-surface-4">{tag}</span>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<div className="h-px bg-border-subtle" />
|
|
195
|
+
|
|
196
|
+
{/* Action */}
|
|
197
|
+
{isInstalled ? (
|
|
198
|
+
<div className="flex items-center gap-3">
|
|
199
|
+
<div className="flex-1 flex items-center gap-2">
|
|
200
|
+
<CheckCircle size={16} className="text-success" />
|
|
201
|
+
<span className="text-sm font-medium text-success font-sans">Installed</span>
|
|
202
|
+
</div>
|
|
203
|
+
<Button variant="ghost" size="sm" onClick={onUninstall} className="text-text-3 hover:text-danger gap-1.5">
|
|
204
|
+
<Trash2 size={12} /> Uninstall
|
|
205
|
+
</Button>
|
|
206
|
+
<Button variant="primary" size="sm" onClick={onNext} className="gap-1">
|
|
207
|
+
Configure <ChevronRight size={12} />
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<Button
|
|
212
|
+
variant="primary"
|
|
213
|
+
size="lg"
|
|
214
|
+
onClick={onInstall}
|
|
215
|
+
disabled={installing}
|
|
216
|
+
className="w-full gap-2"
|
|
217
|
+
>
|
|
218
|
+
{installing ? (
|
|
219
|
+
<>
|
|
220
|
+
<Loader2 size={14} className="animate-spin" />
|
|
221
|
+
Installing...
|
|
222
|
+
</>
|
|
223
|
+
) : (
|
|
224
|
+
'Install'
|
|
225
|
+
)}
|
|
226
|
+
</Button>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{installing && (
|
|
230
|
+
<p className="text-2xs text-text-4 font-sans text-center">This may take up to 30 seconds...</p>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Step: Configure ─────────────────────────────────────
|
|
237
|
+
function ConfigureStep({ item, status, onDone, onRefreshStatus }) {
|
|
238
|
+
const toast = useToast();
|
|
239
|
+
const [authenticating, setAuthenticating] = useState(false);
|
|
240
|
+
const authType = item.authType;
|
|
241
|
+
|
|
242
|
+
async function handleGoogleAutoAuth() {
|
|
243
|
+
setAuthenticating(true);
|
|
244
|
+
try {
|
|
245
|
+
await api.post(`/integrations/${item.id}/authenticate`);
|
|
246
|
+
toast.success('Browser opened — complete sign-in there');
|
|
247
|
+
} catch (err) {
|
|
248
|
+
toast.error('Auth failed', err.message);
|
|
249
|
+
}
|
|
250
|
+
setAuthenticating(false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function handleOAuthStart() {
|
|
254
|
+
setAuthenticating(true);
|
|
255
|
+
try {
|
|
256
|
+
const data = await api.post(`/integrations/${item.id}/oauth/start`);
|
|
257
|
+
if (data.url) {
|
|
258
|
+
window.open(data.url, '_blank', 'noopener');
|
|
259
|
+
toast.success('Browser opened — complete sign-in there');
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
toast.error('OAuth failed', err.message);
|
|
263
|
+
}
|
|
264
|
+
setAuthenticating(false);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if all required keys are set
|
|
268
|
+
const envKeys = status?.envKeys || [];
|
|
269
|
+
const allRequired = envKeys.filter((ek) => ek.required && !ek.hidden);
|
|
270
|
+
const allSet = allRequired.length === 0 || allRequired.every((ek) => ek.set);
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="px-5 py-5 space-y-5">
|
|
274
|
+
{/* Header */}
|
|
275
|
+
<div className="flex items-center gap-3">
|
|
276
|
+
<IntegrationIcon item={item} size={36} />
|
|
277
|
+
<div>
|
|
278
|
+
<h2 className="text-sm font-bold text-text-0 font-sans">Configure {item.name}</h2>
|
|
279
|
+
<p className="text-2xs text-text-3 font-sans">Set up credentials to connect</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Setup steps */}
|
|
284
|
+
{item.setupSteps?.length > 0 && (
|
|
285
|
+
<div className="bg-surface-2 rounded-md px-4 py-3 space-y-2">
|
|
286
|
+
<span className="text-xs font-semibold text-text-1 font-sans">Setup guide</span>
|
|
287
|
+
<ol className="space-y-1.5">
|
|
288
|
+
{item.setupSteps.map((step, i) => (
|
|
289
|
+
<li key={i} className="flex gap-2 text-xs text-text-2 font-sans leading-relaxed">
|
|
290
|
+
<span className="text-text-4 font-mono flex-shrink-0 w-4 text-right">{i + 1}.</span>
|
|
291
|
+
<span>{step}</span>
|
|
292
|
+
</li>
|
|
293
|
+
))}
|
|
294
|
+
</ol>
|
|
295
|
+
{item.setupUrl && (
|
|
296
|
+
<a
|
|
297
|
+
href={item.setupUrl}
|
|
298
|
+
target="_blank"
|
|
299
|
+
rel="noopener noreferrer"
|
|
300
|
+
className="inline-flex items-center gap-1 text-xs text-accent font-sans hover:underline mt-1"
|
|
301
|
+
>
|
|
302
|
+
<ExternalLink size={11} />
|
|
303
|
+
{new URL(item.setupUrl).hostname}
|
|
304
|
+
</a>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
<div className="h-px bg-border-subtle" />
|
|
310
|
+
|
|
311
|
+
{/* Auth type specific UI */}
|
|
312
|
+
{authType === 'api-key' && (
|
|
313
|
+
<div className="space-y-4">
|
|
314
|
+
{envKeys.filter((ek) => !ek.hidden).map((ek) => (
|
|
315
|
+
<CredentialRow
|
|
316
|
+
key={ek.key}
|
|
317
|
+
integrationId={item.id}
|
|
318
|
+
envKey={ek}
|
|
319
|
+
onSaved={onRefreshStatus}
|
|
320
|
+
/>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{authType === 'google-autoauth' && (
|
|
326
|
+
<div className="space-y-3">
|
|
327
|
+
<Button
|
|
328
|
+
variant="primary"
|
|
329
|
+
size="lg"
|
|
330
|
+
onClick={handleGoogleAutoAuth}
|
|
331
|
+
disabled={authenticating}
|
|
332
|
+
className="w-full gap-2"
|
|
333
|
+
>
|
|
334
|
+
{authenticating ? (
|
|
335
|
+
<>
|
|
336
|
+
<Loader2 size={14} className="animate-spin" />
|
|
337
|
+
Opening browser...
|
|
338
|
+
</>
|
|
339
|
+
) : (
|
|
340
|
+
<>
|
|
341
|
+
<img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
|
|
342
|
+
Sign in with Google
|
|
343
|
+
</>
|
|
344
|
+
)}
|
|
345
|
+
</Button>
|
|
346
|
+
<p className="text-2xs text-text-4 font-sans text-center">
|
|
347
|
+
A browser window will open for Google authorization
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{authType === 'oauth-google' && (
|
|
353
|
+
<div className="space-y-3">
|
|
354
|
+
<Button
|
|
355
|
+
variant="primary"
|
|
356
|
+
size="lg"
|
|
357
|
+
onClick={handleOAuthStart}
|
|
358
|
+
disabled={authenticating}
|
|
359
|
+
className="w-full gap-2"
|
|
360
|
+
>
|
|
361
|
+
{authenticating ? (
|
|
362
|
+
<>
|
|
363
|
+
<Loader2 size={14} className="animate-spin" />
|
|
364
|
+
Connecting...
|
|
365
|
+
</>
|
|
366
|
+
) : (
|
|
367
|
+
<>
|
|
368
|
+
<img src="https://cdn.simpleicons.org/google/white" alt="" className="w-4 h-4" />
|
|
369
|
+
Connect with Google
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
</Button>
|
|
373
|
+
<p className="text-2xs text-text-4 font-sans text-center">
|
|
374
|
+
Authorize Groove to access your {item.name}
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
{/* Done button */}
|
|
380
|
+
<Button
|
|
381
|
+
variant={allSet ? 'primary' : 'secondary'}
|
|
382
|
+
size="lg"
|
|
383
|
+
onClick={onDone}
|
|
384
|
+
className="w-full gap-1.5"
|
|
385
|
+
>
|
|
386
|
+
{allSet ? (
|
|
387
|
+
<>
|
|
388
|
+
<Check size={14} />
|
|
389
|
+
Done
|
|
390
|
+
</>
|
|
391
|
+
) : (
|
|
392
|
+
'Skip for now'
|
|
393
|
+
)}
|
|
394
|
+
</Button>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Step: Done ──────────────────────────────────────────
|
|
400
|
+
function DoneStep({ item, onClose }) {
|
|
401
|
+
return (
|
|
402
|
+
<div className="px-5 py-10 flex flex-col items-center text-center space-y-4">
|
|
403
|
+
<div className="w-14 h-14 rounded-full bg-success/15 flex items-center justify-center">
|
|
404
|
+
<CheckCircle size={28} className="text-success" />
|
|
405
|
+
</div>
|
|
406
|
+
<div>
|
|
407
|
+
<h2 className="text-base font-bold text-text-0 font-sans">Integration ready</h2>
|
|
408
|
+
<p className="text-sm text-text-3 font-sans mt-1">
|
|
409
|
+
{item.name} is installed and configured. Agents can now use it.
|
|
410
|
+
</p>
|
|
411
|
+
</div>
|
|
412
|
+
<Button variant="primary" size="lg" onClick={onClose} className="mt-2">
|
|
413
|
+
Close
|
|
414
|
+
</Button>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── Main Wizard ─────────────────────────────────────────
|
|
420
|
+
export function IntegrationWizard({ integration, open, onClose }) {
|
|
421
|
+
const toast = useToast();
|
|
422
|
+
const [step, setStep] = useState('overview'); // overview | configure | done
|
|
423
|
+
const [status, setStatus] = useState(null);
|
|
424
|
+
const [installing, setInstalling] = useState(false);
|
|
425
|
+
const [loadingStatus, setLoadingStatus] = useState(true);
|
|
426
|
+
|
|
427
|
+
const fetchStatus = useCallback(async () => {
|
|
428
|
+
try {
|
|
429
|
+
const data = await api.get(`/integrations/${integration.id}/status`);
|
|
430
|
+
setStatus(data);
|
|
431
|
+
} catch {
|
|
432
|
+
setStatus(null);
|
|
433
|
+
}
|
|
434
|
+
setLoadingStatus(false);
|
|
435
|
+
}, [integration.id]);
|
|
436
|
+
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
if (open && integration) {
|
|
439
|
+
setStep('overview');
|
|
440
|
+
setLoadingStatus(true);
|
|
441
|
+
fetchStatus();
|
|
442
|
+
}
|
|
443
|
+
}, [open, integration, fetchStatus]);
|
|
444
|
+
|
|
445
|
+
async function handleInstall() {
|
|
446
|
+
setInstalling(true);
|
|
447
|
+
try {
|
|
448
|
+
await api.post(`/integrations/${integration.id}/install`);
|
|
449
|
+
toast.success(`${integration.name} installed`);
|
|
450
|
+
await fetchStatus();
|
|
451
|
+
setStep('configure');
|
|
452
|
+
} catch (err) {
|
|
453
|
+
toast.error('Install failed', err.message);
|
|
454
|
+
}
|
|
455
|
+
setInstalling(false);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function handleUninstall() {
|
|
459
|
+
try {
|
|
460
|
+
await api.delete(`/integrations/${integration.id}`);
|
|
461
|
+
toast.success(`${integration.name} uninstalled`);
|
|
462
|
+
await fetchStatus();
|
|
463
|
+
} catch (err) {
|
|
464
|
+
toast.error('Uninstall failed', err.message);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function handleConfigureNext() {
|
|
469
|
+
setStep('configure');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function handleDone() {
|
|
473
|
+
setStep('done');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!integration) return null;
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
|
|
480
|
+
<DialogContent
|
|
481
|
+
title={step === 'overview' ? integration.name : step === 'configure' ? 'Configure' : 'Complete'}
|
|
482
|
+
description={`Setup wizard for ${integration.name}`}
|
|
483
|
+
className="max-w-md"
|
|
484
|
+
>
|
|
485
|
+
{loadingStatus ? (
|
|
486
|
+
<div className="px-5 py-10 flex items-center justify-center">
|
|
487
|
+
<Loader2 size={20} className="animate-spin text-text-4" />
|
|
488
|
+
</div>
|
|
489
|
+
) : step === 'overview' ? (
|
|
490
|
+
<OverviewStep
|
|
491
|
+
item={integration}
|
|
492
|
+
status={status}
|
|
493
|
+
installing={installing}
|
|
494
|
+
onInstall={handleInstall}
|
|
495
|
+
onUninstall={handleUninstall}
|
|
496
|
+
onNext={handleConfigureNext}
|
|
497
|
+
/>
|
|
498
|
+
) : step === 'configure' ? (
|
|
499
|
+
<ConfigureStep
|
|
500
|
+
item={integration}
|
|
501
|
+
status={status}
|
|
502
|
+
onDone={handleDone}
|
|
503
|
+
onRefreshStatus={fetchStatus}
|
|
504
|
+
/>
|
|
505
|
+
) : (
|
|
506
|
+
<DoneStep item={integration} onClose={onClose} />
|
|
507
|
+
)}
|
|
508
|
+
</DialogContent>
|
|
509
|
+
</Dialog>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useDashboard } from '../lib/hooks/use-dashboard';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
3
4
|
import { DashboardHeader } from '../components/dashboard/header-bar';
|
|
4
5
|
import { KpiStrip } from '../components/dashboard/kpi-card';
|
|
5
6
|
import { FleetPanel } from '../components/dashboard/fleet-panel';
|
|
@@ -36,6 +37,7 @@ export default function DashboardView() {
|
|
|
36
37
|
agentBreakdown, routing, rotation, adaptive, journalist, rotating,
|
|
37
38
|
} = useDashboard();
|
|
38
39
|
|
|
40
|
+
const teams = useGrooveStore((s) => s.teams);
|
|
39
41
|
const runningCount = agents.filter((a) => a.status === 'running').length;
|
|
40
42
|
|
|
41
43
|
if (!connected) {
|
|
@@ -143,7 +145,7 @@ export default function DashboardView() {
|
|
|
143
145
|
<div className="px-3 pt-2.5 pb-1 flex-shrink-0">
|
|
144
146
|
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
|
|
145
147
|
</div>
|
|
146
|
-
<FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} />
|
|
148
|
+
<FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
|
|
147
149
|
</div>
|
|
148
150
|
|
|
149
151
|
{/* R4C2-3: Intel Panel (spans 2 cols) */}
|
|
@@ -17,6 +17,7 @@ import { api } from '../lib/api';
|
|
|
17
17
|
import { useToast } from '../lib/hooks/use-toast';
|
|
18
18
|
import { fmtNum, timeAgo } from '../lib/format';
|
|
19
19
|
import { useGrooveStore } from '../stores/groove';
|
|
20
|
+
import { IntegrationWizard } from '../components/marketplace/integration-wizard';
|
|
20
21
|
import {
|
|
21
22
|
ChevronLeft, ChevronDown, Sparkles, Plug, LogIn, LogOut,
|
|
22
23
|
User, Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
|
|
@@ -278,15 +279,30 @@ function IntegrationsBrowse() {
|
|
|
278
279
|
const [items, setItems] = useState([]);
|
|
279
280
|
const [loading, setLoading] = useState(true);
|
|
280
281
|
const [search, setSearch] = useState('');
|
|
281
|
-
const
|
|
282
|
+
const [selectedIntegration, setSelectedIntegration] = useState(null);
|
|
283
|
+
const [showWizard, setShowWizard] = useState(false);
|
|
282
284
|
|
|
283
|
-
|
|
285
|
+
const fetchItems = () => {
|
|
284
286
|
setLoading(true);
|
|
285
287
|
api.get(`/integrations/registry?search=${encodeURIComponent(search)}`)
|
|
286
288
|
.then((d) => setItems(d.integrations || d.items || (Array.isArray(d) ? d : [])))
|
|
287
289
|
.catch(() => setItems([]))
|
|
288
290
|
.finally(() => setLoading(false));
|
|
289
|
-
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
useEffect(() => { fetchItems(); }, [search]);
|
|
294
|
+
|
|
295
|
+
function handleCardClick(item) {
|
|
296
|
+
setSelectedIntegration(item);
|
|
297
|
+
setShowWizard(true);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleWizardClose() {
|
|
301
|
+
setShowWizard(false);
|
|
302
|
+
setSelectedIntegration(null);
|
|
303
|
+
// Refresh list to pick up install/uninstall changes
|
|
304
|
+
fetchItems();
|
|
305
|
+
}
|
|
290
306
|
|
|
291
307
|
return (
|
|
292
308
|
<ScrollArea className="h-full">
|
|
@@ -302,7 +318,7 @@ function IntegrationsBrowse() {
|
|
|
302
318
|
<div className="mt-4 grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))' }}>
|
|
303
319
|
{loading
|
|
304
320
|
? Array.from({ length: 6 }).map((_, i) => <SkillCardSkeleton key={i} />)
|
|
305
|
-
: items.map((item) => <MarketplaceCard key={item.id} item={item} onClick={() =>
|
|
321
|
+
: items.map((item) => <MarketplaceCard key={item.id} item={item} onClick={() => handleCardClick(item)} />)
|
|
306
322
|
}
|
|
307
323
|
</div>
|
|
308
324
|
|
|
@@ -310,6 +326,12 @@ function IntegrationsBrowse() {
|
|
|
310
326
|
<div className="text-center py-16 text-text-4 font-sans text-sm">No integrations found.</div>
|
|
311
327
|
)}
|
|
312
328
|
</div>
|
|
329
|
+
|
|
330
|
+
<IntegrationWizard
|
|
331
|
+
integration={selectedIntegration}
|
|
332
|
+
open={showWizard}
|
|
333
|
+
onClose={handleWizardClose}
|
|
334
|
+
/>
|
|
313
335
|
</ScrollArea>
|
|
314
336
|
);
|
|
315
337
|
}
|