@meetreeve/ui 0.8.1 → 0.10.0
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/console/index.d.mts +65 -0
- package/dist/console/index.d.ts +65 -0
- package/dist/console/index.js +3528 -0
- package/dist/console/index.mjs +3490 -0
- package/dist/index.d.mts +170 -3
- package/dist/index.d.ts +170 -3
- package/dist/index.js +832 -83
- package/dist/index.mjs +832 -83
- package/package.json +10 -1
package/dist/index.mjs
CHANGED
|
@@ -1368,7 +1368,21 @@ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-run
|
|
|
1368
1368
|
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1369
1369
|
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1370
1370
|
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1371
|
+
var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
|
|
1372
|
+
var DEFAULT_GRANT_ERROR = "Couldn't share. Please try again.";
|
|
1373
|
+
var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
|
|
1374
|
+
var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
|
|
1375
|
+
var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
|
|
1376
|
+
var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
|
|
1377
|
+
var UNVERIFIABLE_CREDENTIAL_NOTE = "We can't verify this automatically \u2014 saving it is safe, but nothing has checked it yet.";
|
|
1371
1378
|
var SKELETON_COUNT = 3;
|
|
1379
|
+
var UNGROUNDED_PLACEHOLDER = /\{\{[^}]*\}\}/;
|
|
1380
|
+
function hasUngroundedPlaceholder(text) {
|
|
1381
|
+
return UNGROUNDED_PLACEHOLDER.test(text);
|
|
1382
|
+
}
|
|
1383
|
+
function isDevEnvironment() {
|
|
1384
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
1385
|
+
}
|
|
1372
1386
|
function capabilityLabel(capability) {
|
|
1373
1387
|
if (capability === "read") return "Read only";
|
|
1374
1388
|
if (capability === "write") return "Write only";
|
|
@@ -1382,6 +1396,17 @@ function formatSince(since) {
|
|
|
1382
1396
|
function errorMessageFrom(err, fallback) {
|
|
1383
1397
|
return err instanceof Error && err.message ? err.message : fallback;
|
|
1384
1398
|
}
|
|
1399
|
+
function ownerLabel(entry) {
|
|
1400
|
+
const connection = entry.connection;
|
|
1401
|
+
if (connection.status !== "connected") return null;
|
|
1402
|
+
if (connection.kind === "org") return "Org";
|
|
1403
|
+
if (connection.owned_by_viewer) return "Yours";
|
|
1404
|
+
if (connection.shared_by) return `Shared by ${connection.shared_by.name}`;
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
function isSharedWithViewer(entry) {
|
|
1408
|
+
return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
|
|
1409
|
+
}
|
|
1385
1410
|
function ConnectorPicker({
|
|
1386
1411
|
client,
|
|
1387
1412
|
purpose,
|
|
@@ -1393,6 +1418,9 @@ function ConnectorPicker({
|
|
|
1393
1418
|
const [entries, setEntries] = useState7(null);
|
|
1394
1419
|
const [loadError, setLoadError] = useState7(null);
|
|
1395
1420
|
const [uiState, setUiState] = useState7({});
|
|
1421
|
+
const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
|
|
1422
|
+
const credentialsSupported = typeof client.submitCredentials === "function";
|
|
1423
|
+
const credentialTestSupported = typeof client.testCredentials === "function";
|
|
1396
1424
|
const clientRef = useRef5(client);
|
|
1397
1425
|
clientRef.current = client;
|
|
1398
1426
|
const purposeRef = useRef5(purpose);
|
|
@@ -1451,10 +1479,10 @@ function ConnectorPicker({
|
|
|
1451
1479
|
};
|
|
1452
1480
|
}, [gated, refreshAfterReturn]);
|
|
1453
1481
|
const handleConnect = useCallback4(
|
|
1454
|
-
async (entry) => {
|
|
1482
|
+
async (entry, kind) => {
|
|
1455
1483
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1456
1484
|
try {
|
|
1457
|
-
const result = await clientRef.current.connect(entry);
|
|
1485
|
+
const result = await clientRef.current.connect(entry, { kind });
|
|
1458
1486
|
if (result?.connect_url) {
|
|
1459
1487
|
window.open(result.connect_url, "_blank", "noopener,noreferrer");
|
|
1460
1488
|
return;
|
|
@@ -1476,6 +1504,38 @@ function ConnectorPicker({
|
|
|
1476
1504
|
},
|
|
1477
1505
|
[load]
|
|
1478
1506
|
);
|
|
1507
|
+
const handleSubmitCredentials = useCallback4(
|
|
1508
|
+
async (entry, values, kind) => {
|
|
1509
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1510
|
+
try {
|
|
1511
|
+
await clientRef.current.submitCredentials?.(entry, values, { kind });
|
|
1512
|
+
const list = await load();
|
|
1513
|
+
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1514
|
+
setUiState((prev) => {
|
|
1515
|
+
const next = { ...prev };
|
|
1516
|
+
delete next[entry.slug];
|
|
1517
|
+
return next;
|
|
1518
|
+
});
|
|
1519
|
+
if (updated?.connection.status === "connected") {
|
|
1520
|
+
onConnectedRef.current?.(updated);
|
|
1521
|
+
} else {
|
|
1522
|
+
throw new Error(DEFAULT_CREDENTIAL_ERROR);
|
|
1523
|
+
}
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
setUiState((prev) => {
|
|
1526
|
+
const next = { ...prev };
|
|
1527
|
+
delete next[entry.slug];
|
|
1528
|
+
return next;
|
|
1529
|
+
});
|
|
1530
|
+
throw err;
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
[load]
|
|
1534
|
+
);
|
|
1535
|
+
const testCredentials = useCallback4(
|
|
1536
|
+
(entry, values) => clientRef.current.testCredentials?.(entry, values) ?? Promise.resolve({ valid: null }),
|
|
1537
|
+
[]
|
|
1538
|
+
);
|
|
1479
1539
|
const handleDisconnect = useCallback4(
|
|
1480
1540
|
async (entry) => {
|
|
1481
1541
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
|
|
@@ -1494,121 +1554,810 @@ function ConnectorPicker({
|
|
|
1494
1554
|
},
|
|
1495
1555
|
[load]
|
|
1496
1556
|
);
|
|
1557
|
+
const listGrants = useCallback4(
|
|
1558
|
+
(entry) => clientRef.current.listGrants?.(entry) ?? Promise.resolve([]),
|
|
1559
|
+
[]
|
|
1560
|
+
);
|
|
1561
|
+
const addGrant = useCallback4(
|
|
1562
|
+
(entry, input) => clientRef.current.grant?.(entry, input) ?? Promise.resolve(),
|
|
1563
|
+
[]
|
|
1564
|
+
);
|
|
1565
|
+
const revokeGrant = useCallback4(
|
|
1566
|
+
(entry, grantId) => clientRef.current.revokeGrant?.(entry, grantId) ?? Promise.resolve(),
|
|
1567
|
+
[]
|
|
1568
|
+
);
|
|
1497
1569
|
if (gated) return null;
|
|
1498
1570
|
const containerClassName = cn(
|
|
1499
1571
|
"flex flex-col",
|
|
1500
1572
|
layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
|
|
1501
1573
|
);
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
"
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
"
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
{
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1574
|
+
const ownEntries = entries?.filter((entry) => !isSharedWithViewer(entry)) ?? null;
|
|
1575
|
+
const sharedEntries = entries?.filter(isSharedWithViewer) ?? [];
|
|
1576
|
+
return /* @__PURE__ */ jsxs6("div", { "data-testid": "connector-picker", "data-layout": layout, className: "flex flex-col gap-4", children: [
|
|
1577
|
+
/* @__PURE__ */ jsxs6("div", { className: containerClassName, children: [
|
|
1578
|
+
entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ jsxs6(
|
|
1579
|
+
"div",
|
|
1580
|
+
{
|
|
1581
|
+
"data-testid": "connector-picker-skeleton-item",
|
|
1582
|
+
"aria-hidden": "true",
|
|
1583
|
+
className: "flex items-center gap-3 rounded-lg border border-border p-3",
|
|
1584
|
+
children: [
|
|
1585
|
+
/* @__PURE__ */ jsx7("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
|
|
1586
|
+
/* @__PURE__ */ jsx7("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
|
|
1587
|
+
]
|
|
1588
|
+
},
|
|
1589
|
+
i
|
|
1590
|
+
)),
|
|
1591
|
+
loadError && /* @__PURE__ */ jsxs6(
|
|
1592
|
+
"div",
|
|
1593
|
+
{
|
|
1594
|
+
role: "alert",
|
|
1595
|
+
"data-testid": "connector-picker-load-error",
|
|
1596
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
|
|
1597
|
+
children: [
|
|
1598
|
+
/* @__PURE__ */ jsx7("span", { children: loadError }),
|
|
1599
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
|
|
1600
|
+
]
|
|
1601
|
+
}
|
|
1602
|
+
),
|
|
1603
|
+
entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ jsx7("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
|
|
1604
|
+
ownEntries?.map((entry) => /* @__PURE__ */ jsx7(
|
|
1605
|
+
ConnectorTile,
|
|
1606
|
+
{
|
|
1607
|
+
entry,
|
|
1608
|
+
ui: uiState[entry.slug],
|
|
1609
|
+
layout,
|
|
1610
|
+
grantsSupported,
|
|
1611
|
+
credentialsSupported,
|
|
1612
|
+
credentialTestSupported,
|
|
1613
|
+
onConnect: handleConnect,
|
|
1614
|
+
onDisconnect: handleDisconnect,
|
|
1615
|
+
onSubmitCredentials: handleSubmitCredentials,
|
|
1616
|
+
onTestCredentials: testCredentials,
|
|
1617
|
+
listGrants,
|
|
1618
|
+
addGrant,
|
|
1619
|
+
revokeGrant
|
|
1620
|
+
},
|
|
1621
|
+
entry.slug
|
|
1622
|
+
))
|
|
1623
|
+
] }),
|
|
1624
|
+
sharedEntries.length > 0 && /* @__PURE__ */ jsxs6("section", { "data-testid": "connector-picker-shared-with-me", className: "flex flex-col gap-2", children: [
|
|
1625
|
+
/* @__PURE__ */ jsx7("h3", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Shared with me" }),
|
|
1626
|
+
/* @__PURE__ */ jsx7("div", { className: containerClassName, children: sharedEntries.map((entry) => /* @__PURE__ */ jsx7(
|
|
1627
|
+
ConnectorTile,
|
|
1628
|
+
{
|
|
1629
|
+
entry,
|
|
1630
|
+
ui: uiState[entry.slug],
|
|
1631
|
+
layout,
|
|
1632
|
+
shared: true,
|
|
1633
|
+
grantsSupported,
|
|
1634
|
+
credentialsSupported,
|
|
1635
|
+
credentialTestSupported,
|
|
1636
|
+
onConnect: handleConnect,
|
|
1637
|
+
onDisconnect: handleDisconnect,
|
|
1638
|
+
onSubmitCredentials: handleSubmitCredentials,
|
|
1639
|
+
onTestCredentials: testCredentials,
|
|
1640
|
+
listGrants,
|
|
1641
|
+
addGrant,
|
|
1642
|
+
revokeGrant
|
|
1643
|
+
},
|
|
1644
|
+
entry.slug
|
|
1645
|
+
)) })
|
|
1646
|
+
] })
|
|
1540
1647
|
] });
|
|
1541
1648
|
}
|
|
1542
1649
|
function ConnectorTile({
|
|
1543
1650
|
entry,
|
|
1544
1651
|
ui,
|
|
1545
1652
|
layout,
|
|
1653
|
+
shared = false,
|
|
1654
|
+
grantsSupported,
|
|
1655
|
+
credentialsSupported,
|
|
1656
|
+
credentialTestSupported,
|
|
1546
1657
|
onConnect,
|
|
1547
|
-
onDisconnect
|
|
1658
|
+
onDisconnect,
|
|
1659
|
+
onSubmitCredentials,
|
|
1660
|
+
onTestCredentials,
|
|
1661
|
+
listGrants,
|
|
1662
|
+
addGrant,
|
|
1663
|
+
revokeGrant
|
|
1548
1664
|
}) {
|
|
1549
1665
|
const isConnected = entry.connection.status === "connected";
|
|
1550
1666
|
const isConnecting = ui?.kind === "connecting";
|
|
1551
1667
|
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1552
1668
|
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1669
|
+
const [kind, setKind] = useState7("personal");
|
|
1670
|
+
const [sharingOpen, setSharingOpen] = useState7(false);
|
|
1671
|
+
const [setupOpen, setSetupOpen] = useState7(false);
|
|
1672
|
+
const credentialFields = entry.credential_fields ?? [];
|
|
1673
|
+
const pasteSupported = credentialsSupported && credentialFields.length > 0;
|
|
1674
|
+
const guide = entry.guide ?? null;
|
|
1675
|
+
const canSetUp = !isConnected && (pasteSupported || guide !== null);
|
|
1676
|
+
const orgPermitted = entry.org_connect?.permitted === true;
|
|
1677
|
+
const orgDisabledReason = entry.org_connect?.reason || DEFAULT_ORG_DISABLED_REASON;
|
|
1678
|
+
const attribution = ownerLabel(entry);
|
|
1679
|
+
const canDisconnect = isConnected && !shared;
|
|
1680
|
+
const canManageSharing = isConnected && entry.connection.owned_by_viewer === true && grantsSupported;
|
|
1553
1681
|
return /* @__PURE__ */ jsxs6(
|
|
1554
1682
|
"div",
|
|
1555
1683
|
{
|
|
1556
1684
|
"data-testid": `connector-tile-${entry.slug}`,
|
|
1557
1685
|
"data-connected": isConnected || void 0,
|
|
1558
1686
|
"data-deprecated": entry.deprecated || void 0,
|
|
1559
|
-
className: cn("flex
|
|
1687
|
+
className: cn("flex flex-col p-3", layout === "tiles" && "rounded-lg border border-border"),
|
|
1560
1688
|
children: [
|
|
1561
|
-
/* @__PURE__ */
|
|
1562
|
-
|
|
1563
|
-
/* @__PURE__ */ jsxs6("div", { className: "
|
|
1564
|
-
/* @__PURE__ */
|
|
1565
|
-
|
|
1566
|
-
|
|
1689
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-3", children: [
|
|
1690
|
+
/* @__PURE__ */ jsx7(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
|
|
1691
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
1692
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
1693
|
+
/* @__PURE__ */ jsx7("p", { className: "truncate text-sm font-medium", children: entry.name }),
|
|
1694
|
+
entry.deprecated && /* @__PURE__ */ jsx7(
|
|
1695
|
+
"span",
|
|
1696
|
+
{
|
|
1697
|
+
"data-testid": `connector-deprecated-${entry.slug}`,
|
|
1698
|
+
className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
|
|
1699
|
+
children: "No longer available"
|
|
1700
|
+
}
|
|
1701
|
+
)
|
|
1702
|
+
] }),
|
|
1703
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1704
|
+
/* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
|
|
1705
|
+
entry.category && /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1706
|
+
/* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
1707
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
|
|
1708
|
+
] })
|
|
1709
|
+
] }),
|
|
1710
|
+
isConnected && /* @__PURE__ */ jsx7(
|
|
1711
|
+
"p",
|
|
1567
1712
|
{
|
|
1568
|
-
"data-testid": `connector-
|
|
1569
|
-
className: "
|
|
1570
|
-
children:
|
|
1713
|
+
"data-testid": `connector-connected-since-${entry.slug}`,
|
|
1714
|
+
className: "text-xs text-muted-foreground",
|
|
1715
|
+
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1571
1716
|
}
|
|
1572
|
-
)
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1717
|
+
),
|
|
1718
|
+
attribution && /* @__PURE__ */ jsx7(
|
|
1719
|
+
"p",
|
|
1720
|
+
{
|
|
1721
|
+
"data-testid": `connector-owner-${entry.slug}`,
|
|
1722
|
+
className: "text-xs text-muted-foreground",
|
|
1723
|
+
children: attribution
|
|
1724
|
+
}
|
|
1725
|
+
),
|
|
1726
|
+
!isConnected && /* @__PURE__ */ jsxs6(
|
|
1727
|
+
"div",
|
|
1728
|
+
{
|
|
1729
|
+
"data-testid": `connector-kind-toggle-${entry.slug}`,
|
|
1730
|
+
role: "group",
|
|
1731
|
+
"aria-label": "Connect as",
|
|
1732
|
+
className: "mt-1 flex items-center gap-1",
|
|
1733
|
+
children: [
|
|
1734
|
+
/* @__PURE__ */ jsx7(
|
|
1735
|
+
"button",
|
|
1736
|
+
{
|
|
1737
|
+
type: "button",
|
|
1738
|
+
"data-testid": `connector-kind-personal-${entry.slug}`,
|
|
1739
|
+
"aria-pressed": kind === "personal",
|
|
1740
|
+
disabled: isConnecting,
|
|
1741
|
+
onClick: () => setKind("personal"),
|
|
1742
|
+
className: cn(
|
|
1743
|
+
"rounded-full border px-2 py-0.5 text-[11px]",
|
|
1744
|
+
kind === "personal" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
|
|
1745
|
+
"disabled:opacity-50"
|
|
1746
|
+
),
|
|
1747
|
+
children: "Personal"
|
|
1748
|
+
}
|
|
1749
|
+
),
|
|
1750
|
+
/* @__PURE__ */ jsxs6(
|
|
1751
|
+
"button",
|
|
1752
|
+
{
|
|
1753
|
+
type: "button",
|
|
1754
|
+
"data-testid": `connector-kind-org-${entry.slug}`,
|
|
1755
|
+
"aria-pressed": kind === "org",
|
|
1756
|
+
disabled: isConnecting || !orgPermitted,
|
|
1757
|
+
title: orgPermitted ? void 0 : orgDisabledReason,
|
|
1758
|
+
onClick: () => setKind("org"),
|
|
1759
|
+
className: cn(
|
|
1760
|
+
"rounded-full border px-2 py-0.5 text-[11px]",
|
|
1761
|
+
kind === "org" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
|
|
1762
|
+
"disabled:opacity-50"
|
|
1763
|
+
),
|
|
1764
|
+
children: [
|
|
1765
|
+
"Org",
|
|
1766
|
+
!orgPermitted && /* @__PURE__ */ jsxs6("span", { className: "sr-only", children: [
|
|
1767
|
+
" (",
|
|
1768
|
+
orgDisabledReason,
|
|
1769
|
+
")"
|
|
1770
|
+
] })
|
|
1771
|
+
]
|
|
1772
|
+
}
|
|
1773
|
+
)
|
|
1774
|
+
]
|
|
1775
|
+
}
|
|
1776
|
+
),
|
|
1777
|
+
errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1580
1778
|
] }),
|
|
1581
|
-
isConnected
|
|
1582
|
-
|
|
1779
|
+
isConnected ? /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1780
|
+
canDisconnect && /* @__PURE__ */ jsx7(
|
|
1781
|
+
"button",
|
|
1782
|
+
{
|
|
1783
|
+
type: "button",
|
|
1784
|
+
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1785
|
+
onClick: () => onDisconnect(entry),
|
|
1786
|
+
disabled: isDisconnecting,
|
|
1787
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1788
|
+
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1789
|
+
}
|
|
1790
|
+
),
|
|
1791
|
+
canManageSharing && /* @__PURE__ */ jsx7(
|
|
1792
|
+
"button",
|
|
1793
|
+
{
|
|
1794
|
+
type: "button",
|
|
1795
|
+
"data-testid": `connector-manage-sharing-${entry.slug}`,
|
|
1796
|
+
"aria-expanded": sharingOpen,
|
|
1797
|
+
onClick: () => setSharingOpen((open) => !open),
|
|
1798
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-foreground/10",
|
|
1799
|
+
children: sharingOpen ? "Hide sharing" : "Manage sharing"
|
|
1800
|
+
}
|
|
1801
|
+
)
|
|
1802
|
+
] }) : /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1803
|
+
/* @__PURE__ */ jsx7(
|
|
1804
|
+
"button",
|
|
1805
|
+
{
|
|
1806
|
+
type: "button",
|
|
1807
|
+
"data-testid": `connector-connect-${entry.slug}`,
|
|
1808
|
+
onClick: () => onConnect(entry, kind),
|
|
1809
|
+
disabled: isConnecting,
|
|
1810
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1811
|
+
children: isConnecting ? "Connecting\u2026" : "Connect"
|
|
1812
|
+
}
|
|
1813
|
+
),
|
|
1814
|
+
canSetUp && /* @__PURE__ */ jsx7(
|
|
1815
|
+
"button",
|
|
1816
|
+
{
|
|
1817
|
+
type: "button",
|
|
1818
|
+
"data-testid": `connector-setup-toggle-${entry.slug}`,
|
|
1819
|
+
"aria-expanded": setupOpen,
|
|
1820
|
+
"aria-controls": `connector-setup-panel-${entry.slug}`,
|
|
1821
|
+
onClick: () => setSetupOpen((open) => !open),
|
|
1822
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-foreground/10",
|
|
1823
|
+
children: setupOpen ? "Hide setup" : pasteSupported ? "Use a token instead" : "Setup guide"
|
|
1824
|
+
}
|
|
1825
|
+
)
|
|
1826
|
+
] })
|
|
1827
|
+
] }),
|
|
1828
|
+
canSetUp && setupOpen && /* @__PURE__ */ jsxs6(
|
|
1829
|
+
"div",
|
|
1830
|
+
{
|
|
1831
|
+
id: `connector-setup-panel-${entry.slug}`,
|
|
1832
|
+
"data-testid": `connector-setup-panel-${entry.slug}`,
|
|
1833
|
+
className: "mt-2 flex flex-col gap-3 rounded-md border border-border bg-foreground/5 p-2",
|
|
1834
|
+
children: [
|
|
1835
|
+
guide && /* @__PURE__ */ jsx7(SetupGuidePanel, { entry, guide, showVerify: !pasteSupported }),
|
|
1836
|
+
pasteSupported && /* @__PURE__ */ jsx7(
|
|
1837
|
+
CredentialForm,
|
|
1838
|
+
{
|
|
1839
|
+
entry,
|
|
1840
|
+
fields: credentialFields,
|
|
1841
|
+
kind,
|
|
1842
|
+
verify: guide?.verify ?? null,
|
|
1843
|
+
testSupported: credentialTestSupported,
|
|
1844
|
+
onSubmitCredentials,
|
|
1845
|
+
onTestCredentials
|
|
1846
|
+
}
|
|
1847
|
+
)
|
|
1848
|
+
]
|
|
1849
|
+
}
|
|
1850
|
+
),
|
|
1851
|
+
canManageSharing && sharingOpen && /* @__PURE__ */ jsx7(
|
|
1852
|
+
ManageSharing,
|
|
1853
|
+
{
|
|
1854
|
+
entry,
|
|
1855
|
+
listGrants,
|
|
1856
|
+
addGrant,
|
|
1857
|
+
revokeGrant
|
|
1858
|
+
}
|
|
1859
|
+
)
|
|
1860
|
+
]
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
function groundGuide(guide) {
|
|
1865
|
+
const ungrounded = [];
|
|
1866
|
+
const keep = (text) => {
|
|
1867
|
+
if (!hasUngroundedPlaceholder(text)) return true;
|
|
1868
|
+
ungrounded.push(text);
|
|
1869
|
+
return false;
|
|
1870
|
+
};
|
|
1871
|
+
const prerequisites = guide.prerequisites.filter(keep);
|
|
1872
|
+
const steps = guide.steps.filter(
|
|
1873
|
+
(step) => keep(step.text) && (!step.copyable || keep(step.copyable))
|
|
1874
|
+
);
|
|
1875
|
+
const scopeNote = keep(guide.scope_note) ? guide.scope_note : null;
|
|
1876
|
+
const verify = keep(guide.verify) ? guide.verify : null;
|
|
1877
|
+
return { prerequisites, steps, scopeNote, verify, ungrounded };
|
|
1878
|
+
}
|
|
1879
|
+
function CopyableValue({ value, testId }) {
|
|
1880
|
+
const [copied, setCopied] = useState7(false);
|
|
1881
|
+
const timer = useRef5(null);
|
|
1882
|
+
useEffect6(
|
|
1883
|
+
() => () => {
|
|
1884
|
+
if (timer.current) clearTimeout(timer.current);
|
|
1885
|
+
},
|
|
1886
|
+
[]
|
|
1887
|
+
);
|
|
1888
|
+
const copy = useCallback4(async () => {
|
|
1889
|
+
try {
|
|
1890
|
+
if (typeof navigator.clipboard?.writeText !== "function") {
|
|
1891
|
+
throw new Error("clipboard unavailable");
|
|
1892
|
+
}
|
|
1893
|
+
await navigator.clipboard.writeText(value);
|
|
1894
|
+
setCopied(true);
|
|
1895
|
+
if (timer.current) clearTimeout(timer.current);
|
|
1896
|
+
timer.current = setTimeout(() => setCopied(false), 1500);
|
|
1897
|
+
} catch {
|
|
1898
|
+
setCopied(false);
|
|
1899
|
+
}
|
|
1900
|
+
}, [value]);
|
|
1901
|
+
return /* @__PURE__ */ jsxs6("span", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
1902
|
+
/* @__PURE__ */ jsx7(
|
|
1903
|
+
"code",
|
|
1904
|
+
{
|
|
1905
|
+
"data-testid": `${testId}-value`,
|
|
1906
|
+
className: "min-w-0 flex-1 truncate rounded bg-foreground/10 px-1.5 py-0.5 text-[11px]",
|
|
1907
|
+
children: value
|
|
1908
|
+
}
|
|
1909
|
+
),
|
|
1910
|
+
/* @__PURE__ */ jsx7(
|
|
1911
|
+
"button",
|
|
1912
|
+
{
|
|
1913
|
+
type: "button",
|
|
1914
|
+
"data-testid": testId,
|
|
1915
|
+
onClick: () => void copy(),
|
|
1916
|
+
className: "shrink-0 rounded-md px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-foreground/10",
|
|
1917
|
+
children: copied ? "Copied" : "Copy"
|
|
1918
|
+
}
|
|
1919
|
+
)
|
|
1920
|
+
] });
|
|
1921
|
+
}
|
|
1922
|
+
function SetupGuidePanel({
|
|
1923
|
+
entry,
|
|
1924
|
+
guide,
|
|
1925
|
+
showVerify
|
|
1926
|
+
}) {
|
|
1927
|
+
const { prerequisites, steps, scopeNote, verify, ungrounded } = groundGuide(guide);
|
|
1928
|
+
const leaked = ungrounded.length > 0;
|
|
1929
|
+
useEffect6(() => {
|
|
1930
|
+
if (!leaked) return;
|
|
1931
|
+
console.error(
|
|
1932
|
+
`[ConnectorPicker] setup guide for "${entry.slug}" arrived with ungrounded placeholders:`,
|
|
1933
|
+
ungrounded
|
|
1934
|
+
);
|
|
1935
|
+
}, [entry.slug, guide, leaked]);
|
|
1936
|
+
return /* @__PURE__ */ jsxs6("div", { "data-testid": `connector-guide-${entry.slug}`, className: "flex flex-col gap-2", children: [
|
|
1937
|
+
leaked && isDevEnvironment() && /* @__PURE__ */ jsxs6(
|
|
1938
|
+
"p",
|
|
1939
|
+
{
|
|
1940
|
+
role: "alert",
|
|
1941
|
+
"data-testid": `connector-guide-ungrounded-${entry.slug}`,
|
|
1942
|
+
className: "rounded-md border border-destructive/30 bg-destructive/5 px-2 py-1 text-xs text-destructive",
|
|
1943
|
+
children: [
|
|
1944
|
+
"Setup guide arrived with unresolved placeholders and those lines were dropped: ",
|
|
1945
|
+
ungrounded.join(" \xB7 ")
|
|
1946
|
+
]
|
|
1947
|
+
}
|
|
1948
|
+
),
|
|
1949
|
+
prerequisites.length > 0 && /* @__PURE__ */ jsxs6("div", { children: [
|
|
1950
|
+
/* @__PURE__ */ jsx7("h4", { className: "text-xs font-medium", children: "Before you start" }),
|
|
1951
|
+
/* @__PURE__ */ jsx7(
|
|
1952
|
+
"ul",
|
|
1953
|
+
{
|
|
1954
|
+
"data-testid": `connector-guide-prerequisites-${entry.slug}`,
|
|
1955
|
+
className: "mt-1 list-disc pl-4 text-xs text-muted-foreground",
|
|
1956
|
+
children: prerequisites.map((item) => /* @__PURE__ */ jsx7("li", { children: item }, item))
|
|
1957
|
+
}
|
|
1958
|
+
)
|
|
1959
|
+
] }),
|
|
1960
|
+
steps.length > 0 && /* @__PURE__ */ jsx7(
|
|
1961
|
+
"ol",
|
|
1962
|
+
{
|
|
1963
|
+
"data-testid": `connector-guide-steps-${entry.slug}`,
|
|
1964
|
+
className: "list-decimal pl-4 text-xs",
|
|
1965
|
+
children: steps.map((step, index) => /* @__PURE__ */ jsxs6("li", { className: "mt-1 first:mt-0", children: [
|
|
1966
|
+
/* @__PURE__ */ jsx7("span", { children: step.text }),
|
|
1967
|
+
step.copyable && /* @__PURE__ */ jsx7(
|
|
1968
|
+
CopyableValue,
|
|
1969
|
+
{
|
|
1970
|
+
value: step.copyable,
|
|
1971
|
+
testId: `connector-guide-copy-${entry.slug}-${index}`
|
|
1972
|
+
}
|
|
1973
|
+
)
|
|
1974
|
+
] }, step.text))
|
|
1975
|
+
}
|
|
1976
|
+
),
|
|
1977
|
+
scopeNote && /* @__PURE__ */ jsx7(
|
|
1978
|
+
"p",
|
|
1979
|
+
{
|
|
1980
|
+
"data-testid": `connector-guide-scope-note-${entry.slug}`,
|
|
1981
|
+
className: "rounded-md border border-primary/30 bg-primary/5 px-2 py-1 text-xs font-medium",
|
|
1982
|
+
children: scopeNote
|
|
1983
|
+
}
|
|
1984
|
+
),
|
|
1985
|
+
showVerify && verify && /* @__PURE__ */ jsx7(
|
|
1986
|
+
"p",
|
|
1987
|
+
{
|
|
1988
|
+
"data-testid": `connector-guide-verify-${entry.slug}`,
|
|
1989
|
+
className: "text-xs text-muted-foreground",
|
|
1990
|
+
children: verify
|
|
1991
|
+
}
|
|
1992
|
+
)
|
|
1993
|
+
] });
|
|
1994
|
+
}
|
|
1995
|
+
function CredentialForm({
|
|
1996
|
+
entry,
|
|
1997
|
+
fields,
|
|
1998
|
+
kind,
|
|
1999
|
+
verify,
|
|
2000
|
+
testSupported,
|
|
2001
|
+
onSubmitCredentials,
|
|
2002
|
+
onTestCredentials
|
|
2003
|
+
}) {
|
|
2004
|
+
const [values, setValues] = useState7(() => {
|
|
2005
|
+
const initial = {};
|
|
2006
|
+
for (const field of fields) {
|
|
2007
|
+
if (field.defaultValue) initial[field.key] = field.defaultValue;
|
|
2008
|
+
}
|
|
2009
|
+
return initial;
|
|
2010
|
+
});
|
|
2011
|
+
const [busy, setBusy] = useState7(false);
|
|
2012
|
+
const [testing, setTesting] = useState7(false);
|
|
2013
|
+
const [error, setError] = useState7(null);
|
|
2014
|
+
const [result, setResult] = useState7(null);
|
|
2015
|
+
const entryRef = useRef5(entry);
|
|
2016
|
+
entryRef.current = entry;
|
|
2017
|
+
const updateField = useCallback4((key, value) => {
|
|
2018
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
2019
|
+
setError(null);
|
|
2020
|
+
setResult(null);
|
|
2021
|
+
}, []);
|
|
2022
|
+
const missingRequired = fields.filter((field) => field.required !== false).some((field) => !values[field.key]?.trim());
|
|
2023
|
+
const failedTest = result?.valid === false;
|
|
2024
|
+
const untested = testSupported && result === null;
|
|
2025
|
+
const fieldErrors = result?.field_errors ?? null;
|
|
2026
|
+
const handleTest = useCallback4(async () => {
|
|
2027
|
+
setTesting(true);
|
|
2028
|
+
setError(null);
|
|
2029
|
+
setResult(null);
|
|
2030
|
+
try {
|
|
2031
|
+
setResult(await onTestCredentials(entryRef.current, values));
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
setError(errorMessageFrom(err, DEFAULT_CREDENTIAL_TEST_ERROR));
|
|
2034
|
+
} finally {
|
|
2035
|
+
setTesting(false);
|
|
2036
|
+
}
|
|
2037
|
+
}, [onTestCredentials, values]);
|
|
2038
|
+
const handleSubmit = useCallback4(async () => {
|
|
2039
|
+
setBusy(true);
|
|
2040
|
+
setError(null);
|
|
2041
|
+
try {
|
|
2042
|
+
await onSubmitCredentials(entryRef.current, values, kind);
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
setError(errorMessageFrom(err, DEFAULT_CREDENTIAL_ERROR));
|
|
2045
|
+
} finally {
|
|
2046
|
+
setBusy(false);
|
|
2047
|
+
}
|
|
2048
|
+
}, [kind, onSubmitCredentials, values]);
|
|
2049
|
+
return /* @__PURE__ */ jsxs6(
|
|
2050
|
+
"form",
|
|
2051
|
+
{
|
|
2052
|
+
"data-testid": `connector-credential-form-${entry.slug}`,
|
|
2053
|
+
onSubmit: (event) => {
|
|
2054
|
+
event.preventDefault();
|
|
2055
|
+
if (missingRequired || busy || testing || failedTest || untested) return;
|
|
2056
|
+
void handleSubmit();
|
|
2057
|
+
},
|
|
2058
|
+
className: "flex flex-col gap-2",
|
|
2059
|
+
children: [
|
|
2060
|
+
fields.map((field) => {
|
|
2061
|
+
const inputId = `connector-credential-${entry.slug}-${field.key}`;
|
|
2062
|
+
const fieldError = fieldErrors?.[field.key] ?? null;
|
|
2063
|
+
return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1", children: [
|
|
2064
|
+
/* @__PURE__ */ jsxs6("label", { htmlFor: inputId, className: "text-xs font-medium", children: [
|
|
2065
|
+
field.label,
|
|
2066
|
+
field.required !== false && /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", className: "ml-0.5 text-destructive", children: "*" })
|
|
2067
|
+
] }),
|
|
2068
|
+
/* @__PURE__ */ jsx7(
|
|
2069
|
+
"input",
|
|
2070
|
+
{
|
|
2071
|
+
id: inputId,
|
|
2072
|
+
"data-testid": `connector-credential-input-${entry.slug}-${field.key}`,
|
|
2073
|
+
type: field.type ?? "text",
|
|
2074
|
+
value: values[field.key] ?? "",
|
|
2075
|
+
onChange: (event) => updateField(field.key, event.target.value),
|
|
2076
|
+
placeholder: field.placeholder ?? void 0,
|
|
2077
|
+
required: field.required !== false,
|
|
2078
|
+
disabled: busy || testing,
|
|
2079
|
+
autoComplete: "off",
|
|
2080
|
+
"aria-invalid": fieldError ? true : void 0,
|
|
2081
|
+
"aria-describedby": fieldError ? `${inputId}-error` : void 0,
|
|
2082
|
+
className: cn(
|
|
2083
|
+
"rounded-md border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
|
|
2084
|
+
fieldError ? "border-destructive" : "border-border"
|
|
2085
|
+
)
|
|
2086
|
+
}
|
|
2087
|
+
),
|
|
2088
|
+
fieldError && /* @__PURE__ */ jsx7(
|
|
2089
|
+
"p",
|
|
2090
|
+
{
|
|
2091
|
+
id: `${inputId}-error`,
|
|
2092
|
+
"data-testid": `connector-credential-field-error-${entry.slug}-${field.key}`,
|
|
2093
|
+
className: "text-xs text-destructive",
|
|
2094
|
+
children: fieldError
|
|
2095
|
+
}
|
|
2096
|
+
)
|
|
2097
|
+
] }, field.key);
|
|
2098
|
+
}),
|
|
2099
|
+
verify && /* @__PURE__ */ jsx7(
|
|
2100
|
+
"p",
|
|
2101
|
+
{
|
|
2102
|
+
"data-testid": `connector-guide-verify-${entry.slug}`,
|
|
2103
|
+
className: "text-xs text-muted-foreground",
|
|
2104
|
+
children: verify
|
|
2105
|
+
}
|
|
2106
|
+
),
|
|
2107
|
+
result && /* @__PURE__ */ jsx7(CredentialTestResult, { entry, result }),
|
|
2108
|
+
error && /* @__PURE__ */ jsx7(
|
|
2109
|
+
"p",
|
|
2110
|
+
{
|
|
2111
|
+
role: "alert",
|
|
2112
|
+
"data-testid": `connector-credential-error-${entry.slug}`,
|
|
2113
|
+
className: "text-xs text-destructive",
|
|
2114
|
+
children: error
|
|
2115
|
+
}
|
|
2116
|
+
),
|
|
2117
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-1.5", children: [
|
|
2118
|
+
testSupported && /* @__PURE__ */ jsx7(
|
|
2119
|
+
"button",
|
|
1583
2120
|
{
|
|
1584
|
-
"
|
|
1585
|
-
|
|
1586
|
-
|
|
2121
|
+
type: "button",
|
|
2122
|
+
"data-testid": `connector-credential-test-${entry.slug}`,
|
|
2123
|
+
onClick: () => void handleTest(),
|
|
2124
|
+
disabled: missingRequired || busy || testing,
|
|
2125
|
+
className: "rounded-md border border-border px-2.5 py-1 text-xs font-medium disabled:opacity-50",
|
|
2126
|
+
children: testing ? "Testing\u2026" : "Test connection"
|
|
1587
2127
|
}
|
|
1588
2128
|
),
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
2129
|
+
/* @__PURE__ */ jsx7(
|
|
2130
|
+
"button",
|
|
2131
|
+
{
|
|
2132
|
+
type: "submit",
|
|
2133
|
+
"data-testid": `connector-credential-save-${entry.slug}`,
|
|
2134
|
+
disabled: missingRequired || busy || testing || failedTest || untested,
|
|
2135
|
+
title: failedTest ? "That credential failed its own check \u2014 fix it and test again." : untested ? "Test the credential before saving it." : void 0,
|
|
2136
|
+
className: "rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
2137
|
+
children: busy ? "Saving\u2026" : "Save credential"
|
|
2138
|
+
}
|
|
2139
|
+
)
|
|
2140
|
+
] })
|
|
2141
|
+
]
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
function CredentialTestResult({
|
|
2146
|
+
entry,
|
|
2147
|
+
result
|
|
2148
|
+
}) {
|
|
2149
|
+
if (result.valid === true) {
|
|
2150
|
+
return /* @__PURE__ */ jsxs6(
|
|
2151
|
+
"div",
|
|
2152
|
+
{
|
|
2153
|
+
role: "status",
|
|
2154
|
+
"data-testid": `connector-credential-test-pass-${entry.slug}`,
|
|
2155
|
+
className: "rounded-md border border-primary/30 bg-primary/5 px-2 py-1 text-xs",
|
|
2156
|
+
children: [
|
|
2157
|
+
/* @__PURE__ */ jsx7("p", { children: result.account_name ? `Verified \u2014 connected as ${result.account_name}.` : "Verified \u2014 these credentials work." }),
|
|
2158
|
+
result.permissions_note && /* @__PURE__ */ jsx7(
|
|
2159
|
+
"p",
|
|
2160
|
+
{
|
|
2161
|
+
"data-testid": `connector-credential-permissions-${entry.slug}`,
|
|
2162
|
+
className: "mt-0.5 font-medium",
|
|
2163
|
+
children: result.permissions_note
|
|
2164
|
+
}
|
|
2165
|
+
)
|
|
2166
|
+
]
|
|
2167
|
+
}
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
if (result.valid === false) {
|
|
2171
|
+
return /* @__PURE__ */ jsx7(
|
|
2172
|
+
"p",
|
|
2173
|
+
{
|
|
2174
|
+
role: "alert",
|
|
2175
|
+
"data-testid": `connector-credential-test-fail-${entry.slug}`,
|
|
2176
|
+
className: "rounded-md border border-destructive/30 bg-destructive/5 px-2 py-1 text-xs text-destructive",
|
|
2177
|
+
children: result.error || "That credential didn't pass its check."
|
|
2178
|
+
}
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
return /* @__PURE__ */ jsx7(
|
|
2182
|
+
"p",
|
|
2183
|
+
{
|
|
2184
|
+
role: "status",
|
|
2185
|
+
"data-testid": `connector-credential-test-unverified-${entry.slug}`,
|
|
2186
|
+
className: "rounded-md border border-border px-2 py-1 text-xs text-muted-foreground",
|
|
2187
|
+
children: UNVERIFIABLE_CREDENTIAL_NOTE
|
|
2188
|
+
}
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
function ManageSharing({
|
|
2192
|
+
entry,
|
|
2193
|
+
listGrants,
|
|
2194
|
+
addGrant,
|
|
2195
|
+
revokeGrant
|
|
2196
|
+
}) {
|
|
2197
|
+
const [grants, setGrants] = useState7(null);
|
|
2198
|
+
const [error, setError] = useState7(null);
|
|
2199
|
+
const [busy, setBusy] = useState7(false);
|
|
2200
|
+
const [granteeType, setGranteeType] = useState7("user");
|
|
2201
|
+
const [granteeId, setGranteeId] = useState7("");
|
|
2202
|
+
const roleOptions = entry.org_connect?.role_options ?? [];
|
|
2203
|
+
const entryRef = useRef5(entry);
|
|
2204
|
+
entryRef.current = entry;
|
|
2205
|
+
const load = useCallback4(async () => {
|
|
2206
|
+
try {
|
|
2207
|
+
setGrants(await listGrants(entryRef.current));
|
|
2208
|
+
setError(null);
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
setError(errorMessageFrom(err, DEFAULT_GRANTS_LOAD_ERROR));
|
|
2211
|
+
}
|
|
2212
|
+
}, [listGrants]);
|
|
2213
|
+
useEffect6(() => {
|
|
2214
|
+
void load();
|
|
2215
|
+
}, [load]);
|
|
2216
|
+
const handleAdd = useCallback4(async () => {
|
|
2217
|
+
const id = granteeId.trim();
|
|
2218
|
+
if (!id) return;
|
|
2219
|
+
setBusy(true);
|
|
2220
|
+
try {
|
|
2221
|
+
await addGrant(entryRef.current, { granteeType, granteeId: id });
|
|
2222
|
+
setGranteeId("");
|
|
2223
|
+
await load();
|
|
2224
|
+
} catch (err) {
|
|
2225
|
+
setError(errorMessageFrom(err, DEFAULT_GRANT_ERROR));
|
|
2226
|
+
} finally {
|
|
2227
|
+
setBusy(false);
|
|
2228
|
+
}
|
|
2229
|
+
}, [addGrant, granteeId, granteeType, load]);
|
|
2230
|
+
const handleRevoke = useCallback4(
|
|
2231
|
+
async (grantId) => {
|
|
2232
|
+
setBusy(true);
|
|
2233
|
+
try {
|
|
2234
|
+
await revokeGrant(entryRef.current, grantId);
|
|
2235
|
+
await load();
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
setError(errorMessageFrom(err, DEFAULT_REVOKE_ERROR));
|
|
2238
|
+
} finally {
|
|
2239
|
+
setBusy(false);
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
[load, revokeGrant]
|
|
2243
|
+
);
|
|
2244
|
+
return /* @__PURE__ */ jsxs6(
|
|
2245
|
+
"div",
|
|
2246
|
+
{
|
|
2247
|
+
"data-testid": `connector-sharing-panel-${entry.slug}`,
|
|
2248
|
+
className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-foreground/5 p-2",
|
|
2249
|
+
children: [
|
|
2250
|
+
grants === null && !error && /* @__PURE__ */ jsx7(
|
|
2251
|
+
"p",
|
|
1593
2252
|
{
|
|
1594
|
-
|
|
1595
|
-
"
|
|
1596
|
-
|
|
1597
|
-
disabled: isDisconnecting,
|
|
1598
|
-
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1599
|
-
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
2253
|
+
"data-testid": `connector-grants-loading-${entry.slug}`,
|
|
2254
|
+
className: "text-xs text-muted-foreground",
|
|
2255
|
+
children: "Loading sharing\u2026"
|
|
1600
2256
|
}
|
|
1601
|
-
)
|
|
1602
|
-
|
|
2257
|
+
),
|
|
2258
|
+
error && /* @__PURE__ */ jsx7(
|
|
2259
|
+
"p",
|
|
1603
2260
|
{
|
|
1604
|
-
|
|
1605
|
-
"data-testid": `connector-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1609
|
-
children: isConnecting ? "Connecting\u2026" : "Connect"
|
|
2261
|
+
role: "alert",
|
|
2262
|
+
"data-testid": `connector-sharing-error-${entry.slug}`,
|
|
2263
|
+
className: "text-xs text-destructive",
|
|
2264
|
+
children: error
|
|
1610
2265
|
}
|
|
1611
|
-
)
|
|
2266
|
+
),
|
|
2267
|
+
grants !== null && grants.length === 0 && /* @__PURE__ */ jsx7(
|
|
2268
|
+
"p",
|
|
2269
|
+
{
|
|
2270
|
+
"data-testid": `connector-grants-empty-${entry.slug}`,
|
|
2271
|
+
className: "text-xs text-muted-foreground",
|
|
2272
|
+
children: "Not shared with anyone yet."
|
|
2273
|
+
}
|
|
2274
|
+
),
|
|
2275
|
+
grants?.map((grant) => /* @__PURE__ */ jsxs6(
|
|
2276
|
+
"div",
|
|
2277
|
+
{
|
|
2278
|
+
"data-testid": `connector-grant-${entry.slug}-${grant.id}`,
|
|
2279
|
+
className: "flex items-center justify-between gap-2 text-xs",
|
|
2280
|
+
children: [
|
|
2281
|
+
/* @__PURE__ */ jsxs6("span", { className: "truncate", children: [
|
|
2282
|
+
grant.grantee_name || grant.grantee_id,
|
|
2283
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
|
|
2284
|
+
" ",
|
|
2285
|
+
"\xB7 ",
|
|
2286
|
+
grant.grantee_type === "role" ? "Role" : "User"
|
|
2287
|
+
] })
|
|
2288
|
+
] }),
|
|
2289
|
+
/* @__PURE__ */ jsx7(
|
|
2290
|
+
"button",
|
|
2291
|
+
{
|
|
2292
|
+
type: "button",
|
|
2293
|
+
"data-testid": `connector-grant-revoke-${entry.slug}-${grant.id}`,
|
|
2294
|
+
onClick: () => void handleRevoke(grant.id),
|
|
2295
|
+
disabled: busy,
|
|
2296
|
+
className: "shrink-0 rounded-md px-2 py-0.5 font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
2297
|
+
children: "Revoke"
|
|
2298
|
+
}
|
|
2299
|
+
)
|
|
2300
|
+
]
|
|
2301
|
+
},
|
|
2302
|
+
grant.id
|
|
2303
|
+
)),
|
|
2304
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5", children: [
|
|
2305
|
+
/* @__PURE__ */ jsxs6(
|
|
2306
|
+
"select",
|
|
2307
|
+
{
|
|
2308
|
+
"data-testid": `connector-grant-type-${entry.slug}`,
|
|
2309
|
+
"aria-label": "Grantee type",
|
|
2310
|
+
value: granteeType,
|
|
2311
|
+
onChange: (event) => {
|
|
2312
|
+
setGranteeType(event.target.value);
|
|
2313
|
+
setGranteeId("");
|
|
2314
|
+
},
|
|
2315
|
+
disabled: busy,
|
|
2316
|
+
className: "rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
|
|
2317
|
+
children: [
|
|
2318
|
+
/* @__PURE__ */ jsx7("option", { value: "user", children: "User" }),
|
|
2319
|
+
/* @__PURE__ */ jsx7("option", { value: "role", children: "Role" })
|
|
2320
|
+
]
|
|
2321
|
+
}
|
|
2322
|
+
),
|
|
2323
|
+
granteeType === "user" ? /* @__PURE__ */ jsx7(
|
|
2324
|
+
"input",
|
|
2325
|
+
{
|
|
2326
|
+
"data-testid": `connector-grant-grantee-${entry.slug}`,
|
|
2327
|
+
"aria-label": "User ID",
|
|
2328
|
+
value: granteeId,
|
|
2329
|
+
onChange: (event) => setGranteeId(event.target.value),
|
|
2330
|
+
placeholder: "User ID",
|
|
2331
|
+
disabled: busy,
|
|
2332
|
+
className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50"
|
|
2333
|
+
}
|
|
2334
|
+
) : /* @__PURE__ */ jsxs6(
|
|
2335
|
+
"select",
|
|
2336
|
+
{
|
|
2337
|
+
"data-testid": `connector-grant-role-${entry.slug}`,
|
|
2338
|
+
"aria-label": "Role",
|
|
2339
|
+
value: granteeId,
|
|
2340
|
+
onChange: (event) => setGranteeId(event.target.value),
|
|
2341
|
+
disabled: busy,
|
|
2342
|
+
className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
|
|
2343
|
+
children: [
|
|
2344
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Select a role\u2026" }),
|
|
2345
|
+
roleOptions.map((role) => /* @__PURE__ */ jsx7("option", { value: role.id, children: role.name }, role.id))
|
|
2346
|
+
]
|
|
2347
|
+
}
|
|
2348
|
+
),
|
|
2349
|
+
/* @__PURE__ */ jsx7(
|
|
2350
|
+
"button",
|
|
2351
|
+
{
|
|
2352
|
+
type: "button",
|
|
2353
|
+
"data-testid": `connector-grant-add-${entry.slug}`,
|
|
2354
|
+
onClick: () => void handleAdd(),
|
|
2355
|
+
disabled: busy || !granteeId.trim(),
|
|
2356
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
2357
|
+
children: "Share"
|
|
2358
|
+
}
|
|
2359
|
+
)
|
|
2360
|
+
] })
|
|
1612
2361
|
]
|
|
1613
2362
|
}
|
|
1614
2363
|
);
|