(props: {
readonly connection: Connection;
/** The integration declares scopes this connection was not granted — it must
* reconnect to grant the newly-needed access (e.g. after a service was added). */
readonly needsReconsent: boolean;
readonly showOwnerLabel: boolean;
readonly onEdit: () => void;
readonly onReconnect: () => void;
/** Reconnect routing needs the stored client binding; while the client
* summaries carry no data the route is unknown, so the action is
* disabled rather than guessed (same idiom as "Check now" above). */
readonly reconnectDisabled: boolean;
/** The summaries query failed: the Reconnect item stays disabled but says
* so (never a silently dead action). Retained stale data never routes — a
* binding changed since the snapshot could misroute. Opening the menu
* retries the query via `onMenuOpenChange`, so the hint reflects a retry
* that just failed, not a permanently stuck state. */
readonly reconnectFailed: boolean;
/** Forwarded to the row menu; the owner uses the OPEN transition to retry
* a failed client-summaries query. */
readonly onMenuOpenChange: (open: boolean) => void;
readonly onRemove: () => void;
})
| 119 | }; |
| 120 | |
| 121 | function AccountRow(props: { |
| 122 | readonly connection: Connection; |
| 123 | /** The integration declares scopes this connection was not granted — it must |
| 124 | * reconnect to grant the newly-needed access (e.g. after a service was added). */ |
| 125 | readonly needsReconsent: boolean; |
| 126 | readonly showOwnerLabel: boolean; |
| 127 | readonly onEdit: () => void; |
| 128 | readonly onReconnect: () => void; |
| 129 | /** Reconnect routing needs the stored client binding; while the client |
| 130 | * summaries carry no data the route is unknown, so the action is |
| 131 | * disabled rather than guessed (same idiom as "Check now" above). */ |
| 132 | readonly reconnectDisabled: boolean; |
| 133 | /** The summaries query failed: the Reconnect item stays disabled but says |
| 134 | * so (never a silently dead action). Retained stale data never routes — a |
| 135 | * binding changed since the snapshot could misroute. Opening the menu |
| 136 | * retries the query via `onMenuOpenChange`, so the hint reflects a retry |
| 137 | * that just failed, not a permanently stuck state. */ |
| 138 | readonly reconnectFailed: boolean; |
| 139 | /** Forwarded to the row menu; the owner uses the OPEN transition to retry |
| 140 | * a failed client-summaries query. */ |
| 141 | readonly onMenuOpenChange: (open: boolean) => void; |
| 142 | readonly onRemove: () => void; |
| 143 | }) { |
| 144 | const { connection, needsReconsent } = props; |
| 145 | const [checking, setChecking] = useState(false); |
| 146 | |
| 147 | // The status renders WITHOUT any clicking: every checkHealth run persists its |
| 148 | // verdict on the connection row, so the list answers "has this expired?" at a |
| 149 | // glance. A live check from this session takes precedence. We deliberately do |
| 150 | // NOT derive expiry from the stored `expiresAt`: that's the access-token |
| 151 | // lifetime, which refreshes, so a passive countdown means nothing. |
| 152 | // |
| 153 | // Health checks are AUTOMATIC: the hook revalidates on mount, stale-while- |
| 154 | // revalidate style (shared with the integrations-list summary), and the |
| 155 | // persisted verdict renders instantly while the probe corrects it in place. |
| 156 | const { probe, status, runCheck } = useConnectionHealth(connection); |
| 157 | const indicator = HEALTH_INDICATOR_COLOR[status]; |
| 158 | |
| 159 | // Prefer the stored label from the connection row, then a probed identity, |
| 160 | // then the connection name. OAuth labels come from the grant's OIDC claims, |
| 161 | // while health identities remain useful for non-OAuth probes. |
| 162 | const identity = |
| 163 | (connection.identityLabel && connection.identityLabel.length > 0 |
| 164 | ? connection.identityLabel |
| 165 | : null) ?? (probe?.identity && probe.identity.length > 0 ? probe.identity : null); |
| 166 | const displayLabel = identity ?? String(connection.name); |
| 167 | |
| 168 | const expired = status === "expired"; |
| 169 | // `misconfigured` is deliberately NOT folded into `needsHealthAttention`: it |
| 170 | // gets its own amber "API disabled" badge and its own link-rendered detail |
| 171 | // below, because the remediation is a console visit, not a reconnect. |
| 172 | const misconfigured = status === "misconfigured"; |
| 173 | const needsHealthAttention = status === "expired" || status === "degraded"; |
| 174 | const healthDetail = needsHealthAttention ? probe?.detail : undefined; |
| 175 | const missingOAuthScopes = connection.missingOAuthScopes ?? []; |
| 176 | |
| 177 | const handleCheck = async () => { |
| 178 | if (checking) return; |
nothing calls this directly
no test coverage detected