(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;
readonly onRemove: () => void;
})
| 110 | }; |
| 111 | |
| 112 | function AccountRow(props: { |
| 113 | readonly connection: Connection; |
| 114 | /** The integration declares scopes this connection was not granted — it must |
| 115 | * reconnect to grant the newly-needed access (e.g. after a service was added). */ |
| 116 | readonly needsReconsent: boolean; |
| 117 | readonly showOwnerLabel: boolean; |
| 118 | readonly onEdit: () => void; |
| 119 | readonly onReconnect: () => void; |
| 120 | readonly onRemove: () => void; |
| 121 | }) { |
| 122 | const { connection, needsReconsent } = props; |
| 123 | const [checking, setChecking] = useState(false); |
| 124 | |
| 125 | // The status renders WITHOUT any clicking: every checkHealth run persists its |
| 126 | // verdict on the connection row, so the list answers "has this expired?" at a |
| 127 | // glance. A live check from this session takes precedence. We deliberately do |
| 128 | // NOT derive expiry from the stored `expiresAt`: that's the access-token |
| 129 | // lifetime, which refreshes, so a passive countdown means nothing. |
| 130 | // |
| 131 | // Health checks are AUTOMATIC: the hook revalidates on mount, stale-while- |
| 132 | // revalidate style (shared with the integrations-list summary), and the |
| 133 | // persisted verdict renders instantly while the probe corrects it in place. |
| 134 | const { probe, status, runCheck } = useConnectionHealth(connection); |
| 135 | const indicator = HEALTH_INDICATOR_COLOR[status]; |
| 136 | |
| 137 | // Prefer the stored label from the connection row, then a probed identity, |
| 138 | // then the connection name. OAuth labels come from the grant's OIDC claims, |
| 139 | // while health identities remain useful for non-OAuth probes. |
| 140 | const identity = |
| 141 | (connection.identityLabel && connection.identityLabel.length > 0 |
| 142 | ? connection.identityLabel |
| 143 | : null) ?? (probe?.identity && probe.identity.length > 0 ? probe.identity : null); |
| 144 | const displayLabel = identity ?? String(connection.name); |
| 145 | |
| 146 | const expired = status === "expired"; |
| 147 | // `misconfigured` is deliberately NOT folded into `needsHealthAttention`: it |
| 148 | // gets its own amber "API disabled" badge and its own link-rendered detail |
| 149 | // below, because the remediation is a console visit, not a reconnect. |
| 150 | const misconfigured = status === "misconfigured"; |
| 151 | const needsHealthAttention = status === "expired" || status === "degraded"; |
| 152 | const healthDetail = needsHealthAttention ? probe?.detail : undefined; |
| 153 | const missingOAuthScopes = connection.missingOAuthScopes ?? []; |
| 154 | |
| 155 | const handleCheck = async () => { |
| 156 | if (checking) return; |
| 157 | setChecking(true); |
| 158 | const exit = await runCheck(); |
| 159 | setChecking(false); |
| 160 | if (Exit.isFailure(exit)) { |
| 161 | toast.error(messageFromExit(exit, "Health check failed")); |
| 162 | return; |
| 163 | } |
| 164 | // The hook already folded the fresh probe into the live state. |
| 165 | if (exit.value.status === "healthy") { |
| 166 | toast.success( |
| 167 | exit.value.identity ? `Healthy: ${exit.value.identity}` : "Connection is healthy", |
| 168 | ); |
| 169 | } else if (exit.value.status === "expired") { |
nothing calls this directly
no test coverage detected