@meetreeve/ui 0.11.0 → 0.12.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/index.d.mts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +116 -13
- package/dist/index.mjs +116 -13
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -719,6 +719,19 @@ interface ConnectorCredentialTestResult {
|
|
|
719
719
|
*/
|
|
720
720
|
permissions_note?: string | null;
|
|
721
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Connector health as the DEV-1964 probe classifies it — Reeve.Connects-wide
|
|
724
|
+
* vocabulary, not provider-specific (the same shape the adbuyer surfaces
|
|
725
|
+
* already read off `GET /api/connects/v1/connections`). Deliberately
|
|
726
|
+
* tri-state, never a `needs_reconnect` boolean (Matt's call, DEV-7873): a
|
|
727
|
+
* probe that has not run yet ("unknown") or has not run at all (the whole
|
|
728
|
+
* field absent) is not the same claim as a probe that ran and failed.
|
|
729
|
+
*/
|
|
730
|
+
interface ConnectorConnectionHealth {
|
|
731
|
+
status: "healthy" | "unhealthy" | "unknown";
|
|
732
|
+
error_code?: string | null;
|
|
733
|
+
checked_at?: string | null;
|
|
734
|
+
}
|
|
722
735
|
interface ConnectableEntry {
|
|
723
736
|
slug: string;
|
|
724
737
|
name: string;
|
|
@@ -749,6 +762,21 @@ interface ConnectableEntry {
|
|
|
749
762
|
} | null;
|
|
750
763
|
/** true = the viewer owns this connection (enables manage-sharing). */
|
|
751
764
|
owned_by_viewer?: boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Live health probe for this connection (DEV-1964 / DEV-7873), when the
|
|
767
|
+
* backend has one. Absent/undefined is NOT "healthy" — it means no probe
|
|
768
|
+
* has run; the picker only ever reacts to an explicit "unhealthy"
|
|
769
|
+
* status, so "unknown" and absent both render as a plain connected
|
|
770
|
+
* tile, same as before this field existed.
|
|
771
|
+
*/
|
|
772
|
+
health?: ConnectorConnectionHealth | null;
|
|
773
|
+
/**
|
|
774
|
+
* Human-readable identity of the external account this connection
|
|
775
|
+
* authenticated as (an email, a workspace name, whatever the provider's
|
|
776
|
+
* own identity claim resolves to). Optional and provider-agnostic — the
|
|
777
|
+
* picker never assumes an email shape or synthesizes this itself.
|
|
778
|
+
*/
|
|
779
|
+
account_label?: string | null;
|
|
752
780
|
};
|
|
753
781
|
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
754
782
|
deprecated: boolean;
|
|
@@ -766,6 +794,15 @@ interface ConnectableEntry {
|
|
|
766
794
|
* connect button renders, exactly as before.
|
|
767
795
|
*/
|
|
768
796
|
credential_fields?: ConnectorCredentialField[] | null;
|
|
797
|
+
/**
|
|
798
|
+
* Plain-language description of what a LIVE connection actually reads or
|
|
799
|
+
* writes (e.g. "Reads recent activity to build your weekly summary") —
|
|
800
|
+
* entirely server-authored copy, same posture as `category`/`name`.
|
|
801
|
+
* Rendered only once connected (it describes a live grant, not the
|
|
802
|
+
* catalog listing); absent renders nothing. This file must never
|
|
803
|
+
* synthesize this text itself for a specific provider.
|
|
804
|
+
*/
|
|
805
|
+
access_summary?: string | null;
|
|
769
806
|
}
|
|
770
807
|
interface ConnectorPickerClient {
|
|
771
808
|
listConnectable(opts: {
|
|
@@ -829,4 +866,4 @@ interface ConnectorPickerProps {
|
|
|
829
866
|
}
|
|
830
867
|
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
831
868
|
|
|
832
|
-
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
|
869
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
package/dist/index.d.ts
CHANGED
|
@@ -719,6 +719,19 @@ interface ConnectorCredentialTestResult {
|
|
|
719
719
|
*/
|
|
720
720
|
permissions_note?: string | null;
|
|
721
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Connector health as the DEV-1964 probe classifies it — Reeve.Connects-wide
|
|
724
|
+
* vocabulary, not provider-specific (the same shape the adbuyer surfaces
|
|
725
|
+
* already read off `GET /api/connects/v1/connections`). Deliberately
|
|
726
|
+
* tri-state, never a `needs_reconnect` boolean (Matt's call, DEV-7873): a
|
|
727
|
+
* probe that has not run yet ("unknown") or has not run at all (the whole
|
|
728
|
+
* field absent) is not the same claim as a probe that ran and failed.
|
|
729
|
+
*/
|
|
730
|
+
interface ConnectorConnectionHealth {
|
|
731
|
+
status: "healthy" | "unhealthy" | "unknown";
|
|
732
|
+
error_code?: string | null;
|
|
733
|
+
checked_at?: string | null;
|
|
734
|
+
}
|
|
722
735
|
interface ConnectableEntry {
|
|
723
736
|
slug: string;
|
|
724
737
|
name: string;
|
|
@@ -749,6 +762,21 @@ interface ConnectableEntry {
|
|
|
749
762
|
} | null;
|
|
750
763
|
/** true = the viewer owns this connection (enables manage-sharing). */
|
|
751
764
|
owned_by_viewer?: boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Live health probe for this connection (DEV-1964 / DEV-7873), when the
|
|
767
|
+
* backend has one. Absent/undefined is NOT "healthy" — it means no probe
|
|
768
|
+
* has run; the picker only ever reacts to an explicit "unhealthy"
|
|
769
|
+
* status, so "unknown" and absent both render as a plain connected
|
|
770
|
+
* tile, same as before this field existed.
|
|
771
|
+
*/
|
|
772
|
+
health?: ConnectorConnectionHealth | null;
|
|
773
|
+
/**
|
|
774
|
+
* Human-readable identity of the external account this connection
|
|
775
|
+
* authenticated as (an email, a workspace name, whatever the provider's
|
|
776
|
+
* own identity claim resolves to). Optional and provider-agnostic — the
|
|
777
|
+
* picker never assumes an email shape or synthesizes this itself.
|
|
778
|
+
*/
|
|
779
|
+
account_label?: string | null;
|
|
752
780
|
};
|
|
753
781
|
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
754
782
|
deprecated: boolean;
|
|
@@ -766,6 +794,15 @@ interface ConnectableEntry {
|
|
|
766
794
|
* connect button renders, exactly as before.
|
|
767
795
|
*/
|
|
768
796
|
credential_fields?: ConnectorCredentialField[] | null;
|
|
797
|
+
/**
|
|
798
|
+
* Plain-language description of what a LIVE connection actually reads or
|
|
799
|
+
* writes (e.g. "Reads recent activity to build your weekly summary") —
|
|
800
|
+
* entirely server-authored copy, same posture as `category`/`name`.
|
|
801
|
+
* Rendered only once connected (it describes a live grant, not the
|
|
802
|
+
* catalog listing); absent renders nothing. This file must never
|
|
803
|
+
* synthesize this text itself for a specific provider.
|
|
804
|
+
*/
|
|
805
|
+
access_summary?: string | null;
|
|
769
806
|
}
|
|
770
807
|
interface ConnectorPickerClient {
|
|
771
808
|
listConnectable(opts: {
|
|
@@ -829,4 +866,4 @@ interface ConnectorPickerProps {
|
|
|
829
866
|
}
|
|
830
867
|
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
831
868
|
|
|
832
|
-
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
|
869
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
package/dist/index.js
CHANGED
|
@@ -1425,6 +1425,8 @@ function ContextSwitcher({
|
|
|
1425
1425
|
var import_react9 = require("react");
|
|
1426
1426
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1427
1427
|
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1428
|
+
var MAX_RECONNECT_REFOCUS_CHECKS = 3;
|
|
1429
|
+
var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
|
|
1428
1430
|
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1429
1431
|
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1430
1432
|
var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
|
|
@@ -1433,6 +1435,8 @@ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
|
|
|
1433
1435
|
var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
|
|
1434
1436
|
var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
|
|
1435
1437
|
var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
|
|
1438
|
+
var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
|
|
1439
|
+
var DISCONNECT_REVOKES_HINT = "Disconnecting revokes Reeve's access to this account.";
|
|
1436
1440
|
var UNVERIFIABLE_CREDENTIAL_NOTE = "We can't verify this automatically \u2014 saving it is safe, but nothing has checked it yet.";
|
|
1437
1441
|
var SKELETON_COUNT = 3;
|
|
1438
1442
|
var UNGROUNDED_PLACEHOLDER = /\{\{[^}]*\}\}/;
|
|
@@ -1466,6 +1470,9 @@ function ownerLabel(entry) {
|
|
|
1466
1470
|
function isSharedWithViewer(entry) {
|
|
1467
1471
|
return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
|
|
1468
1472
|
}
|
|
1473
|
+
function isUnhealthy(entry) {
|
|
1474
|
+
return entry.connection.status === "connected" && entry.connection.health?.status === "unhealthy";
|
|
1475
|
+
}
|
|
1469
1476
|
function ConnectorPicker({
|
|
1470
1477
|
client,
|
|
1471
1478
|
purpose,
|
|
@@ -1477,6 +1484,9 @@ function ConnectorPicker({
|
|
|
1477
1484
|
const [entries, setEntries] = (0, import_react9.useState)(null);
|
|
1478
1485
|
const [loadError, setLoadError] = (0, import_react9.useState)(null);
|
|
1479
1486
|
const [uiState, setUiState] = (0, import_react9.useState)({});
|
|
1487
|
+
const reconnectingRef = (0, import_react9.useRef)(/* @__PURE__ */ new Set());
|
|
1488
|
+
const reconnectChecksRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
1489
|
+
const refreshInFlightRef = (0, import_react9.useRef)(false);
|
|
1480
1490
|
const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
|
|
1481
1491
|
const credentialsSupported = typeof client.submitCredentials === "function";
|
|
1482
1492
|
const credentialTestSupported = typeof client.testCredentials === "function";
|
|
@@ -1505,20 +1515,45 @@ function ConnectorPicker({
|
|
|
1505
1515
|
void load();
|
|
1506
1516
|
}, [gated, load, purpose]);
|
|
1507
1517
|
const refreshAfterReturn = (0, import_react9.useCallback)(async () => {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1518
|
+
if (refreshInFlightRef.current) return;
|
|
1519
|
+
refreshInFlightRef.current = true;
|
|
1520
|
+
try {
|
|
1521
|
+
const list = await load();
|
|
1522
|
+
if (!list) return;
|
|
1523
|
+
const stillUnhealthyChecks = /* @__PURE__ */ new Map();
|
|
1513
1524
|
for (const entry of list) {
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1525
|
+
const stillUnhealthy = reconnectingRef.current.has(entry.slug) && entry.connection.health?.status === "unhealthy";
|
|
1526
|
+
if (!stillUnhealthy) continue;
|
|
1527
|
+
const checks = (reconnectChecksRef.current.get(entry.slug) ?? 0) + 1;
|
|
1528
|
+
reconnectChecksRef.current.set(entry.slug, checks);
|
|
1529
|
+
stillUnhealthyChecks.set(entry.slug, checks);
|
|
1519
1530
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1531
|
+
setUiState((prev) => {
|
|
1532
|
+
let changed = false;
|
|
1533
|
+
const next = { ...prev };
|
|
1534
|
+
for (const entry of list) {
|
|
1535
|
+
if (next[entry.slug]?.kind !== "connecting" || entry.connection.status !== "connected") {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const checks = stillUnhealthyChecks.get(entry.slug);
|
|
1539
|
+
if (checks !== void 0 && checks >= MAX_RECONNECT_REFOCUS_CHECKS) {
|
|
1540
|
+
changed = true;
|
|
1541
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1542
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1543
|
+
next[entry.slug] = { kind: "error", message: RECONNECT_UNCONFIRMED_ERROR };
|
|
1544
|
+
} else if (checks === void 0) {
|
|
1545
|
+
changed = true;
|
|
1546
|
+
delete next[entry.slug];
|
|
1547
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1548
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1549
|
+
onConnectedRef.current?.(entry);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return changed ? next : prev;
|
|
1553
|
+
});
|
|
1554
|
+
} finally {
|
|
1555
|
+
refreshInFlightRef.current = false;
|
|
1556
|
+
}
|
|
1522
1557
|
}, [load]);
|
|
1523
1558
|
(0, import_react9.useEffect)(() => {
|
|
1524
1559
|
if (gated) return;
|
|
@@ -1539,6 +1574,12 @@ function ConnectorPicker({
|
|
|
1539
1574
|
}, [gated, refreshAfterReturn]);
|
|
1540
1575
|
const handleConnect = (0, import_react9.useCallback)(
|
|
1541
1576
|
async (entry, kind) => {
|
|
1577
|
+
if (isUnhealthy(entry)) {
|
|
1578
|
+
reconnectingRef.current.add(entry.slug);
|
|
1579
|
+
} else {
|
|
1580
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1581
|
+
}
|
|
1582
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1542
1583
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1543
1584
|
try {
|
|
1544
1585
|
const result = await clientRef.current.connect(entry, { kind });
|
|
@@ -1548,6 +1589,8 @@ function ConnectorPicker({
|
|
|
1548
1589
|
}
|
|
1549
1590
|
const list = await load();
|
|
1550
1591
|
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1592
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1593
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1551
1594
|
setUiState((prev) => {
|
|
1552
1595
|
const next = { ...prev };
|
|
1553
1596
|
delete next[entry.slug];
|
|
@@ -1557,6 +1600,8 @@ function ConnectorPicker({
|
|
|
1557
1600
|
onConnectedRef.current?.(updated);
|
|
1558
1601
|
}
|
|
1559
1602
|
} catch (err) {
|
|
1603
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1604
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1560
1605
|
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1561
1606
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1562
1607
|
}
|
|
@@ -1725,6 +1770,7 @@ function ConnectorTile({
|
|
|
1725
1770
|
const isConnecting = ui?.kind === "connecting";
|
|
1726
1771
|
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1727
1772
|
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1773
|
+
const unhealthy = isUnhealthy(entry);
|
|
1728
1774
|
const [kind, setKind] = (0, import_react9.useState)("personal");
|
|
1729
1775
|
const [sharingOpen, setSharingOpen] = (0, import_react9.useState)(false);
|
|
1730
1776
|
const [setupOpen, setSetupOpen] = (0, import_react9.useState)(false);
|
|
@@ -1774,6 +1820,34 @@ function ConnectorTile({
|
|
|
1774
1820
|
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1775
1821
|
}
|
|
1776
1822
|
),
|
|
1823
|
+
isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1824
|
+
"p",
|
|
1825
|
+
{
|
|
1826
|
+
"data-testid": `connector-account-${entry.slug}`,
|
|
1827
|
+
className: "text-xs text-muted-foreground",
|
|
1828
|
+
children: [
|
|
1829
|
+
"Connected as ",
|
|
1830
|
+
entry.connection.account_label
|
|
1831
|
+
]
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
isConnected && entry.access_summary && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1835
|
+
"p",
|
|
1836
|
+
{
|
|
1837
|
+
"data-testid": `connector-access-summary-${entry.slug}`,
|
|
1838
|
+
className: "text-xs text-muted-foreground",
|
|
1839
|
+
children: entry.access_summary
|
|
1840
|
+
}
|
|
1841
|
+
),
|
|
1842
|
+
unhealthy && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1843
|
+
"p",
|
|
1844
|
+
{
|
|
1845
|
+
role: "alert",
|
|
1846
|
+
"data-testid": `connector-unhealthy-${entry.slug}`,
|
|
1847
|
+
className: "text-xs font-medium text-destructive",
|
|
1848
|
+
children: DEFAULT_RECONNECT_MESSAGE
|
|
1849
|
+
}
|
|
1850
|
+
),
|
|
1777
1851
|
attribution && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1778
1852
|
"p",
|
|
1779
1853
|
{
|
|
@@ -1836,17 +1910,46 @@ function ConnectorTile({
|
|
|
1836
1910
|
errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1837
1911
|
] }),
|
|
1838
1912
|
isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1913
|
+
unhealthy && !shared && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1914
|
+
"button",
|
|
1915
|
+
{
|
|
1916
|
+
type: "button",
|
|
1917
|
+
"data-testid": `connector-reconnect-${entry.slug}`,
|
|
1918
|
+
onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
|
|
1919
|
+
disabled: isConnecting || isDisconnecting,
|
|
1920
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1921
|
+
children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
|
|
1922
|
+
}
|
|
1923
|
+
),
|
|
1839
1924
|
canDisconnect && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1840
1925
|
"button",
|
|
1841
1926
|
{
|
|
1842
1927
|
type: "button",
|
|
1843
1928
|
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1929
|
+
title: DISCONNECT_REVOKES_HINT,
|
|
1844
1930
|
onClick: () => onDisconnect(entry),
|
|
1845
|
-
disabled: isDisconnecting,
|
|
1931
|
+
disabled: isDisconnecting || isConnecting,
|
|
1846
1932
|
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1847
1933
|
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1848
1934
|
}
|
|
1849
1935
|
),
|
|
1936
|
+
canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
|
|
1937
|
+
// paragraph's one long sentence forced the WHOLE shrink-0
|
|
1938
|
+
// button column to grow to its natural (unwrapped) width,
|
|
1939
|
+
// squeezing the sibling `min-w-0 flex-1` text column down to
|
|
1940
|
+
// one word per line at any realistic tile width. RTL's jsdom
|
|
1941
|
+
// tests never caught it — jsdom doesn't lay out real flexbox
|
|
1942
|
+
// sizing, so every textContent assertion still passed. A
|
|
1943
|
+
// narrow max-width forces the hint to wrap within the same
|
|
1944
|
+
// footprint as the button above it instead.
|
|
1945
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1946
|
+
"p",
|
|
1947
|
+
{
|
|
1948
|
+
"data-testid": `connector-disconnect-hint-${entry.slug}`,
|
|
1949
|
+
className: "max-w-28 text-right text-[11px] text-muted-foreground",
|
|
1950
|
+
children: DISCONNECT_REVOKES_HINT
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1850
1953
|
canManageSharing && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1851
1954
|
"button",
|
|
1852
1955
|
{
|
package/dist/index.mjs
CHANGED
|
@@ -1370,6 +1370,8 @@ function ContextSwitcher({
|
|
|
1370
1370
|
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1371
1371
|
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1372
1372
|
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1373
|
+
var MAX_RECONNECT_REFOCUS_CHECKS = 3;
|
|
1374
|
+
var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
|
|
1373
1375
|
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1374
1376
|
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1375
1377
|
var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
|
|
@@ -1378,6 +1380,8 @@ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
|
|
|
1378
1380
|
var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
|
|
1379
1381
|
var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
|
|
1380
1382
|
var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
|
|
1383
|
+
var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
|
|
1384
|
+
var DISCONNECT_REVOKES_HINT = "Disconnecting revokes Reeve's access to this account.";
|
|
1381
1385
|
var UNVERIFIABLE_CREDENTIAL_NOTE = "We can't verify this automatically \u2014 saving it is safe, but nothing has checked it yet.";
|
|
1382
1386
|
var SKELETON_COUNT = 3;
|
|
1383
1387
|
var UNGROUNDED_PLACEHOLDER = /\{\{[^}]*\}\}/;
|
|
@@ -1411,6 +1415,9 @@ function ownerLabel(entry) {
|
|
|
1411
1415
|
function isSharedWithViewer(entry) {
|
|
1412
1416
|
return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
|
|
1413
1417
|
}
|
|
1418
|
+
function isUnhealthy(entry) {
|
|
1419
|
+
return entry.connection.status === "connected" && entry.connection.health?.status === "unhealthy";
|
|
1420
|
+
}
|
|
1414
1421
|
function ConnectorPicker({
|
|
1415
1422
|
client,
|
|
1416
1423
|
purpose,
|
|
@@ -1422,6 +1429,9 @@ function ConnectorPicker({
|
|
|
1422
1429
|
const [entries, setEntries] = useState7(null);
|
|
1423
1430
|
const [loadError, setLoadError] = useState7(null);
|
|
1424
1431
|
const [uiState, setUiState] = useState7({});
|
|
1432
|
+
const reconnectingRef = useRef5(/* @__PURE__ */ new Set());
|
|
1433
|
+
const reconnectChecksRef = useRef5(/* @__PURE__ */ new Map());
|
|
1434
|
+
const refreshInFlightRef = useRef5(false);
|
|
1425
1435
|
const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
|
|
1426
1436
|
const credentialsSupported = typeof client.submitCredentials === "function";
|
|
1427
1437
|
const credentialTestSupported = typeof client.testCredentials === "function";
|
|
@@ -1450,20 +1460,45 @@ function ConnectorPicker({
|
|
|
1450
1460
|
void load();
|
|
1451
1461
|
}, [gated, load, purpose]);
|
|
1452
1462
|
const refreshAfterReturn = useCallback4(async () => {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1463
|
+
if (refreshInFlightRef.current) return;
|
|
1464
|
+
refreshInFlightRef.current = true;
|
|
1465
|
+
try {
|
|
1466
|
+
const list = await load();
|
|
1467
|
+
if (!list) return;
|
|
1468
|
+
const stillUnhealthyChecks = /* @__PURE__ */ new Map();
|
|
1458
1469
|
for (const entry of list) {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1470
|
+
const stillUnhealthy = reconnectingRef.current.has(entry.slug) && entry.connection.health?.status === "unhealthy";
|
|
1471
|
+
if (!stillUnhealthy) continue;
|
|
1472
|
+
const checks = (reconnectChecksRef.current.get(entry.slug) ?? 0) + 1;
|
|
1473
|
+
reconnectChecksRef.current.set(entry.slug, checks);
|
|
1474
|
+
stillUnhealthyChecks.set(entry.slug, checks);
|
|
1464
1475
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1476
|
+
setUiState((prev) => {
|
|
1477
|
+
let changed = false;
|
|
1478
|
+
const next = { ...prev };
|
|
1479
|
+
for (const entry of list) {
|
|
1480
|
+
if (next[entry.slug]?.kind !== "connecting" || entry.connection.status !== "connected") {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const checks = stillUnhealthyChecks.get(entry.slug);
|
|
1484
|
+
if (checks !== void 0 && checks >= MAX_RECONNECT_REFOCUS_CHECKS) {
|
|
1485
|
+
changed = true;
|
|
1486
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1487
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1488
|
+
next[entry.slug] = { kind: "error", message: RECONNECT_UNCONFIRMED_ERROR };
|
|
1489
|
+
} else if (checks === void 0) {
|
|
1490
|
+
changed = true;
|
|
1491
|
+
delete next[entry.slug];
|
|
1492
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1493
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1494
|
+
onConnectedRef.current?.(entry);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return changed ? next : prev;
|
|
1498
|
+
});
|
|
1499
|
+
} finally {
|
|
1500
|
+
refreshInFlightRef.current = false;
|
|
1501
|
+
}
|
|
1467
1502
|
}, [load]);
|
|
1468
1503
|
useEffect6(() => {
|
|
1469
1504
|
if (gated) return;
|
|
@@ -1484,6 +1519,12 @@ function ConnectorPicker({
|
|
|
1484
1519
|
}, [gated, refreshAfterReturn]);
|
|
1485
1520
|
const handleConnect = useCallback4(
|
|
1486
1521
|
async (entry, kind) => {
|
|
1522
|
+
if (isUnhealthy(entry)) {
|
|
1523
|
+
reconnectingRef.current.add(entry.slug);
|
|
1524
|
+
} else {
|
|
1525
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1526
|
+
}
|
|
1527
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1487
1528
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1488
1529
|
try {
|
|
1489
1530
|
const result = await clientRef.current.connect(entry, { kind });
|
|
@@ -1493,6 +1534,8 @@ function ConnectorPicker({
|
|
|
1493
1534
|
}
|
|
1494
1535
|
const list = await load();
|
|
1495
1536
|
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1537
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1538
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1496
1539
|
setUiState((prev) => {
|
|
1497
1540
|
const next = { ...prev };
|
|
1498
1541
|
delete next[entry.slug];
|
|
@@ -1502,6 +1545,8 @@ function ConnectorPicker({
|
|
|
1502
1545
|
onConnectedRef.current?.(updated);
|
|
1503
1546
|
}
|
|
1504
1547
|
} catch (err) {
|
|
1548
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1549
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1505
1550
|
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1506
1551
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1507
1552
|
}
|
|
@@ -1670,6 +1715,7 @@ function ConnectorTile({
|
|
|
1670
1715
|
const isConnecting = ui?.kind === "connecting";
|
|
1671
1716
|
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1672
1717
|
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1718
|
+
const unhealthy = isUnhealthy(entry);
|
|
1673
1719
|
const [kind, setKind] = useState7("personal");
|
|
1674
1720
|
const [sharingOpen, setSharingOpen] = useState7(false);
|
|
1675
1721
|
const [setupOpen, setSetupOpen] = useState7(false);
|
|
@@ -1719,6 +1765,34 @@ function ConnectorTile({
|
|
|
1719
1765
|
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1720
1766
|
}
|
|
1721
1767
|
),
|
|
1768
|
+
isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ jsxs6(
|
|
1769
|
+
"p",
|
|
1770
|
+
{
|
|
1771
|
+
"data-testid": `connector-account-${entry.slug}`,
|
|
1772
|
+
className: "text-xs text-muted-foreground",
|
|
1773
|
+
children: [
|
|
1774
|
+
"Connected as ",
|
|
1775
|
+
entry.connection.account_label
|
|
1776
|
+
]
|
|
1777
|
+
}
|
|
1778
|
+
),
|
|
1779
|
+
isConnected && entry.access_summary && /* @__PURE__ */ jsx7(
|
|
1780
|
+
"p",
|
|
1781
|
+
{
|
|
1782
|
+
"data-testid": `connector-access-summary-${entry.slug}`,
|
|
1783
|
+
className: "text-xs text-muted-foreground",
|
|
1784
|
+
children: entry.access_summary
|
|
1785
|
+
}
|
|
1786
|
+
),
|
|
1787
|
+
unhealthy && /* @__PURE__ */ jsx7(
|
|
1788
|
+
"p",
|
|
1789
|
+
{
|
|
1790
|
+
role: "alert",
|
|
1791
|
+
"data-testid": `connector-unhealthy-${entry.slug}`,
|
|
1792
|
+
className: "text-xs font-medium text-destructive",
|
|
1793
|
+
children: DEFAULT_RECONNECT_MESSAGE
|
|
1794
|
+
}
|
|
1795
|
+
),
|
|
1722
1796
|
attribution && /* @__PURE__ */ jsx7(
|
|
1723
1797
|
"p",
|
|
1724
1798
|
{
|
|
@@ -1781,17 +1855,46 @@ function ConnectorTile({
|
|
|
1781
1855
|
errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1782
1856
|
] }),
|
|
1783
1857
|
isConnected ? /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1858
|
+
unhealthy && !shared && /* @__PURE__ */ jsx7(
|
|
1859
|
+
"button",
|
|
1860
|
+
{
|
|
1861
|
+
type: "button",
|
|
1862
|
+
"data-testid": `connector-reconnect-${entry.slug}`,
|
|
1863
|
+
onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
|
|
1864
|
+
disabled: isConnecting || isDisconnecting,
|
|
1865
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1866
|
+
children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
|
|
1867
|
+
}
|
|
1868
|
+
),
|
|
1784
1869
|
canDisconnect && /* @__PURE__ */ jsx7(
|
|
1785
1870
|
"button",
|
|
1786
1871
|
{
|
|
1787
1872
|
type: "button",
|
|
1788
1873
|
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1874
|
+
title: DISCONNECT_REVOKES_HINT,
|
|
1789
1875
|
onClick: () => onDisconnect(entry),
|
|
1790
|
-
disabled: isDisconnecting,
|
|
1876
|
+
disabled: isDisconnecting || isConnecting,
|
|
1791
1877
|
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1792
1878
|
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1793
1879
|
}
|
|
1794
1880
|
),
|
|
1881
|
+
canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
|
|
1882
|
+
// paragraph's one long sentence forced the WHOLE shrink-0
|
|
1883
|
+
// button column to grow to its natural (unwrapped) width,
|
|
1884
|
+
// squeezing the sibling `min-w-0 flex-1` text column down to
|
|
1885
|
+
// one word per line at any realistic tile width. RTL's jsdom
|
|
1886
|
+
// tests never caught it — jsdom doesn't lay out real flexbox
|
|
1887
|
+
// sizing, so every textContent assertion still passed. A
|
|
1888
|
+
// narrow max-width forces the hint to wrap within the same
|
|
1889
|
+
// footprint as the button above it instead.
|
|
1890
|
+
/* @__PURE__ */ jsx7(
|
|
1891
|
+
"p",
|
|
1892
|
+
{
|
|
1893
|
+
"data-testid": `connector-disconnect-hint-${entry.slug}`,
|
|
1894
|
+
className: "max-w-28 text-right text-[11px] text-muted-foreground",
|
|
1895
|
+
children: DISCONNECT_REVOKES_HINT
|
|
1896
|
+
}
|
|
1897
|
+
),
|
|
1795
1898
|
canManageSharing && /* @__PURE__ */ jsx7(
|
|
1796
1899
|
"button",
|
|
1797
1900
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetreeve/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Shared brand-adaptive UI primitives for MindFortress frontends — min-size tokens, a favicon→logo→monogram BrandGlyph, a responsive header that keeps the search bar usable on mobile, and AppSidebar's viewport-driven auto-collapse ladder.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "./dist/index.js",
|