@greatapps/greatagents-ui 0.3.3 → 0.3.5
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/index.d.ts +214 -3
- package/dist/index.js +2532 -850
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +14 -0
- package/src/components/agents/agent-form-dialog.tsx +109 -29
- package/src/components/agents/agent-tabs.tsx +39 -2
- package/src/components/capabilities/advanced-tab.tsx +82 -0
- package/src/components/capabilities/capabilities-tab.tsx +475 -0
- package/src/components/capabilities/integration-card.tsx +162 -0
- package/src/components/capabilities/integration-wizard.tsx +537 -0
- package/src/components/capabilities/integrations-tab.tsx +61 -0
- package/src/components/capabilities/types.ts +48 -0
- package/src/components/capabilities/wizard-steps/config-step.tsx +117 -0
- package/src/components/capabilities/wizard-steps/confirm-step.tsx +123 -0
- package/src/components/capabilities/wizard-steps/credentials-step.tsx +205 -0
- package/src/components/capabilities/wizard-steps/info-step.tsx +78 -0
- package/src/data/integrations-registry.ts +23 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-capabilities.ts +50 -0
- package/src/hooks/use-integrations.ts +114 -0
- package/src/index.ts +34 -0
- package/src/pages/agent-capabilities-page.tsx +159 -0
- package/src/pages/index.ts +2 -0
- package/src/pages/integrations-management-page.tsx +166 -0
- package/src/types/capabilities.ts +32 -0
- package/src/types/index.ts +10 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
4
|
+
import type { GagentsHookConfig } from "../../hooks/types";
|
|
5
|
+
import {
|
|
6
|
+
useCapabilities,
|
|
7
|
+
useAgentCapabilities,
|
|
8
|
+
useUpdateAgentCapabilities,
|
|
9
|
+
} from "../../hooks";
|
|
10
|
+
import type {
|
|
11
|
+
CapabilityCategory,
|
|
12
|
+
CapabilityModule,
|
|
13
|
+
AgentCapability,
|
|
14
|
+
AgentCapabilitiesPayload,
|
|
15
|
+
} from "../../types/capabilities";
|
|
16
|
+
import {
|
|
17
|
+
Accordion,
|
|
18
|
+
AccordionItem,
|
|
19
|
+
AccordionTrigger,
|
|
20
|
+
AccordionContent,
|
|
21
|
+
Switch,
|
|
22
|
+
Checkbox,
|
|
23
|
+
Badge,
|
|
24
|
+
Button,
|
|
25
|
+
Skeleton,
|
|
26
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
27
|
+
import {
|
|
28
|
+
Calendar,
|
|
29
|
+
Users,
|
|
30
|
+
Settings,
|
|
31
|
+
HeartHandshake,
|
|
32
|
+
Package,
|
|
33
|
+
ChevronDown,
|
|
34
|
+
} from "lucide-react";
|
|
35
|
+
import { toast } from "sonner";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Operation label mapping (pt-BR)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const OPERATION_LABELS: Record<string, string> = {
|
|
42
|
+
list: "Listar",
|
|
43
|
+
view: "Visualizar",
|
|
44
|
+
create: "Criar",
|
|
45
|
+
update: "Atualizar",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getOperationLabel(slug: string): string {
|
|
49
|
+
return OPERATION_LABELS[slug] ?? slug;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Category icon mapping
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const CATEGORY_ICONS: Record<string, React.ElementType> = {
|
|
57
|
+
agenda: Calendar,
|
|
58
|
+
cadastros: Users,
|
|
59
|
+
infraestrutura: Settings,
|
|
60
|
+
relacionamentos: HeartHandshake,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function getCategoryIcon(slug: string): React.ElementType {
|
|
64
|
+
return CATEGORY_ICONS[slug] ?? Package;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Internal state helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Map from module slug -> set of enabled operation slugs */
|
|
72
|
+
type CapabilityState = Map<string, Set<string>>;
|
|
73
|
+
|
|
74
|
+
function buildStateFromAgent(agentCaps: AgentCapability[]): CapabilityState {
|
|
75
|
+
const state: CapabilityState = new Map();
|
|
76
|
+
for (const cap of agentCaps) {
|
|
77
|
+
state.set(cap.module, new Set(cap.operations));
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stateToPayload(state: CapabilityState): AgentCapabilitiesPayload {
|
|
83
|
+
const capabilities: AgentCapability[] = [];
|
|
84
|
+
state.forEach((ops, mod) => {
|
|
85
|
+
if (ops.size > 0) {
|
|
86
|
+
capabilities.push({ module: mod, operations: Array.from(ops) });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return { capabilities };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cloneState(state: CapabilityState): CapabilityState {
|
|
93
|
+
const next: CapabilityState = new Map();
|
|
94
|
+
state.forEach((ops, mod) => next.set(mod, new Set(ops)));
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function statesEqual(a: CapabilityState, b: CapabilityState): boolean {
|
|
99
|
+
if (a.size !== b.size) return false;
|
|
100
|
+
for (const [mod, opsA] of a) {
|
|
101
|
+
const opsB = b.get(mod);
|
|
102
|
+
if (!opsB || opsA.size !== opsB.size) return false;
|
|
103
|
+
for (const op of opsA) {
|
|
104
|
+
if (!opsB.has(op)) return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Props
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export interface CapabilitiesTabProps {
|
|
115
|
+
config: GagentsHookConfig;
|
|
116
|
+
agentId: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Component
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
|
|
124
|
+
const { data: registry, isLoading: isLoadingRegistry } = useCapabilities(config);
|
|
125
|
+
const { data: agentCaps, isLoading: isLoadingAgent } = useAgentCapabilities(config, agentId);
|
|
126
|
+
const updateMutation = useUpdateAgentCapabilities(config);
|
|
127
|
+
|
|
128
|
+
// Local state for optimistic updates
|
|
129
|
+
const [localState, setLocalState] = useState<CapabilityState>(new Map());
|
|
130
|
+
const [serverState, setServerState] = useState<CapabilityState>(new Map());
|
|
131
|
+
const [initialized, setInitialized] = useState(false);
|
|
132
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
133
|
+
|
|
134
|
+
// Sync server data into local state on first load / refetch
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (agentCaps && !initialized) {
|
|
137
|
+
const state = buildStateFromAgent(agentCaps);
|
|
138
|
+
setLocalState(state);
|
|
139
|
+
setServerState(cloneState(state));
|
|
140
|
+
setInitialized(true);
|
|
141
|
+
}
|
|
142
|
+
}, [agentCaps, initialized]);
|
|
143
|
+
|
|
144
|
+
// Reset initialized when agentId changes
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setInitialized(false);
|
|
147
|
+
}, [agentId]);
|
|
148
|
+
|
|
149
|
+
const hasChanges = useMemo(
|
|
150
|
+
() => initialized && !statesEqual(localState, serverState),
|
|
151
|
+
[localState, serverState, initialized],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ------ Debounced save ------
|
|
155
|
+
const scheduleSave = useCallback(
|
|
156
|
+
(nextState: CapabilityState) => {
|
|
157
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
158
|
+
debounceRef.current = setTimeout(() => {
|
|
159
|
+
const payload = stateToPayload(nextState);
|
|
160
|
+
updateMutation.mutate(
|
|
161
|
+
{ agentId, payload },
|
|
162
|
+
{
|
|
163
|
+
onSuccess: () => {
|
|
164
|
+
setServerState(cloneState(nextState));
|
|
165
|
+
toast.success("Capacidades salvas");
|
|
166
|
+
},
|
|
167
|
+
onError: () => {
|
|
168
|
+
// Rollback to server state
|
|
169
|
+
setLocalState(cloneState(serverState));
|
|
170
|
+
toast.error("Erro ao salvar capacidades");
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}, 500);
|
|
175
|
+
},
|
|
176
|
+
[agentId, updateMutation, serverState],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ------ State mutation helpers ------
|
|
180
|
+
const updateState = useCallback(
|
|
181
|
+
(updater: (prev: CapabilityState) => CapabilityState) => {
|
|
182
|
+
setLocalState((prev) => {
|
|
183
|
+
const next = updater(prev);
|
|
184
|
+
scheduleSave(next);
|
|
185
|
+
return next;
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
[scheduleSave],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const toggleModule = useCallback(
|
|
192
|
+
(mod: CapabilityModule, enabled: boolean) => {
|
|
193
|
+
updateState((prev) => {
|
|
194
|
+
const next = cloneState(prev);
|
|
195
|
+
if (enabled) {
|
|
196
|
+
next.set(mod.slug, new Set(mod.operations));
|
|
197
|
+
} else {
|
|
198
|
+
next.delete(mod.slug);
|
|
199
|
+
}
|
|
200
|
+
return next;
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
[updateState],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const toggleOperation = useCallback(
|
|
207
|
+
(mod: CapabilityModule, opSlug: string, enabled: boolean) => {
|
|
208
|
+
updateState((prev) => {
|
|
209
|
+
const next = cloneState(prev);
|
|
210
|
+
const ops = new Set(next.get(mod.slug) ?? []);
|
|
211
|
+
if (enabled) {
|
|
212
|
+
ops.add(opSlug);
|
|
213
|
+
} else {
|
|
214
|
+
ops.delete(opSlug);
|
|
215
|
+
}
|
|
216
|
+
if (ops.size > 0) {
|
|
217
|
+
next.set(mod.slug, ops);
|
|
218
|
+
} else {
|
|
219
|
+
next.delete(mod.slug);
|
|
220
|
+
}
|
|
221
|
+
return next;
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
[updateState],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const enableAll = useCallback(() => {
|
|
228
|
+
if (!registry) return;
|
|
229
|
+
updateState(() => {
|
|
230
|
+
const next: CapabilityState = new Map();
|
|
231
|
+
for (const cat of registry.categories) {
|
|
232
|
+
for (const mod of cat.modules) {
|
|
233
|
+
next.set(mod.slug, new Set(mod.operations));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return next;
|
|
237
|
+
});
|
|
238
|
+
}, [registry, updateState]);
|
|
239
|
+
|
|
240
|
+
const disableAll = useCallback(() => {
|
|
241
|
+
updateState(() => new Map());
|
|
242
|
+
}, [updateState]);
|
|
243
|
+
|
|
244
|
+
const discard = useCallback(() => {
|
|
245
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
246
|
+
setLocalState(cloneState(serverState));
|
|
247
|
+
}, [serverState]);
|
|
248
|
+
|
|
249
|
+
const saveNow = useCallback(() => {
|
|
250
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
251
|
+
const payload = stateToPayload(localState);
|
|
252
|
+
updateMutation.mutate(
|
|
253
|
+
{ agentId, payload },
|
|
254
|
+
{
|
|
255
|
+
onSuccess: () => {
|
|
256
|
+
setServerState(cloneState(localState));
|
|
257
|
+
toast.success("Capacidades salvas");
|
|
258
|
+
},
|
|
259
|
+
onError: () => {
|
|
260
|
+
setLocalState(cloneState(serverState));
|
|
261
|
+
toast.error("Erro ao salvar capacidades");
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}, [agentId, localState, serverState, updateMutation]);
|
|
266
|
+
|
|
267
|
+
// ------ Counting helpers ------
|
|
268
|
+
function countActiveModules(cat: CapabilityCategory): number {
|
|
269
|
+
return cat.modules.filter((m) => (localState.get(m.slug)?.size ?? 0) > 0).length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ------ Loading state ------
|
|
273
|
+
if (isLoadingRegistry || isLoadingAgent) {
|
|
274
|
+
return (
|
|
275
|
+
<div className="space-y-3">
|
|
276
|
+
{[1, 2, 3, 4].map((i) => (
|
|
277
|
+
<Skeleton key={i} className="h-14 w-full" />
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ------ Empty state ------
|
|
284
|
+
if (!registry || !registry.categories.length) {
|
|
285
|
+
return (
|
|
286
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
287
|
+
<Package className="h-12 w-12 text-muted-foreground mb-3" />
|
|
288
|
+
<h3 className="text-base font-medium">Nenhuma capacidade disponível</h3>
|
|
289
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-sm">
|
|
290
|
+
Este produto ainda não possui capacidades registadas. As capacidades serão
|
|
291
|
+
adicionadas automaticamente quando o produto for configurado.
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ------ Render ------
|
|
298
|
+
return (
|
|
299
|
+
<div className="space-y-4">
|
|
300
|
+
{/* Header with global actions */}
|
|
301
|
+
<div className="flex items-center justify-between">
|
|
302
|
+
<div>
|
|
303
|
+
<h3 className="text-sm font-medium">Capacidades do agente</h3>
|
|
304
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
305
|
+
Ative ou desative módulos e operações disponíveis para este agente.
|
|
306
|
+
</p>
|
|
307
|
+
</div>
|
|
308
|
+
<div className="flex items-center gap-2">
|
|
309
|
+
<Button variant="outline" size="sm" onClick={enableAll}>
|
|
310
|
+
Ativar tudo
|
|
311
|
+
</Button>
|
|
312
|
+
<Button variant="outline" size="sm" onClick={disableAll}>
|
|
313
|
+
Desativar tudo
|
|
314
|
+
</Button>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Category accordions */}
|
|
319
|
+
<Accordion type="multiple" className="space-y-2">
|
|
320
|
+
{registry.categories.map((cat) => {
|
|
321
|
+
const Icon = getCategoryIcon(cat.slug);
|
|
322
|
+
const activeCount = countActiveModules(cat);
|
|
323
|
+
const totalModules = cat.modules.length;
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<AccordionItem
|
|
327
|
+
key={cat.slug}
|
|
328
|
+
value={cat.slug}
|
|
329
|
+
className="border rounded-lg px-4"
|
|
330
|
+
>
|
|
331
|
+
<AccordionTrigger className="hover:no-underline py-3">
|
|
332
|
+
<div className="flex items-center gap-3 flex-1">
|
|
333
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
334
|
+
<span className="font-medium text-sm">{cat.label}</span>
|
|
335
|
+
<Badge variant="secondary" className="text-xs">
|
|
336
|
+
{activeCount} de {totalModules} módulos ativos
|
|
337
|
+
</Badge>
|
|
338
|
+
</div>
|
|
339
|
+
</AccordionTrigger>
|
|
340
|
+
<AccordionContent className="pb-3">
|
|
341
|
+
<div className="space-y-1">
|
|
342
|
+
{cat.modules.map((mod) => {
|
|
343
|
+
const enabledOps = localState.get(mod.slug);
|
|
344
|
+
const isModuleOn = (enabledOps?.size ?? 0) > 0;
|
|
345
|
+
const allOpsEnabled =
|
|
346
|
+
enabledOps?.size === mod.operations.length;
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<ModuleRow
|
|
350
|
+
key={mod.slug}
|
|
351
|
+
module={mod}
|
|
352
|
+
isOn={isModuleOn}
|
|
353
|
+
allOpsEnabled={allOpsEnabled}
|
|
354
|
+
enabledOps={enabledOps ?? new Set()}
|
|
355
|
+
onToggleModule={(on) => toggleModule(mod, on)}
|
|
356
|
+
onToggleOperation={(op, on) =>
|
|
357
|
+
toggleOperation(mod, op, on)
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
);
|
|
361
|
+
})}
|
|
362
|
+
</div>
|
|
363
|
+
</AccordionContent>
|
|
364
|
+
</AccordionItem>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</Accordion>
|
|
368
|
+
|
|
369
|
+
{/* Save bar */}
|
|
370
|
+
{hasChanges && (
|
|
371
|
+
<div className="sticky bottom-0 bg-background border-t py-3 px-4 -mx-4 flex items-center justify-between">
|
|
372
|
+
<span className="text-sm text-muted-foreground">
|
|
373
|
+
Você tem alterações não salvas.
|
|
374
|
+
</span>
|
|
375
|
+
<div className="flex items-center gap-2">
|
|
376
|
+
<Button variant="outline" size="sm" onClick={discard}>
|
|
377
|
+
Descartar
|
|
378
|
+
</Button>
|
|
379
|
+
<Button
|
|
380
|
+
size="sm"
|
|
381
|
+
onClick={saveNow}
|
|
382
|
+
disabled={updateMutation.isPending}
|
|
383
|
+
>
|
|
384
|
+
{updateMutation.isPending ? "Salvando..." : "Salvar"}
|
|
385
|
+
</Button>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// ModuleRow sub-component
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
interface ModuleRowProps {
|
|
398
|
+
module: CapabilityModule;
|
|
399
|
+
isOn: boolean;
|
|
400
|
+
allOpsEnabled: boolean;
|
|
401
|
+
enabledOps: Set<string>;
|
|
402
|
+
onToggleModule: (on: boolean) => void;
|
|
403
|
+
onToggleOperation: (op: string, on: boolean) => void;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function ModuleRow({
|
|
407
|
+
module: mod,
|
|
408
|
+
isOn,
|
|
409
|
+
enabledOps,
|
|
410
|
+
onToggleModule,
|
|
411
|
+
onToggleOperation,
|
|
412
|
+
}: ModuleRowProps) {
|
|
413
|
+
const [expanded, setExpanded] = useState(false);
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<div className="rounded-md border px-3 py-2">
|
|
417
|
+
<div className="flex items-center justify-between">
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
className="flex items-center gap-2 flex-1 text-left"
|
|
421
|
+
onClick={() => setExpanded(!expanded)}
|
|
422
|
+
aria-expanded={expanded}
|
|
423
|
+
aria-label={`Expandir ${mod.label}`}
|
|
424
|
+
>
|
|
425
|
+
<ChevronDown
|
|
426
|
+
className={`h-3.5 w-3.5 text-muted-foreground transition-transform ${
|
|
427
|
+
expanded ? "rotate-0" : "-rotate-90"
|
|
428
|
+
}`}
|
|
429
|
+
/>
|
|
430
|
+
<span className="text-sm font-medium">{mod.label}</span>
|
|
431
|
+
{mod.description && (
|
|
432
|
+
<span className="text-xs text-muted-foreground hidden sm:inline">
|
|
433
|
+
— {mod.description}
|
|
434
|
+
</span>
|
|
435
|
+
)}
|
|
436
|
+
{isOn && (
|
|
437
|
+
<Badge variant="secondary" className="text-xs ml-1">
|
|
438
|
+
{enabledOps.size}/{mod.operations.length}
|
|
439
|
+
</Badge>
|
|
440
|
+
)}
|
|
441
|
+
</button>
|
|
442
|
+
<Switch
|
|
443
|
+
checked={isOn}
|
|
444
|
+
onCheckedChange={onToggleModule}
|
|
445
|
+
aria-label={`Ativar módulo ${mod.label}`}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{expanded && (
|
|
450
|
+
<div className="mt-2 ml-6 flex flex-wrap gap-x-5 gap-y-1.5 pb-1">
|
|
451
|
+
{mod.operations.map((op) => {
|
|
452
|
+
const checked = enabledOps.has(op);
|
|
453
|
+
return (
|
|
454
|
+
<label
|
|
455
|
+
key={op}
|
|
456
|
+
className="flex items-center gap-1.5 text-sm cursor-pointer"
|
|
457
|
+
>
|
|
458
|
+
<Checkbox
|
|
459
|
+
checked={checked}
|
|
460
|
+
onCheckedChange={(val) =>
|
|
461
|
+
onToggleOperation(op, val === true)
|
|
462
|
+
}
|
|
463
|
+
aria-label={`${getOperationLabel(op)} em ${mod.label}`}
|
|
464
|
+
/>
|
|
465
|
+
<span className={checked ? "" : "text-muted-foreground"}>
|
|
466
|
+
{getOperationLabel(op)}
|
|
467
|
+
</span>
|
|
468
|
+
</label>
|
|
469
|
+
);
|
|
470
|
+
})}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { IntegrationCardData, IntegrationCardState } from "../../hooks/use-integrations";
|
|
4
|
+
import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from "@greatapps/greatauth-ui/ui";
|
|
5
|
+
import {
|
|
6
|
+
CalendarSync,
|
|
7
|
+
Plug,
|
|
8
|
+
Settings,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Users,
|
|
11
|
+
Clock,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import type { LucideIcon } from "lucide-react";
|
|
14
|
+
import { cn } from "../../lib";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Icon mapping — extend as new integrations are added
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const ICON_MAP: Record<string, LucideIcon> = {
|
|
21
|
+
CalendarSync,
|
|
22
|
+
Plug,
|
|
23
|
+
Settings,
|
|
24
|
+
RefreshCw,
|
|
25
|
+
Users,
|
|
26
|
+
Clock,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveIcon(name: string): LucideIcon {
|
|
30
|
+
return ICON_MAP[name] ?? Plug;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Badge helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
interface BadgeVariant {
|
|
38
|
+
label: string;
|
|
39
|
+
className: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STATE_BADGES: Record<IntegrationCardState, BadgeVariant> = {
|
|
43
|
+
available: {
|
|
44
|
+
label: "Disponível",
|
|
45
|
+
className: "bg-muted text-muted-foreground",
|
|
46
|
+
},
|
|
47
|
+
connected: {
|
|
48
|
+
label: "Conectado",
|
|
49
|
+
className: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
|
|
50
|
+
},
|
|
51
|
+
expired: {
|
|
52
|
+
label: "Expirado",
|
|
53
|
+
className: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
|
54
|
+
},
|
|
55
|
+
coming_soon: {
|
|
56
|
+
label: "Em breve",
|
|
57
|
+
className: "bg-muted text-muted-foreground",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function getActionLabel(state: IntegrationCardState): string {
|
|
62
|
+
switch (state) {
|
|
63
|
+
case "available":
|
|
64
|
+
return "Conectar";
|
|
65
|
+
case "connected":
|
|
66
|
+
return "Configurar";
|
|
67
|
+
case "expired":
|
|
68
|
+
return "Reconectar";
|
|
69
|
+
default:
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Component
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export interface IntegrationCardProps {
|
|
79
|
+
card: IntegrationCardData;
|
|
80
|
+
onConnect: (card: IntegrationCardData) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
|
|
84
|
+
const { definition, state, sharedByAgentsCount } = card;
|
|
85
|
+
const Icon = resolveIcon(definition.icon);
|
|
86
|
+
const badge = STATE_BADGES[state];
|
|
87
|
+
const actionLabel = getActionLabel(state);
|
|
88
|
+
const isComingSoon = state === "coming_soon";
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
"group relative flex flex-col gap-3 rounded-xl border bg-card p-5 transition-shadow",
|
|
94
|
+
isComingSoon
|
|
95
|
+
? "opacity-60 cursor-default"
|
|
96
|
+
: "hover:shadow-md cursor-pointer",
|
|
97
|
+
)}
|
|
98
|
+
role="button"
|
|
99
|
+
tabIndex={isComingSoon ? -1 : 0}
|
|
100
|
+
aria-label={`${definition.name} — ${badge.label}`}
|
|
101
|
+
aria-disabled={isComingSoon}
|
|
102
|
+
onClick={() => !isComingSoon && onConnect(card)}
|
|
103
|
+
onKeyDown={(e) => {
|
|
104
|
+
if (!isComingSoon && (e.key === "Enter" || e.key === " ")) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
onConnect(card);
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{/* Header row */}
|
|
111
|
+
<div className="flex items-start justify-between gap-2">
|
|
112
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
113
|
+
<Icon className="h-5 w-5" />
|
|
114
|
+
</div>
|
|
115
|
+
<Badge variant="outline" className={cn("text-xs", badge.className)}>
|
|
116
|
+
{badge.label}
|
|
117
|
+
</Badge>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Name + description */}
|
|
121
|
+
<div className="space-y-1">
|
|
122
|
+
<h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
|
|
123
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
124
|
+
{definition.description}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Footer */}
|
|
129
|
+
<div className="mt-auto flex items-center justify-between gap-2 pt-1">
|
|
130
|
+
{sharedByAgentsCount > 0 ? (
|
|
131
|
+
<Tooltip>
|
|
132
|
+
<TooltipTrigger asChild>
|
|
133
|
+
<span className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
|
|
134
|
+
<Users className="h-3.5 w-3.5" />
|
|
135
|
+
Compartilhada
|
|
136
|
+
</span>
|
|
137
|
+
</TooltipTrigger>
|
|
138
|
+
<TooltipContent>
|
|
139
|
+
Esta credencial está disponível para todos os agentes da conta
|
|
140
|
+
</TooltipContent>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
) : (
|
|
143
|
+
<span />
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{!isComingSoon && (
|
|
147
|
+
<Button
|
|
148
|
+
variant={state === "expired" ? "destructive" : "outline"}
|
|
149
|
+
size="sm"
|
|
150
|
+
className="text-xs"
|
|
151
|
+
onClick={(e) => {
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
onConnect(card);
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{actionLabel}
|
|
157
|
+
</Button>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|