@agent-native/dispatch 0.15.13 → 0.15.16
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/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/provider-api-register.d.ts +12 -12
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +96 -72
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/routes/pages/approvals.d.ts.map +1 -1
- package/dist/routes/pages/approvals.js +13 -9
- package/dist/routes/pages/approvals.js.map +1 -1
- package/dist/routes/pages/destinations.d.ts.map +1 -1
- package/dist/routes/pages/destinations.js +33 -30
- package/dist/routes/pages/destinations.js.map +1 -1
- package/package.json +3 -3
- package/src/components/messaging-setup-panel.spec.tsx +51 -0
- package/src/components/messaging-setup-panel.tsx +601 -512
- package/src/routes/pages/approvals.tsx +75 -57
- package/src/routes/pages/destinations.tsx +107 -85
|
@@ -41,9 +41,15 @@ import {
|
|
|
41
41
|
IconMail,
|
|
42
42
|
IconPlug,
|
|
43
43
|
} from "@tabler/icons-react";
|
|
44
|
-
import { useEffect, useMemo, useState } from "react";
|
|
44
|
+
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
45
45
|
import { toast } from "sonner";
|
|
46
46
|
|
|
47
|
+
import {
|
|
48
|
+
Accordion,
|
|
49
|
+
AccordionContent,
|
|
50
|
+
AccordionItem,
|
|
51
|
+
AccordionTrigger,
|
|
52
|
+
} from "./ui/accordion";
|
|
47
53
|
import { Button } from "./ui/button";
|
|
48
54
|
import {
|
|
49
55
|
Collapsible,
|
|
@@ -118,20 +124,51 @@ function StatusPill({
|
|
|
118
124
|
}) {
|
|
119
125
|
const toneClass =
|
|
120
126
|
tone === "success"
|
|
121
|
-
? "
|
|
127
|
+
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
|
122
128
|
: tone === "warning"
|
|
123
|
-
? "
|
|
124
|
-
: "
|
|
129
|
+
? "bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
|
130
|
+
: "bg-muted text-muted-foreground";
|
|
125
131
|
|
|
126
132
|
return (
|
|
127
133
|
<span
|
|
128
|
-
className={`inline-flex items-center rounded-full
|
|
134
|
+
className={`inline-flex items-center rounded-full px-2 py-1 text-[11px] font-medium ${toneClass}`}
|
|
129
135
|
>
|
|
130
136
|
{label}
|
|
131
137
|
</span>
|
|
132
138
|
);
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
function DisclosureSection({
|
|
142
|
+
title,
|
|
143
|
+
summary,
|
|
144
|
+
children,
|
|
145
|
+
defaultOpen = false,
|
|
146
|
+
}: {
|
|
147
|
+
title: string;
|
|
148
|
+
summary?: string;
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
defaultOpen?: boolean;
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<Collapsible defaultOpen={defaultOpen} className="border-t">
|
|
154
|
+
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center justify-between gap-3 py-3 text-left">
|
|
155
|
+
<span className="min-w-0">
|
|
156
|
+
<span className="block text-sm font-medium text-foreground">
|
|
157
|
+
{title}
|
|
158
|
+
</span>
|
|
159
|
+
{summary ? (
|
|
160
|
+
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
161
|
+
{summary}
|
|
162
|
+
</span>
|
|
163
|
+
) : null}
|
|
164
|
+
</span>
|
|
165
|
+
<IconChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
166
|
+
</CollapsibleTrigger>
|
|
167
|
+
<CollapsibleContent className="pb-3">{children}</CollapsibleContent>
|
|
168
|
+
</Collapsible>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
/** Render a non-secret env value (e.g. EMAIL_AGENT_ADDRESS) as a copyable
|
|
136
173
|
* text block. We can't read the actual value from the backend (env-status
|
|
137
174
|
* only reports `configured: true|false`), so we offer a one-click reveal
|
|
@@ -443,7 +480,11 @@ export function MessagingSetupPanel() {
|
|
|
443
480
|
|
|
444
481
|
return (
|
|
445
482
|
<div className="space-y-4">
|
|
446
|
-
<
|
|
483
|
+
<Accordion
|
|
484
|
+
type="single"
|
|
485
|
+
collapsible
|
|
486
|
+
className="overflow-hidden rounded-2xl border bg-card"
|
|
487
|
+
>
|
|
447
488
|
{CHANNELS.map((platform) => {
|
|
448
489
|
const status = statusByPlatform.get(platform.id);
|
|
449
490
|
const configured =
|
|
@@ -464,20 +505,27 @@ export function MessagingSetupPanel() {
|
|
|
464
505
|
envKeys,
|
|
465
506
|
envStatusByKey,
|
|
466
507
|
);
|
|
508
|
+
const configuredCredentialCount = envKeys.filter(
|
|
509
|
+
(envKey) => envStatusByKey.get(envKey.key)?.configured,
|
|
510
|
+
).length;
|
|
511
|
+
const credentialSummary = envLoading
|
|
512
|
+
? "Checking saved credentials"
|
|
513
|
+
: missingRequiredCredentials
|
|
514
|
+
? "Required credentials are missing"
|
|
515
|
+
: `${configuredCredentialCount} saved`;
|
|
467
516
|
const Icon = PLATFORM_ICONS[platform.iconKey] ?? IconPlug;
|
|
468
517
|
|
|
469
518
|
return (
|
|
470
|
-
<
|
|
519
|
+
<AccordionItem
|
|
471
520
|
key={platform.id}
|
|
472
|
-
|
|
521
|
+
value={platform.id}
|
|
522
|
+
className="border-b last:border-b-0"
|
|
473
523
|
>
|
|
474
|
-
<
|
|
475
|
-
<div className="flex items-
|
|
476
|
-
<
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<div>
|
|
480
|
-
<div className="flex items-center gap-2">
|
|
524
|
+
<AccordionTrigger className="gap-3 px-4 text-left hover:no-underline">
|
|
525
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
526
|
+
<Icon className="h-5 w-5 shrink-0 text-muted-foreground" />
|
|
527
|
+
<div className="min-w-0">
|
|
528
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
481
529
|
<h3 className="text-base font-semibold text-foreground">
|
|
482
530
|
{platform.name}
|
|
483
531
|
</h3>
|
|
@@ -486,289 +534,339 @@ export function MessagingSetupPanel() {
|
|
|
486
534
|
enabled={enabled}
|
|
487
535
|
/>
|
|
488
536
|
</div>
|
|
489
|
-
<p className="mt-1 text-sm text-muted-foreground">
|
|
537
|
+
<p className="mt-1 truncate text-sm text-muted-foreground">
|
|
490
538
|
{platform.description}
|
|
491
539
|
</p>
|
|
492
540
|
</div>
|
|
493
541
|
</div>
|
|
494
|
-
|
|
495
|
-
<Button
|
|
496
|
-
asChild
|
|
497
|
-
variant="ghost"
|
|
498
|
-
size="sm"
|
|
499
|
-
className="h-7 px-2 text-xs text-muted-foreground"
|
|
500
|
-
>
|
|
501
|
-
<a
|
|
502
|
-
href={platform.documentation.href}
|
|
503
|
-
target="_blank"
|
|
504
|
-
rel="noreferrer"
|
|
505
|
-
>
|
|
506
|
-
Docs
|
|
507
|
-
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
508
|
-
</a>
|
|
509
|
-
</Button>
|
|
510
|
-
{platform.documentation.externalHref ? (
|
|
511
|
-
<Button
|
|
512
|
-
asChild
|
|
513
|
-
variant="ghost"
|
|
514
|
-
size="sm"
|
|
515
|
-
className="h-7 px-2 text-xs text-muted-foreground"
|
|
516
|
-
>
|
|
517
|
-
<a
|
|
518
|
-
href={platform.documentation.externalHref}
|
|
519
|
-
target="_blank"
|
|
520
|
-
rel="noreferrer"
|
|
521
|
-
>
|
|
522
|
-
{platform.documentation.externalLabel ?? "Open"}
|
|
523
|
-
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
524
|
-
</a>
|
|
525
|
-
</Button>
|
|
526
|
-
) : null}
|
|
527
|
-
</div>
|
|
528
|
-
</div>
|
|
542
|
+
</AccordionTrigger>
|
|
529
543
|
|
|
530
|
-
|
|
531
|
-
<div className="
|
|
532
|
-
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
544
|
+
<AccordionContent className="px-4">
|
|
545
|
+
<div className="border-t">
|
|
546
|
+
<div className="flex flex-wrap items-center justify-between gap-3 py-3">
|
|
533
547
|
<div>
|
|
534
|
-
<
|
|
535
|
-
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
548
|
+
<p className="text-sm font-medium text-foreground">
|
|
549
|
+
{enabled
|
|
550
|
+
? "Messages are flowing"
|
|
551
|
+
: configured
|
|
552
|
+
? "Ready to connect"
|
|
553
|
+
: "Finish setup to connect"}
|
|
539
554
|
</p>
|
|
540
|
-
<p className="mt-
|
|
541
|
-
{
|
|
555
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
556
|
+
{credentialSummary}
|
|
542
557
|
</p>
|
|
543
558
|
</div>
|
|
544
|
-
<div className="
|
|
545
|
-
|
|
546
|
-
<Button
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
559
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
560
|
+
{platform.id === "telegram" && configured ? (
|
|
561
|
+
<Button
|
|
562
|
+
variant="outline"
|
|
563
|
+
size="sm"
|
|
564
|
+
onClick={() => runSetup(platform)}
|
|
565
|
+
disabled={setupPlatform === platform.id}
|
|
566
|
+
>
|
|
567
|
+
{setupPlatform === platform.id ? (
|
|
568
|
+
<>
|
|
569
|
+
<IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
570
|
+
Setting up...
|
|
571
|
+
</>
|
|
572
|
+
) : (
|
|
573
|
+
"Set up webhook"
|
|
574
|
+
)}
|
|
551
575
|
</Button>
|
|
552
|
-
{missingRequiredCredentials ? (
|
|
553
|
-
<Button size="sm" disabled>
|
|
554
|
-
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
555
|
-
{t("messaging.managed.addToSlack")}
|
|
556
|
-
</Button>
|
|
557
|
-
) : (
|
|
558
|
-
<Button asChild size="sm">
|
|
559
|
-
<a href={managedIntegrationOAuthUrl("slack")}>
|
|
560
|
-
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
561
|
-
{t("messaging.managed.addToSlack")}
|
|
562
|
-
</a>
|
|
563
|
-
</Button>
|
|
564
|
-
)}
|
|
565
|
-
</div>
|
|
566
|
-
{missingRequiredCredentials ? (
|
|
567
|
-
<p className="max-w-72 text-xs text-amber-700 dark:text-amber-300">
|
|
568
|
-
{t("messaging.managed.requiredCredentials")}
|
|
569
|
-
</p>
|
|
570
576
|
) : null}
|
|
577
|
+
{!configured && !enabled ? (
|
|
578
|
+
<Tooltip>
|
|
579
|
+
<TooltipTrigger asChild>
|
|
580
|
+
<span tabIndex={0}>
|
|
581
|
+
<Button size="sm" disabled>
|
|
582
|
+
Enable
|
|
583
|
+
</Button>
|
|
584
|
+
</span>
|
|
585
|
+
</TooltipTrigger>
|
|
586
|
+
<TooltipContent>
|
|
587
|
+
Save the required credentials first.
|
|
588
|
+
</TooltipContent>
|
|
589
|
+
</Tooltip>
|
|
590
|
+
) : (
|
|
591
|
+
<Button
|
|
592
|
+
size="sm"
|
|
593
|
+
onClick={() => togglePlatform(platform, enabled)}
|
|
594
|
+
disabled={togglingPlatform === platform.id}
|
|
595
|
+
>
|
|
596
|
+
{togglingPlatform === platform.id ? (
|
|
597
|
+
<>
|
|
598
|
+
<IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
599
|
+
Saving...
|
|
600
|
+
</>
|
|
601
|
+
) : enabled ? (
|
|
602
|
+
"Disable"
|
|
603
|
+
) : (
|
|
604
|
+
"Enable"
|
|
605
|
+
)}
|
|
606
|
+
</Button>
|
|
607
|
+
)}
|
|
571
608
|
</div>
|
|
572
609
|
</div>
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
installation.health === "healthy"
|
|
600
|
-
? "success"
|
|
601
|
-
: installation.health === "degraded" ||
|
|
602
|
-
installation.health === "revoked"
|
|
603
|
-
? "warning"
|
|
604
|
-
: "neutral"
|
|
605
|
-
}
|
|
606
|
-
label={t(
|
|
607
|
-
`messaging.managed.health.${installation.health}`,
|
|
608
|
-
)}
|
|
609
|
-
/>
|
|
610
|
-
<Button
|
|
611
|
-
variant="outline"
|
|
612
|
-
size="sm"
|
|
613
|
-
onClick={() =>
|
|
614
|
-
runInstallationAction(installation, "test")
|
|
615
|
-
}
|
|
616
|
-
disabled={
|
|
617
|
-
installationAction === `test:${installation.id}`
|
|
618
|
-
}
|
|
619
|
-
>
|
|
620
|
-
{t("messaging.managed.test")}
|
|
610
|
+
|
|
611
|
+
{platform.id === "slack" ? (
|
|
612
|
+
<DisclosureSection
|
|
613
|
+
title={t("messaging.managed.title")}
|
|
614
|
+
summary={
|
|
615
|
+
installations.length
|
|
616
|
+
? `${installations.length} connected`
|
|
617
|
+
: "No workspaces connected yet"
|
|
618
|
+
}
|
|
619
|
+
>
|
|
620
|
+
<div className="space-y-3">
|
|
621
|
+
<p className="text-xs text-muted-foreground">
|
|
622
|
+
{t("messaging.managed.description")}{" "}
|
|
623
|
+
{t("messaging.managed.agentManifestDescription")}
|
|
624
|
+
</p>
|
|
625
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
626
|
+
<Button asChild variant="outline" size="sm">
|
|
627
|
+
<a href={managedSlackAgentManifestUrl()}>
|
|
628
|
+
<IconFileDescription className="mr-2 h-4 w-4" />
|
|
629
|
+
{t("messaging.managed.agentManifest")}
|
|
630
|
+
</a>
|
|
631
|
+
</Button>
|
|
632
|
+
{missingRequiredCredentials ? (
|
|
633
|
+
<Button size="sm" disabled>
|
|
634
|
+
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
635
|
+
{t("messaging.managed.addToSlack")}
|
|
621
636
|
</Button>
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
"disconnect",
|
|
629
|
-
)
|
|
630
|
-
}
|
|
631
|
-
disabled={
|
|
632
|
-
installationAction ===
|
|
633
|
-
`disconnect:${installation.id}`
|
|
634
|
-
}
|
|
635
|
-
>
|
|
636
|
-
{t("messaging.managed.disconnect")}
|
|
637
|
+
) : (
|
|
638
|
+
<Button asChild size="sm">
|
|
639
|
+
<a href={managedIntegrationOAuthUrl("slack")}>
|
|
640
|
+
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
641
|
+
{t("messaging.managed.addToSlack")}
|
|
642
|
+
</a>
|
|
637
643
|
</Button>
|
|
638
|
-
|
|
644
|
+
)}
|
|
639
645
|
</div>
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
646
|
+
{missingRequiredCredentials ? (
|
|
647
|
+
<p className="text-xs text-amber-700 dark:text-amber-300">
|
|
648
|
+
{t("messaging.managed.requiredCredentials")}
|
|
649
|
+
</p>
|
|
650
|
+
) : null}
|
|
651
|
+
{installations.length ? (
|
|
652
|
+
<div className="divide-y rounded-lg bg-muted/20 px-3">
|
|
653
|
+
{installations.map((installation) => (
|
|
654
|
+
<div
|
|
655
|
+
key={installation.id}
|
|
656
|
+
className="flex flex-wrap items-center justify-between gap-3 py-3"
|
|
657
|
+
>
|
|
658
|
+
<div>
|
|
659
|
+
<div className="text-sm font-medium text-foreground">
|
|
660
|
+
{installation.teamName ||
|
|
661
|
+
installation.enterpriseName ||
|
|
662
|
+
installation.teamId ||
|
|
663
|
+
t("messaging.managed.workspaceFallback")}
|
|
664
|
+
</div>
|
|
665
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
666
|
+
{t("messaging.managed.scopesUpdated", {
|
|
667
|
+
count: installation.scopes.length,
|
|
668
|
+
date: formatDate(installation.updatedAt, {
|
|
669
|
+
dateStyle: "medium",
|
|
670
|
+
}),
|
|
671
|
+
})}
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div className="flex items-center gap-2">
|
|
675
|
+
<StatusPill
|
|
676
|
+
tone={
|
|
677
|
+
installation.health === "healthy"
|
|
678
|
+
? "success"
|
|
679
|
+
: installation.health === "degraded" ||
|
|
680
|
+
installation.health === "revoked"
|
|
681
|
+
? "warning"
|
|
682
|
+
: "neutral"
|
|
683
|
+
}
|
|
684
|
+
label={t(
|
|
685
|
+
`messaging.managed.health.${installation.health}`,
|
|
686
|
+
)}
|
|
687
|
+
/>
|
|
688
|
+
<Button
|
|
689
|
+
variant="outline"
|
|
690
|
+
size="sm"
|
|
691
|
+
onClick={() =>
|
|
692
|
+
runInstallationAction(
|
|
693
|
+
installation,
|
|
694
|
+
"test",
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
disabled={
|
|
698
|
+
installationAction ===
|
|
699
|
+
`test:${installation.id}`
|
|
700
|
+
}
|
|
701
|
+
>
|
|
702
|
+
{t("messaging.managed.test")}
|
|
703
|
+
</Button>
|
|
704
|
+
<Button
|
|
705
|
+
variant="ghost"
|
|
706
|
+
size="sm"
|
|
707
|
+
onClick={() =>
|
|
708
|
+
runInstallationAction(
|
|
709
|
+
installation,
|
|
710
|
+
"disconnect",
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
disabled={
|
|
714
|
+
installationAction ===
|
|
715
|
+
`disconnect:${installation.id}`
|
|
716
|
+
}
|
|
717
|
+
>
|
|
718
|
+
{t("messaging.managed.disconnect")}
|
|
719
|
+
</Button>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
))}
|
|
689
723
|
</div>
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
724
|
+
) : (
|
|
725
|
+
<p className="text-xs text-muted-foreground">
|
|
726
|
+
{t("messaging.managed.empty")}
|
|
727
|
+
</p>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
730
|
+
</DisclosureSection>
|
|
731
|
+
) : null}
|
|
732
|
+
|
|
733
|
+
{platform.id === "slack" && scopes.length ? (
|
|
734
|
+
<DisclosureSection
|
|
735
|
+
title={t("messaging.managed.channelAccessTitle")}
|
|
736
|
+
summary={`${scopes.length} channel${scopes.length === 1 ? "" : "s"}`}
|
|
737
|
+
>
|
|
738
|
+
<div className="space-y-3">
|
|
739
|
+
<p className="text-xs text-muted-foreground">
|
|
740
|
+
{t("messaging.managed.channelAccessDescription")}
|
|
741
|
+
</p>
|
|
742
|
+
{scopes.map((scope) => {
|
|
743
|
+
const subjectId = JSON.stringify([
|
|
744
|
+
scope.platform,
|
|
745
|
+
scope.tenantId,
|
|
746
|
+
scope.conversationId,
|
|
747
|
+
]);
|
|
748
|
+
const budget = budgets.find(
|
|
749
|
+
(item) =>
|
|
750
|
+
item.subjectType === "scope" &&
|
|
751
|
+
item.subjectId === subjectId &&
|
|
752
|
+
item.period === "month",
|
|
753
|
+
);
|
|
754
|
+
return (
|
|
755
|
+
<Collapsible
|
|
756
|
+
key={scope.id}
|
|
757
|
+
className="border-b last:border-b-0"
|
|
712
758
|
>
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
759
|
+
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center justify-between gap-3 py-3 text-left">
|
|
760
|
+
<span className="min-w-0">
|
|
761
|
+
<span className="block truncate font-mono text-xs text-foreground">
|
|
762
|
+
{scope.conversationId}
|
|
763
|
+
</span>
|
|
764
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
765
|
+
{t("messaging.managed.isolatedIdentity", {
|
|
766
|
+
trust: t(
|
|
767
|
+
`messaging.managed.trust.${scope.trust}`,
|
|
768
|
+
),
|
|
769
|
+
})}
|
|
770
|
+
</span>
|
|
771
|
+
</span>
|
|
772
|
+
<span className="flex shrink-0 items-center gap-2">
|
|
773
|
+
<StatusPill
|
|
774
|
+
tone={
|
|
775
|
+
scope.trust === "trusted"
|
|
776
|
+
? "success"
|
|
777
|
+
: "warning"
|
|
778
|
+
}
|
|
779
|
+
label={t(
|
|
780
|
+
`messaging.managed.trust.${scope.trust}`,
|
|
781
|
+
)}
|
|
782
|
+
/>
|
|
783
|
+
<IconChevronRight className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
784
|
+
</span>
|
|
785
|
+
</CollapsibleTrigger>
|
|
786
|
+
<CollapsibleContent className="pb-3">
|
|
787
|
+
<div className="grid gap-2 sm:grid-cols-3">
|
|
788
|
+
{[
|
|
789
|
+
[
|
|
790
|
+
"requireMention",
|
|
791
|
+
t("messaging.managed.requireMention"),
|
|
792
|
+
],
|
|
793
|
+
[
|
|
794
|
+
"allowGuests",
|
|
795
|
+
t("messaging.managed.allowGuests"),
|
|
796
|
+
],
|
|
797
|
+
[
|
|
798
|
+
"allowExternalShared",
|
|
799
|
+
t("messaging.managed.allowSlackConnect"),
|
|
800
|
+
],
|
|
801
|
+
].map(([key, label]) => (
|
|
802
|
+
<label
|
|
803
|
+
key={key}
|
|
804
|
+
className="flex items-center justify-between gap-2 rounded-md border px-2.5 py-2 text-xs"
|
|
805
|
+
>
|
|
806
|
+
{label}
|
|
807
|
+
<Switch
|
|
808
|
+
checked={
|
|
809
|
+
scope.policy[
|
|
810
|
+
key as keyof ClientIntegrationScope["policy"]
|
|
811
|
+
]
|
|
812
|
+
}
|
|
813
|
+
disabled={savingScope === scope.id}
|
|
814
|
+
onCheckedChange={(checked) =>
|
|
815
|
+
updateScopePolicy(scope, {
|
|
816
|
+
[key]: checked,
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
/>
|
|
820
|
+
</label>
|
|
821
|
+
))}
|
|
822
|
+
</div>
|
|
823
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
824
|
+
<div className="min-w-40 flex-1 space-y-1">
|
|
825
|
+
<label className="text-xs font-medium text-foreground">
|
|
826
|
+
{t("messaging.managed.monthlyBudgetUsd")}
|
|
827
|
+
</label>
|
|
828
|
+
<Input
|
|
829
|
+
inputMode="decimal"
|
|
830
|
+
value={
|
|
831
|
+
scopeBudget[scope.id] ??
|
|
832
|
+
(budget
|
|
833
|
+
? String(
|
|
834
|
+
budget.limitMicros / 1_000_000,
|
|
835
|
+
)
|
|
836
|
+
: "")
|
|
837
|
+
}
|
|
838
|
+
onChange={(event) =>
|
|
839
|
+
setScopeBudget((current) => ({
|
|
840
|
+
...current,
|
|
841
|
+
[scope.id]: event.target.value,
|
|
842
|
+
}))
|
|
843
|
+
}
|
|
844
|
+
placeholder="25"
|
|
845
|
+
/>
|
|
846
|
+
</div>
|
|
847
|
+
<Button
|
|
848
|
+
variant="outline"
|
|
849
|
+
onClick={() => saveScopeBudget(scope)}
|
|
850
|
+
disabled={
|
|
851
|
+
savingScope === `budget:${scope.id}`
|
|
852
|
+
}
|
|
853
|
+
>
|
|
854
|
+
{t("messaging.managed.saveBudget")}
|
|
855
|
+
</Button>
|
|
856
|
+
</div>
|
|
857
|
+
</CollapsibleContent>
|
|
858
|
+
</Collapsible>
|
|
859
|
+
);
|
|
860
|
+
})}
|
|
758
861
|
</div>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
</div>
|
|
762
|
-
) : null}
|
|
862
|
+
</DisclosureSection>
|
|
863
|
+
) : null}
|
|
763
864
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
<CollapsibleContent>
|
|
770
|
-
<div className="mt-2 rounded-xl border bg-muted/20 p-4">
|
|
771
|
-
<ol className="space-y-2 text-sm text-muted-foreground">
|
|
865
|
+
<DisclosureSection
|
|
866
|
+
title="Setup steps"
|
|
867
|
+
summary={`${platform.setup.steps.length} step${platform.setup.steps.length === 1 ? "" : "s"}`}
|
|
868
|
+
>
|
|
869
|
+
<ol className="space-y-2 rounded-lg bg-muted/20 px-3 py-3 text-sm text-muted-foreground">
|
|
772
870
|
{platform.setup.steps.map((step, index) => (
|
|
773
871
|
<li key={step} className="flex gap-2">
|
|
774
872
|
<span className="text-muted-foreground/60">
|
|
@@ -778,252 +876,243 @@ export function MessagingSetupPanel() {
|
|
|
778
876
|
</li>
|
|
779
877
|
))}
|
|
780
878
|
</ol>
|
|
781
|
-
</
|
|
782
|
-
</CollapsibleContent>
|
|
783
|
-
</Collapsible>
|
|
879
|
+
</DisclosureSection>
|
|
784
880
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
<label className="text-xs font-medium text-foreground">
|
|
811
|
-
{label}
|
|
812
|
-
{!envKey.required ? (
|
|
813
|
-
<span className="ml-1 text-muted-foreground">
|
|
814
|
-
(optional)
|
|
815
|
-
</span>
|
|
816
|
-
) : null}
|
|
817
|
-
</label>
|
|
818
|
-
{helpText ? (
|
|
819
|
-
<HelpTooltip content={helpText} />
|
|
820
|
-
) : null}
|
|
821
|
-
</div>
|
|
822
|
-
{isConfigured ? (
|
|
823
|
-
<StatusPill tone="success" label="Saved" />
|
|
824
|
-
) : (
|
|
825
|
-
<StatusPill
|
|
826
|
-
tone="neutral"
|
|
827
|
-
label={envKey.required ? "Missing" : "Not set"}
|
|
828
|
-
/>
|
|
829
|
-
)}
|
|
830
|
-
</div>
|
|
831
|
-
{isConfigured && isPublicValue ? (
|
|
832
|
-
<PublicValueReveal envKey={envKey.key} />
|
|
833
|
-
) : !isConfigured ? (
|
|
834
|
-
<Input
|
|
835
|
-
type={isPublicValue ? "text" : "password"}
|
|
836
|
-
value={envValues[envKey.key] || ""}
|
|
837
|
-
onChange={(event) =>
|
|
838
|
-
setEnvValues((current) => ({
|
|
839
|
-
...current,
|
|
840
|
-
[envKey.key]: event.target.value,
|
|
841
|
-
}))
|
|
842
|
-
}
|
|
843
|
-
placeholder={
|
|
844
|
-
isPublicValue
|
|
845
|
-
? "agent@yourcompany.com"
|
|
846
|
-
: `Enter ${label}`
|
|
847
|
-
}
|
|
848
|
-
autoComplete="off"
|
|
849
|
-
/>
|
|
850
|
-
) : null}
|
|
851
|
-
</div>
|
|
852
|
-
);
|
|
853
|
-
})}
|
|
854
|
-
</div>
|
|
855
|
-
{legacyEnvKeys.length ? (
|
|
856
|
-
<Collapsible>
|
|
857
|
-
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
858
|
-
<IconChevronRight className="h-3.5 w-3.5 transition-transform group-data-[state=open]:rotate-90" />
|
|
859
|
-
<span>{legacyEnvKeys[0]?.label}</span>
|
|
860
|
-
</CollapsibleTrigger>
|
|
861
|
-
<CollapsibleContent>
|
|
862
|
-
<div className="mt-2 space-y-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4">
|
|
863
|
-
<p className="text-xs text-muted-foreground">
|
|
864
|
-
{legacyEnvKeys[0]?.helpText}
|
|
865
|
-
</p>
|
|
866
|
-
{legacyEnvKeys.map((envKey) => {
|
|
867
|
-
const envStatus = envStatusByKey.get(envKey.key);
|
|
868
|
-
const isConfigured = !!envStatus?.configured;
|
|
869
|
-
const helpText =
|
|
870
|
-
envKey.helpText ?? envStatus?.helpText;
|
|
871
|
-
const label =
|
|
872
|
-
envKey.label || envStatus?.label || envKey.key;
|
|
873
|
-
return (
|
|
874
|
-
<div key={envKey.key} className="space-y-1.5">
|
|
875
|
-
<div className="flex items-center justify-between gap-3">
|
|
876
|
-
<div className="flex items-center gap-1.5">
|
|
877
|
-
<label className="text-xs font-medium text-foreground">
|
|
878
|
-
{label}
|
|
879
|
-
</label>
|
|
880
|
-
{helpText ? (
|
|
881
|
-
<HelpTooltip content={helpText} />
|
|
881
|
+
<DisclosureSection
|
|
882
|
+
title="Credentials"
|
|
883
|
+
summary={credentialSummary}
|
|
884
|
+
>
|
|
885
|
+
<div className="space-y-3">
|
|
886
|
+
{primaryEnvKeys.map((envKey) => {
|
|
887
|
+
const envStatus = envStatusByKey.get(envKey.key);
|
|
888
|
+
const isConfigured = !!envStatus?.configured;
|
|
889
|
+
const helpText = envKey.helpText ?? envStatus?.helpText;
|
|
890
|
+
const label =
|
|
891
|
+
envKey.label || envStatus?.label || envKey.key;
|
|
892
|
+
// Email agent address is not a secret — show it plainly
|
|
893
|
+
// so users can copy and share it.
|
|
894
|
+
const isPublicValue =
|
|
895
|
+
envKey.key === "EMAIL_AGENT_ADDRESS";
|
|
896
|
+
return (
|
|
897
|
+
<div key={envKey.key} className="space-y-1.5">
|
|
898
|
+
<div className="flex items-center justify-between gap-3">
|
|
899
|
+
<div className="flex items-center gap-1.5">
|
|
900
|
+
<label className="text-xs font-medium text-foreground">
|
|
901
|
+
{label}
|
|
902
|
+
{!envKey.required ? (
|
|
903
|
+
<span className="ml-1 text-muted-foreground">
|
|
904
|
+
(optional)
|
|
905
|
+
</span>
|
|
882
906
|
) : null}
|
|
883
|
-
</
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
/>
|
|
907
|
+
</label>
|
|
908
|
+
{helpText ? (
|
|
909
|
+
<HelpTooltip content={helpText} />
|
|
910
|
+
) : null}
|
|
888
911
|
</div>
|
|
889
|
-
{
|
|
890
|
-
<
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
[envKey.key]: event.target.value,
|
|
897
|
-
}))
|
|
912
|
+
{isConfigured ? (
|
|
913
|
+
<StatusPill tone="success" label="Saved" />
|
|
914
|
+
) : (
|
|
915
|
+
<StatusPill
|
|
916
|
+
tone="neutral"
|
|
917
|
+
label={
|
|
918
|
+
envKey.required ? "Missing" : "Not set"
|
|
898
919
|
}
|
|
899
|
-
placeholder={`Enter ${label}`}
|
|
900
|
-
autoComplete="off"
|
|
901
920
|
/>
|
|
902
|
-
)
|
|
921
|
+
)}
|
|
903
922
|
</div>
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
923
|
+
{isConfigured && isPublicValue ? (
|
|
924
|
+
<PublicValueReveal envKey={envKey.key} />
|
|
925
|
+
) : !isConfigured ? (
|
|
926
|
+
<Input
|
|
927
|
+
type={isPublicValue ? "text" : "password"}
|
|
928
|
+
value={envValues[envKey.key] || ""}
|
|
929
|
+
onChange={(event) =>
|
|
930
|
+
setEnvValues((current) => ({
|
|
931
|
+
...current,
|
|
932
|
+
[envKey.key]: event.target.value,
|
|
933
|
+
}))
|
|
934
|
+
}
|
|
935
|
+
placeholder={
|
|
936
|
+
isPublicValue
|
|
937
|
+
? "agent@yourcompany.com"
|
|
938
|
+
: `Enter ${label}`
|
|
939
|
+
}
|
|
940
|
+
autoComplete="off"
|
|
941
|
+
/>
|
|
942
|
+
) : null}
|
|
943
|
+
</div>
|
|
944
|
+
);
|
|
945
|
+
})}
|
|
946
|
+
</div>
|
|
947
|
+
{legacyEnvKeys.length ? (
|
|
948
|
+
<Collapsible>
|
|
949
|
+
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
950
|
+
<IconChevronRight className="h-3.5 w-3.5 transition-transform group-data-[state=open]:rotate-90" />
|
|
951
|
+
<span>{legacyEnvKeys[0]?.label}</span>
|
|
952
|
+
</CollapsibleTrigger>
|
|
953
|
+
<CollapsibleContent>
|
|
954
|
+
<div className="mt-2 space-y-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4">
|
|
955
|
+
<p className="text-xs text-muted-foreground">
|
|
956
|
+
{legacyEnvKeys[0]?.helpText}
|
|
957
|
+
</p>
|
|
958
|
+
{legacyEnvKeys.map((envKey) => {
|
|
959
|
+
const envStatus = envStatusByKey.get(envKey.key);
|
|
960
|
+
const isConfigured = !!envStatus?.configured;
|
|
961
|
+
const helpText =
|
|
962
|
+
envKey.helpText ?? envStatus?.helpText;
|
|
963
|
+
const label =
|
|
964
|
+
envKey.label || envStatus?.label || envKey.key;
|
|
965
|
+
return (
|
|
966
|
+
<div key={envKey.key} className="space-y-1.5">
|
|
967
|
+
<div className="flex items-center justify-between gap-3">
|
|
968
|
+
<div className="flex items-center gap-1.5">
|
|
969
|
+
<label className="text-xs font-medium text-foreground">
|
|
970
|
+
{label}
|
|
971
|
+
</label>
|
|
972
|
+
{helpText ? (
|
|
973
|
+
<HelpTooltip content={helpText} />
|
|
974
|
+
) : null}
|
|
975
|
+
</div>
|
|
976
|
+
<StatusPill
|
|
977
|
+
tone={
|
|
978
|
+
isConfigured ? "success" : "neutral"
|
|
979
|
+
}
|
|
980
|
+
label={isConfigured ? "Saved" : "Not set"}
|
|
981
|
+
/>
|
|
982
|
+
</div>
|
|
983
|
+
{!isConfigured ? (
|
|
984
|
+
<Input
|
|
985
|
+
type="password"
|
|
986
|
+
value={envValues[envKey.key] || ""}
|
|
987
|
+
onChange={(event) =>
|
|
988
|
+
setEnvValues((current) => ({
|
|
989
|
+
...current,
|
|
990
|
+
[envKey.key]: event.target.value,
|
|
991
|
+
}))
|
|
992
|
+
}
|
|
993
|
+
placeholder={`Enter ${label}`}
|
|
994
|
+
autoComplete="off"
|
|
995
|
+
/>
|
|
996
|
+
) : null}
|
|
997
|
+
</div>
|
|
998
|
+
);
|
|
999
|
+
})}
|
|
1000
|
+
{legacyEnvKeys.some(
|
|
1001
|
+
(envKey) =>
|
|
1002
|
+
!envStatusByKey.get(envKey.key)?.configured,
|
|
1003
|
+
) ? (
|
|
1004
|
+
<Button
|
|
1005
|
+
variant="outline"
|
|
1006
|
+
onClick={() =>
|
|
1007
|
+
saveEnvKeys(
|
|
1008
|
+
platform,
|
|
1009
|
+
legacyEnvKeys.map((envKey) => envKey.key),
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
disabled={savingKeysFor === platform.id}
|
|
1013
|
+
>
|
|
1014
|
+
{savingKeysFor === platform.id
|
|
1015
|
+
? "Saving..."
|
|
1016
|
+
: "Save credentials"}
|
|
1017
|
+
</Button>
|
|
1018
|
+
) : null}
|
|
1019
|
+
</div>
|
|
1020
|
+
</CollapsibleContent>
|
|
1021
|
+
</Collapsible>
|
|
1022
|
+
) : null}
|
|
1023
|
+
{missingRequiredCredentials ? (
|
|
1024
|
+
<Button
|
|
1025
|
+
variant="outline"
|
|
1026
|
+
onClick={() =>
|
|
1027
|
+
saveEnvKeys(
|
|
1028
|
+
platform,
|
|
1029
|
+
envKeys.map((k) => k.key),
|
|
1030
|
+
)
|
|
1031
|
+
}
|
|
1032
|
+
disabled={savingKeysFor === platform.id}
|
|
1033
|
+
>
|
|
1034
|
+
{savingKeysFor === platform.id ? (
|
|
1035
|
+
<>
|
|
1036
|
+
<IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1037
|
+
Saving...
|
|
1038
|
+
</>
|
|
1039
|
+
) : (
|
|
1040
|
+
"Save credentials"
|
|
1041
|
+
)}
|
|
1042
|
+
</Button>
|
|
1043
|
+
) : null}
|
|
1044
|
+
</DisclosureSection>
|
|
951
1045
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
</div>
|
|
957
|
-
<div className="flex items-center gap-2">
|
|
958
|
-
<code className="flex-1 truncate rounded-md border bg-muted/30 px-3 py-2 text-xs text-foreground">
|
|
959
|
-
{status.webhookUrl}
|
|
960
|
-
</code>
|
|
961
|
-
<Button
|
|
962
|
-
variant="outline"
|
|
963
|
-
size="icon"
|
|
964
|
-
onClick={() => copyWebhook(status.webhookUrl!)}
|
|
965
|
-
aria-label={`Copy ${platform.name} webhook URL`}
|
|
1046
|
+
{status?.webhookUrl ? (
|
|
1047
|
+
<DisclosureSection
|
|
1048
|
+
title="Webhook URL"
|
|
1049
|
+
summary="Copy the endpoint for this channel"
|
|
966
1050
|
>
|
|
967
|
-
|
|
968
|
-
<
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1051
|
+
<div className="flex items-center gap-2">
|
|
1052
|
+
<code className="flex-1 truncate rounded-md bg-muted/30 px-3 py-2 text-xs text-foreground">
|
|
1053
|
+
{status.webhookUrl}
|
|
1054
|
+
</code>
|
|
1055
|
+
<Button
|
|
1056
|
+
variant="outline"
|
|
1057
|
+
size="icon"
|
|
1058
|
+
onClick={() => copyWebhook(status.webhookUrl!)}
|
|
1059
|
+
aria-label={`Copy ${platform.name} webhook URL`}
|
|
1060
|
+
>
|
|
1061
|
+
{copiedWebhook === status.webhookUrl ? (
|
|
1062
|
+
<IconCheck className="h-4 w-4" />
|
|
1063
|
+
) : (
|
|
1064
|
+
<IconCopy className="h-4 w-4" />
|
|
1065
|
+
)}
|
|
1066
|
+
</Button>
|
|
1067
|
+
</div>
|
|
1068
|
+
</DisclosureSection>
|
|
1069
|
+
) : null}
|
|
976
1070
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
variant="outline"
|
|
981
|
-
onClick={() => runSetup(platform)}
|
|
982
|
-
disabled={setupPlatform === platform.id}
|
|
1071
|
+
<DisclosureSection
|
|
1072
|
+
title="Resources"
|
|
1073
|
+
summary="Documentation and external links"
|
|
983
1074
|
>
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
</
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
</div>
|
|
1023
|
-
</section>
|
|
1075
|
+
<div className="flex flex-wrap gap-2">
|
|
1076
|
+
<Button
|
|
1077
|
+
asChild
|
|
1078
|
+
variant="ghost"
|
|
1079
|
+
size="sm"
|
|
1080
|
+
className="px-0 text-xs text-muted-foreground hover:bg-transparent hover:text-foreground"
|
|
1081
|
+
>
|
|
1082
|
+
<a
|
|
1083
|
+
href={platform.documentation.href}
|
|
1084
|
+
target="_blank"
|
|
1085
|
+
rel="noreferrer"
|
|
1086
|
+
>
|
|
1087
|
+
Docs
|
|
1088
|
+
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
1089
|
+
</a>
|
|
1090
|
+
</Button>
|
|
1091
|
+
{platform.documentation.externalHref ? (
|
|
1092
|
+
<Button
|
|
1093
|
+
asChild
|
|
1094
|
+
variant="ghost"
|
|
1095
|
+
size="sm"
|
|
1096
|
+
className="px-0 text-xs text-muted-foreground hover:bg-transparent hover:text-foreground"
|
|
1097
|
+
>
|
|
1098
|
+
<a
|
|
1099
|
+
href={platform.documentation.externalHref}
|
|
1100
|
+
target="_blank"
|
|
1101
|
+
rel="noreferrer"
|
|
1102
|
+
>
|
|
1103
|
+
{platform.documentation.externalLabel ?? "Open"}
|
|
1104
|
+
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
1105
|
+
</a>
|
|
1106
|
+
</Button>
|
|
1107
|
+
) : null}
|
|
1108
|
+
</div>
|
|
1109
|
+
</DisclosureSection>
|
|
1110
|
+
</div>
|
|
1111
|
+
</AccordionContent>
|
|
1112
|
+
</AccordionItem>
|
|
1024
1113
|
);
|
|
1025
1114
|
})}
|
|
1026
|
-
</
|
|
1115
|
+
</Accordion>
|
|
1027
1116
|
|
|
1028
1117
|
{loading ? (
|
|
1029
1118
|
<div className="rounded-2xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
|