Show bridge errors in connector cards

Display SCIM bridge state directly on Google Workspace and Microsoft
365 connector cards so admins can quickly identify failing bridges.

When a bridge is failed, render an inline error callout and surface the
latest provisioning event error message to make troubleshooting visible
without leaving the connector section.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-05-27 23:33:41 +00:00
committed by Bryan Frimin
parent 4d4f4fa526
commit f02c3b88b7
2 changed files with 306 additions and 220 deletions

View File

@@ -23,6 +23,7 @@ import {
DialogFooter, DialogFooter,
Google, Google,
IconSettingsGear2, IconSettingsGear2,
IconWarning,
Input, Input,
useDialogRef, useDialogRef,
useToast, useToast,
@@ -38,9 +39,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
const googleWorkspaceConnectorFragment = graphql` const googleWorkspaceConnectorFragment = graphql`
fragment GoogleWorkspaceConnectorFragment on SCIMConfiguration { fragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {
id id
events(first: 1) {
edges {
node {
id
errorMessage
}
}
}
bridge { bridge {
id id
type type
state
excludedUserNames excludedUserNames
connector { connector {
id id
@@ -83,9 +93,26 @@ export function GoogleWorkspaceConnector(props: {
const connector = bridge?.connector; const connector = bridge?.connector;
const scimConfigurationId = data?.id; const scimConfigurationId = data?.id;
const bridgeId = bridge?.id; const bridgeId = bridge?.id;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const bridgeState = bridge?.state ?? null;
const latestBridgeError = data?.events?.edges?.[0]?.node?.errorMessage ?? null;
const isBridgeFailed = bridgeState === "FAILED";
const isBridgePending = bridgeState === "PENDING";
const bridgeStatusBadgeVariant = isBridgeFailed
? "danger"
: isBridgePending
? "warning"
: "success";
const bridgeStatusLabel = isBridgeFailed
? __("Error")
: isBridgePending
? __("Syncing")
: __("Connected");
const bridgeErrorMessage = latestBridgeError
?? __(
"The bridge is currently failing to sync. Check the provisioning event history for more details.",
);
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const excludedUserNamesDialogRef = useDialogRef(); const excludedUserNamesDialogRef = useDialogRef();
@@ -225,118 +252,134 @@ export function GoogleWorkspaceConnector(props: {
// Connected state // Connected state
return ( return (
<Card padded className="flex items-center gap-3"> <Card padded className="space-y-3">
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded"> <div className="flex items-center gap-3">
<Google className="w-6 h-6" /> <div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
</div> <Google className="w-6 h-6" />
<div className="mr-auto"> </div>
<h3 className="font-medium">{__("Google Workspace / Cloud Identity")}</h3> <div className="mr-auto">
<p className="text-sm text-txt-secondary"> <h3 className="font-medium">{__("Google Workspace / Cloud Identity")}</h3>
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))} <p className="text-sm text-txt-secondary">
</p> {sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
</div> </p>
<Badge variant="success" size="md"> </div>
{__("Connected")} <Badge variant={bridgeStatusBadgeVariant} size="md">
</Badge> {bridgeStatusLabel}
<Dialog </Badge>
ref={excludedUserNamesDialogRef} <Dialog
trigger={( ref={excludedUserNamesDialogRef}
<Button variant="secondary"> trigger={(
<IconSettingsGear2 size={16} /> <Button variant="secondary">
{__("Settings")} <IconSettingsGear2 size={16} />
</Button> {__("Settings")}
)} </Button>
title={__("Google Workspace / Cloud Identity Settings")} )}
className="max-w-lg" title={__("Google Workspace / Cloud Identity Settings")}
> className="max-w-lg"
<DialogContent padded className="space-y-6"> >
<div className="space-y-4"> <DialogContent padded className="space-y-6">
<div> <div className="space-y-4">
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4> <div>
<p className="text-sm text-txt-secondary mt-1"> <h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
{__("Users with these user names will not be synced from Google Workspace / Cloud Identity.")} <p className="text-sm text-txt-secondary mt-1">
</p> {__("Users with these user names will not be synced from Google Workspace / Cloud Identity.")}
</div> </p>
<div className="flex gap-2"> </div>
<Input <div className="flex gap-2">
type="text" <Input
value={newUser} type="text"
onChange={e => setNewUser(e.target.value)} value={newUser}
onKeyDown={(e) => { onChange={e => setNewUser(e.target.value)}
if (e.key === "Enter") { onKeyDown={(e) => {
e.preventDefault(); if (e.key === "Enter") {
if (isUpdating) return; e.preventDefault();
handleAddUser(); if (isUpdating) return;
} handleAddUser();
}} }
placeholder="user@example.com" }}
className="flex-1" placeholder="user@example.com"
/> className="flex-1"
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}> />
{__("Add")} <Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
</Button> {__("Add")}
</div> </Button>
{currentExcludedUserNames.length > 0 && (
<div className="space-y-2">
{currentExcludedUserNames.map((user: string) => (
<div
key={user}
className="flex items-center justify-between p-2 bg-subtle rounded"
>
<span className="text-sm">{user}</span>
<Button
variant="quaternary"
onClick={() => handleRemoveUser(user)}
disabled={isUpdating}
>
{__("Remove")}
</Button>
</div>
))}
</div> </div>
)}
{currentExcludedUserNames.length === 0 && ( {currentExcludedUserNames.length > 0 && (
<p className="text-sm text-txt-secondary text-center py-4"> <div className="space-y-2">
{__("No excluded user names. All Google Workspace / Cloud Identity users will be synced.")} {currentExcludedUserNames.map((user: string) => (
</p> <div
)} key={user}
className="flex items-center justify-between p-2 bg-subtle rounded"
>
<span className="text-sm">{user}</span>
<Button
variant="quaternary"
onClick={() => handleRemoveUser(user)}
disabled={isUpdating}
>
{__("Remove")}
</Button>
</div>
))}
</div>
)}
{currentExcludedUserNames.length === 0 && (
<p className="text-sm text-txt-secondary text-center py-4">
{__("No excluded user names. All Google Workspace / Cloud Identity users will be synced.")}
</p>
)}
</div>
</DialogContent>
</Dialog>
<Dialog
ref={dialogRef}
trigger={(
<Button variant="danger">
{__("Disconnect")}
</Button>
)}
title={__("Disconnect Google Workspace / Cloud Identity")}
className="max-w-lg"
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{__(
"This will disconnect your Google Workspace / Cloud Identity integration. Users will no longer be automatically synced via SCIM.",
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDisconnect}
disabled={isDeleting}
>
{isDeleting
? __("Disconnecting...")
: __("Disconnect")}
</Button>
</DialogFooter>
</Dialog>
</div>
{isBridgeFailed && (
<div className="flex items-start gap-2 rounded-lg bg-bg-warning/10 border border-border-warning p-3">
<IconWarning size={16} className="text-txt-danger shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium text-txt-danger">
{__("Bridge is in an error state")}
</p>
<p className="text-sm text-txt-secondary whitespace-pre-wrap break-all">
{bridgeErrorMessage}
</p>
</div> </div>
</DialogContent> </div>
</Dialog> )}
<Dialog
ref={dialogRef}
trigger={(
<Button variant="danger">
{__("Disconnect")}
</Button>
)}
title={__("Disconnect Google Workspace / Cloud Identity")}
className="max-w-lg"
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{__(
"This will disconnect your Google Workspace / Cloud Identity integration. Users will no longer be automatically synced via SCIM.",
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDisconnect}
disabled={isDeleting}
>
{isDeleting
? __("Disconnecting...")
: __("Disconnect")}
</Button>
</DialogFooter>
</Dialog>
</Card> </Card>
); );
} }

View File

@@ -22,6 +22,7 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
IconSettingsGear2, IconSettingsGear2,
IconWarning,
Input, Input,
Microsoft, Microsoft,
useDialogRef, useDialogRef,
@@ -38,9 +39,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
const microsoft365ConnectorFragment = graphql` const microsoft365ConnectorFragment = graphql`
fragment Microsoft365ConnectorFragment on SCIMConfiguration { fragment Microsoft365ConnectorFragment on SCIMConfiguration {
id id
events(first: 1) {
edges {
node {
id
errorMessage
}
}
}
bridge { bridge {
id id
type type
state
excludedUserNames excludedUserNames
connector { connector {
id id
@@ -83,9 +93,26 @@ export function Microsoft365Connector(props: {
const connector = bridge?.connector; const connector = bridge?.connector;
const scimConfigurationId = data?.id; const scimConfigurationId = data?.id;
const bridgeId = bridge?.id; const bridgeId = bridge?.id;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const bridgeState = bridge?.state ?? null;
const latestBridgeError = data?.events?.edges?.[0]?.node?.errorMessage ?? null;
const isBridgeFailed = bridgeState === "FAILED";
const isBridgePending = bridgeState === "PENDING";
const bridgeStatusBadgeVariant = isBridgeFailed
? "danger"
: isBridgePending
? "warning"
: "success";
const bridgeStatusLabel = isBridgeFailed
? __("Error")
: isBridgePending
? __("Syncing")
: __("Connected");
const bridgeErrorMessage = latestBridgeError
?? __(
"The bridge is currently failing to sync. Check the provisioning event history for more details.",
);
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const excludedUserNamesDialogRef = useDialogRef(); const excludedUserNamesDialogRef = useDialogRef();
@@ -223,118 +250,134 @@ export function Microsoft365Connector(props: {
} }
return ( return (
<Card padded className="flex items-center gap-3"> <Card padded className="space-y-3">
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded"> <div className="flex items-center gap-3">
<Microsoft className="w-6 h-6" /> <div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
</div> <Microsoft className="w-6 h-6" />
<div className="mr-auto"> </div>
<h3 className="font-medium">{__("Microsoft 365")}</h3> <div className="mr-auto">
<p className="text-sm text-txt-secondary"> <h3 className="font-medium">{__("Microsoft 365")}</h3>
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))} <p className="text-sm text-txt-secondary">
</p> {sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
</div> </p>
<Badge variant="success" size="md"> </div>
{__("Connected")} <Badge variant={bridgeStatusBadgeVariant} size="md">
</Badge> {bridgeStatusLabel}
<Dialog </Badge>
ref={excludedUserNamesDialogRef} <Dialog
trigger={( ref={excludedUserNamesDialogRef}
<Button variant="secondary"> trigger={(
<IconSettingsGear2 size={16} /> <Button variant="secondary">
{__("Settings")} <IconSettingsGear2 size={16} />
</Button> {__("Settings")}
)} </Button>
title={__("Microsoft 365 Settings")} )}
className="max-w-lg" title={__("Microsoft 365 Settings")}
> className="max-w-lg"
<DialogContent padded className="space-y-6"> >
<div className="space-y-4"> <DialogContent padded className="space-y-6">
<div> <div className="space-y-4">
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4> <div>
<p className="text-sm text-txt-secondary mt-1"> <h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
{__("Users with these user names will not be synced from Microsoft 365.")} <p className="text-sm text-txt-secondary mt-1">
</p> {__("Users with these user names will not be synced from Microsoft 365.")}
</div> </p>
<div className="flex gap-2"> </div>
<Input <div className="flex gap-2">
type="text" <Input
value={newUser} type="text"
onChange={e => setNewUser(e.target.value)} value={newUser}
onKeyDown={(e) => { onChange={e => setNewUser(e.target.value)}
if (e.key === "Enter") { onKeyDown={(e) => {
e.preventDefault(); if (e.key === "Enter") {
if (isUpdating) return; e.preventDefault();
handleAddUser(); if (isUpdating) return;
} handleAddUser();
}} }
placeholder="user@example.com" }}
className="flex-1" placeholder="user@example.com"
/> className="flex-1"
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}> />
{__("Add")} <Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
</Button> {__("Add")}
</div> </Button>
{currentExcludedUserNames.length > 0 && (
<div className="space-y-2">
{currentExcludedUserNames.map((user: string) => (
<div
key={user}
className="flex items-center justify-between p-2 bg-subtle rounded"
>
<span className="text-sm">{user}</span>
<Button
variant="quaternary"
onClick={() => handleRemoveUser(user)}
disabled={isUpdating}
>
{__("Remove")}
</Button>
</div>
))}
</div> </div>
)}
{currentExcludedUserNames.length === 0 && ( {currentExcludedUserNames.length > 0 && (
<p className="text-sm text-txt-secondary text-center py-4"> <div className="space-y-2">
{__("No excluded user names. All Microsoft 365 users will be synced.")} {currentExcludedUserNames.map((user: string) => (
</p> <div
)} key={user}
className="flex items-center justify-between p-2 bg-subtle rounded"
>
<span className="text-sm">{user}</span>
<Button
variant="quaternary"
onClick={() => handleRemoveUser(user)}
disabled={isUpdating}
>
{__("Remove")}
</Button>
</div>
))}
</div>
)}
{currentExcludedUserNames.length === 0 && (
<p className="text-sm text-txt-secondary text-center py-4">
{__("No excluded user names. All Microsoft 365 users will be synced.")}
</p>
)}
</div>
</DialogContent>
</Dialog>
<Dialog
ref={dialogRef}
trigger={(
<Button variant="danger">
{__("Disconnect")}
</Button>
)}
title={__("Disconnect Microsoft 365")}
className="max-w-lg"
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{__(
"This will disconnect your Microsoft 365 integration. Users will no longer be automatically synced via SCIM.",
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDisconnect}
disabled={isDeleting}
>
{isDeleting
? __("Disconnecting...")
: __("Disconnect")}
</Button>
</DialogFooter>
</Dialog>
</div>
{isBridgeFailed && (
<div className="flex items-start gap-2 rounded-lg bg-bg-warning/10 border border-border-warning p-3">
<IconWarning size={16} className="text-txt-danger shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium text-txt-danger">
{__("Bridge is in an error state")}
</p>
<p className="text-sm text-txt-secondary whitespace-pre-wrap break-all">
{bridgeErrorMessage}
</p>
</div> </div>
</DialogContent> </div>
</Dialog> )}
<Dialog
ref={dialogRef}
trigger={(
<Button variant="danger">
{__("Disconnect")}
</Button>
)}
title={__("Disconnect Microsoft 365")}
className="max-w-lg"
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{__(
"This will disconnect your Microsoft 365 integration. Users will no longer be automatically synced via SCIM.",
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDisconnect}
disabled={isDeleting}
>
{isDeleting
? __("Disconnecting...")
: __("Disconnect")}
</Button>
</DialogFooter>
</Dialog>
</Card> </Card>
); );
} }