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:
committed by
Bryan Frimin
parent
4d4f4fa526
commit
f02c3b88b7
@@ -23,6 +23,7 @@ import {
|
||||
DialogFooter,
|
||||
Google,
|
||||
IconSettingsGear2,
|
||||
IconWarning,
|
||||
Input,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
@@ -38,9 +39,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
const googleWorkspaceConnectorFragment = graphql`
|
||||
fragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {
|
||||
id
|
||||
events(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
bridge {
|
||||
id
|
||||
type
|
||||
state
|
||||
excludedUserNames
|
||||
connector {
|
||||
id
|
||||
@@ -83,9 +93,26 @@ export function GoogleWorkspaceConnector(props: {
|
||||
const connector = bridge?.connector;
|
||||
const scimConfigurationId = data?.id;
|
||||
const bridgeId = bridge?.id;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
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 dialogRef = useDialogRef();
|
||||
const excludedUserNamesDialogRef = useDialogRef();
|
||||
@@ -225,118 +252,134 @@ export function GoogleWorkspaceConnector(props: {
|
||||
|
||||
// Connected state
|
||||
return (
|
||||
<Card padded className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
|
||||
<Google className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="mr-auto">
|
||||
<h3 className="font-medium">{__("Google Workspace / Cloud Identity")}</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="success" size="md">
|
||||
{__("Connected")}
|
||||
</Badge>
|
||||
<Dialog
|
||||
ref={excludedUserNamesDialogRef}
|
||||
trigger={(
|
||||
<Button variant="secondary">
|
||||
<IconSettingsGear2 size={16} />
|
||||
{__("Settings")}
|
||||
</Button>
|
||||
)}
|
||||
title={__("Google Workspace / Cloud Identity Settings")}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
|
||||
<p className="text-sm text-txt-secondary mt-1">
|
||||
{__("Users with these user names will not be synced from Google Workspace / Cloud Identity.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={newUser}
|
||||
onChange={e => setNewUser(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (isUpdating) return;
|
||||
handleAddUser();
|
||||
}
|
||||
}}
|
||||
placeholder="user@example.com"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
|
||||
{__("Add")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
))}
|
||||
<Card padded className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
|
||||
<Google className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="mr-auto">
|
||||
<h3 className="font-medium">{__("Google Workspace / Cloud Identity")}</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={bridgeStatusBadgeVariant} size="md">
|
||||
{bridgeStatusLabel}
|
||||
</Badge>
|
||||
<Dialog
|
||||
ref={excludedUserNamesDialogRef}
|
||||
trigger={(
|
||||
<Button variant="secondary">
|
||||
<IconSettingsGear2 size={16} />
|
||||
{__("Settings")}
|
||||
</Button>
|
||||
)}
|
||||
title={__("Google Workspace / Cloud Identity Settings")}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
|
||||
<p className="text-sm text-txt-secondary mt-1">
|
||||
{__("Users with these user names will not be synced from Google Workspace / Cloud Identity.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={newUser}
|
||||
onChange={e => setNewUser(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (isUpdating) return;
|
||||
handleAddUser();
|
||||
}
|
||||
}}
|
||||
placeholder="user@example.com"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
|
||||
{__("Add")}
|
||||
</Button>
|
||||
</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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
IconSettingsGear2,
|
||||
IconWarning,
|
||||
Input,
|
||||
Microsoft,
|
||||
useDialogRef,
|
||||
@@ -38,9 +39,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
const microsoft365ConnectorFragment = graphql`
|
||||
fragment Microsoft365ConnectorFragment on SCIMConfiguration {
|
||||
id
|
||||
events(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
bridge {
|
||||
id
|
||||
type
|
||||
state
|
||||
excludedUserNames
|
||||
connector {
|
||||
id
|
||||
@@ -83,9 +93,26 @@ export function Microsoft365Connector(props: {
|
||||
const connector = bridge?.connector;
|
||||
const scimConfigurationId = data?.id;
|
||||
const bridgeId = bridge?.id;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
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 dialogRef = useDialogRef();
|
||||
const excludedUserNamesDialogRef = useDialogRef();
|
||||
@@ -223,118 +250,134 @@ export function Microsoft365Connector(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card padded className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
|
||||
<Microsoft className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="mr-auto">
|
||||
<h3 className="font-medium">{__("Microsoft 365")}</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="success" size="md">
|
||||
{__("Connected")}
|
||||
</Badge>
|
||||
<Dialog
|
||||
ref={excludedUserNamesDialogRef}
|
||||
trigger={(
|
||||
<Button variant="secondary">
|
||||
<IconSettingsGear2 size={16} />
|
||||
{__("Settings")}
|
||||
</Button>
|
||||
)}
|
||||
title={__("Microsoft 365 Settings")}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
|
||||
<p className="text-sm text-txt-secondary mt-1">
|
||||
{__("Users with these user names will not be synced from Microsoft 365.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={newUser}
|
||||
onChange={e => setNewUser(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (isUpdating) return;
|
||||
handleAddUser();
|
||||
}
|
||||
}}
|
||||
placeholder="user@example.com"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
|
||||
{__("Add")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
))}
|
||||
<Card padded className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-subtle rounded">
|
||||
<Microsoft className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="mr-auto">
|
||||
<h3 className="font-medium">{__("Microsoft 365")}</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{sprintf(__("Connected on %s"), dateTimeFormat(connector.createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={bridgeStatusBadgeVariant} size="md">
|
||||
{bridgeStatusLabel}
|
||||
</Badge>
|
||||
<Dialog
|
||||
ref={excludedUserNamesDialogRef}
|
||||
trigger={(
|
||||
<Button variant="secondary">
|
||||
<IconSettingsGear2 size={16} />
|
||||
{__("Settings")}
|
||||
</Button>
|
||||
)}
|
||||
title={__("Microsoft 365 Settings")}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
|
||||
<p className="text-sm text-txt-secondary mt-1">
|
||||
{__("Users with these user names will not be synced from Microsoft 365.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={newUser}
|
||||
onChange={e => setNewUser(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (isUpdating) return;
|
||||
handleAddUser();
|
||||
}
|
||||
}}
|
||||
placeholder="user@example.com"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
|
||||
{__("Add")}
|
||||
</Button>
|
||||
</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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user