refactor(web): resolve Mantine/CSS rule violations in mid-session auth UI
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
This commit is contained in:
parent
f4b6095719
commit
74c6491b52
5 changed files with 173 additions and 155 deletions
|
|
@ -412,3 +412,13 @@
|
|||
.server-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sticky re-auth banner bar. Positioning (pos/top) and background are set via
|
||||
* Mantine props on the Box; z-index and box-shadow have no Mantine style-prop
|
||||
* shorthand, so they live here.
|
||||
*/
|
||||
.reauth-banner-bar {
|
||||
z-index: 200;
|
||||
box-shadow: var(--mantine-shadow-sm);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3434,20 +3434,154 @@ function App() {
|
|||
[pendingElicitations],
|
||||
);
|
||||
|
||||
const handleStepUpAuthorize = async () => {
|
||||
if (!pendingStepUp || stepUpAuthorizeInProgressRef.current) {
|
||||
return;
|
||||
}
|
||||
const stepUp = pendingStepUp;
|
||||
const client = inspectorClient;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepUp.enterpriseManaged) {
|
||||
stepUpAuthorizeInProgressRef.current = true;
|
||||
setPendingStepUp(null);
|
||||
notifications.show({
|
||||
title: "Organization permissions",
|
||||
message: emaStepUpInProgressMessage(),
|
||||
color: "blue",
|
||||
autoClose: 4000,
|
||||
});
|
||||
try {
|
||||
const outcome = await client.handleAuthChallenge(stepUp.challenge, {
|
||||
confirmedStepUp: true,
|
||||
});
|
||||
if (outcome.kind === "satisfied") {
|
||||
await client.pushRemoteAuthState();
|
||||
notifications.show({
|
||||
title: "Permissions updated",
|
||||
message: emaStepUpSuccessMessage({
|
||||
recoverySource: stepUp.source,
|
||||
}),
|
||||
color: "green",
|
||||
autoClose: 5000,
|
||||
});
|
||||
const retry = pendingStepUpRetryRef.current;
|
||||
pendingStepUpRetryRef.current = null;
|
||||
if (retry) {
|
||||
await retry();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (outcome.kind === "interactive") {
|
||||
prepareOAuthRedirect({
|
||||
serverId: stepUp.serverId,
|
||||
authKind: "step_up",
|
||||
authorizationUrl: outcome.authorizationUrl,
|
||||
authChallenge: outcome.challenge,
|
||||
recoverySource: stepUp.source,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (outcome.kind === "failed") {
|
||||
const failureMessage = emaStepUpFailureMessage(outcome.error.message);
|
||||
notifications.show({
|
||||
title: "Organization permissions",
|
||||
message: failureMessage,
|
||||
color: "red",
|
||||
autoClose: 6000,
|
||||
});
|
||||
switch (stepUp.source) {
|
||||
case "tool":
|
||||
setToolCallState({
|
||||
status: "error",
|
||||
error: failureMessage,
|
||||
});
|
||||
break;
|
||||
case "prompt":
|
||||
setGetPromptState((prev) =>
|
||||
prev
|
||||
? { ...prev, status: "error", error: failureMessage }
|
||||
: prev,
|
||||
);
|
||||
break;
|
||||
case "resource":
|
||||
setReadResourceState((prev) =>
|
||||
prev
|
||||
? { ...prev, status: "error", error: failureMessage }
|
||||
: prev,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stepUpAuthorizeInProgressRef.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
stepUpAuthorizeInProgressRef.current = true;
|
||||
prepareOAuthRedirect({
|
||||
serverId: stepUp.serverId,
|
||||
authKind: "step_up",
|
||||
authorizationUrl: stepUp.authorizationUrl,
|
||||
authChallenge: stepUp.challenge,
|
||||
recoverySource: stepUp.source,
|
||||
});
|
||||
setPendingStepUp(null);
|
||||
pendingStepUpRetryRef.current = null;
|
||||
stepUpAuthorizeInProgressRef.current = false;
|
||||
};
|
||||
|
||||
const handleStepUpCancel = () => {
|
||||
const stepUp = pendingStepUpRef.current;
|
||||
setPendingStepUp(null);
|
||||
pendingStepUpRetryRef.current = null;
|
||||
if (!stepUp) {
|
||||
return;
|
||||
}
|
||||
const cancelled = "Authorization cancelled.";
|
||||
switch (stepUp.source) {
|
||||
case "tool":
|
||||
setToolCallState({ status: "error", error: cancelled });
|
||||
break;
|
||||
case "prompt":
|
||||
setGetPromptState((prev) =>
|
||||
prev ? { ...prev, status: "error", error: cancelled } : prev,
|
||||
);
|
||||
break;
|
||||
case "resource":
|
||||
setReadResourceState((prev) =>
|
||||
prev ? { ...prev, status: "error", error: cancelled } : prev,
|
||||
);
|
||||
break;
|
||||
case "app":
|
||||
notifications.show({
|
||||
title: "Authorization cancelled",
|
||||
message: cancelled,
|
||||
color: "gray",
|
||||
autoClose: 4000,
|
||||
});
|
||||
break;
|
||||
case "ambient":
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
{reAuthBanner ? (
|
||||
<Box
|
||||
className="reauth-banner-bar"
|
||||
px="md"
|
||||
pt="xs"
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 60,
|
||||
zIndex: 200,
|
||||
backgroundColor: "var(--mantine-color-body)",
|
||||
boxShadow: "var(--mantine-shadow-sm)",
|
||||
}}
|
||||
pos="sticky"
|
||||
top={60}
|
||||
bg="var(--mantine-color-body)"
|
||||
>
|
||||
<ReAuthBanner
|
||||
message={reAuthBanner.message}
|
||||
|
|
@ -3693,144 +3827,8 @@ function App() {
|
|||
challenge={pendingStepUp?.challenge ?? null}
|
||||
authorizationScopes={pendingStepUp?.challenge?.authorizationScopes}
|
||||
enterpriseManaged={pendingStepUp?.enterpriseManaged}
|
||||
onAuthorize={async () => {
|
||||
if (!pendingStepUp || stepUpAuthorizeInProgressRef.current) {
|
||||
return;
|
||||
}
|
||||
const stepUp = pendingStepUp;
|
||||
const client = inspectorClient;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepUp.enterpriseManaged) {
|
||||
stepUpAuthorizeInProgressRef.current = true;
|
||||
setPendingStepUp(null);
|
||||
notifications.show({
|
||||
title: "Organization permissions",
|
||||
message: emaStepUpInProgressMessage(),
|
||||
color: "blue",
|
||||
autoClose: 4000,
|
||||
});
|
||||
try {
|
||||
const outcome = await client.handleAuthChallenge(
|
||||
stepUp.challenge,
|
||||
{ confirmedStepUp: true },
|
||||
);
|
||||
if (outcome.kind === "satisfied") {
|
||||
await client.pushRemoteAuthState();
|
||||
notifications.show({
|
||||
title: "Permissions updated",
|
||||
message: emaStepUpSuccessMessage({
|
||||
recoverySource: stepUp.source,
|
||||
}),
|
||||
color: "green",
|
||||
autoClose: 5000,
|
||||
});
|
||||
const retry = pendingStepUpRetryRef.current;
|
||||
pendingStepUpRetryRef.current = null;
|
||||
if (retry) {
|
||||
await retry();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (outcome.kind === "interactive") {
|
||||
prepareOAuthRedirect({
|
||||
serverId: stepUp.serverId,
|
||||
authKind: "step_up",
|
||||
authorizationUrl: outcome.authorizationUrl,
|
||||
authChallenge: outcome.challenge,
|
||||
recoverySource: stepUp.source,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (outcome.kind === "failed") {
|
||||
const failureMessage = emaStepUpFailureMessage(
|
||||
outcome.error.message,
|
||||
);
|
||||
notifications.show({
|
||||
title: "Organization permissions",
|
||||
message: failureMessage,
|
||||
color: "red",
|
||||
autoClose: 6000,
|
||||
});
|
||||
switch (stepUp.source) {
|
||||
case "tool":
|
||||
setToolCallState({
|
||||
status: "error",
|
||||
error: failureMessage,
|
||||
});
|
||||
break;
|
||||
case "prompt":
|
||||
setGetPromptState((prev) =>
|
||||
prev
|
||||
? { ...prev, status: "error", error: failureMessage }
|
||||
: prev,
|
||||
);
|
||||
break;
|
||||
case "resource":
|
||||
setReadResourceState((prev) =>
|
||||
prev
|
||||
? { ...prev, status: "error", error: failureMessage }
|
||||
: prev,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stepUpAuthorizeInProgressRef.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
stepUpAuthorizeInProgressRef.current = true;
|
||||
prepareOAuthRedirect({
|
||||
serverId: stepUp.serverId,
|
||||
authKind: "step_up",
|
||||
authorizationUrl: stepUp.authorizationUrl,
|
||||
authChallenge: stepUp.challenge,
|
||||
recoverySource: stepUp.source,
|
||||
});
|
||||
setPendingStepUp(null);
|
||||
pendingStepUpRetryRef.current = null;
|
||||
stepUpAuthorizeInProgressRef.current = false;
|
||||
}}
|
||||
onCancel={() => {
|
||||
const stepUp = pendingStepUpRef.current;
|
||||
setPendingStepUp(null);
|
||||
pendingStepUpRetryRef.current = null;
|
||||
if (!stepUp) {
|
||||
return;
|
||||
}
|
||||
const cancelled = "Authorization cancelled.";
|
||||
switch (stepUp.source) {
|
||||
case "tool":
|
||||
setToolCallState({ status: "error", error: cancelled });
|
||||
break;
|
||||
case "prompt":
|
||||
setGetPromptState((prev) =>
|
||||
prev ? { ...prev, status: "error", error: cancelled } : prev,
|
||||
);
|
||||
break;
|
||||
case "resource":
|
||||
setReadResourceState((prev) =>
|
||||
prev ? { ...prev, status: "error", error: cancelled } : prev,
|
||||
);
|
||||
break;
|
||||
case "app":
|
||||
notifications.show({
|
||||
title: "Authorization cancelled",
|
||||
message: cancelled,
|
||||
color: "gray",
|
||||
autoClose: 4000,
|
||||
});
|
||||
break;
|
||||
case "ambient":
|
||||
break;
|
||||
}
|
||||
}}
|
||||
onAuthorize={handleStepUpAuthorize}
|
||||
onCancel={handleStepUpCancel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Alert, Button, Group } from "@mantine/core";
|
||||
import { Alert, Button, Group, Text } from "@mantine/core";
|
||||
|
||||
export interface ReAuthBannerProps {
|
||||
message: string;
|
||||
|
|
@ -14,19 +14,15 @@ export function ReAuthBanner({
|
|||
return (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
variant="reauth"
|
||||
title="Re-authentication required"
|
||||
withCloseButton
|
||||
onClose={onDismiss}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: "var(--mantine-color-body)",
|
||||
border: "1px solid var(--mantine-color-red-3)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap" gap="md">
|
||||
<span>{message}</span>
|
||||
<Text component="span" size="sm">
|
||||
{message}
|
||||
</Text>
|
||||
<Button size="xs" variant="filled" onClick={onReauthenticate}>
|
||||
Re-authenticate
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export function StepUpAuthModal({
|
|||
}: StepUpAuthModalProps) {
|
||||
const additionalScopes = challenge ? stepUpAdditionalScopes(challenge) : [];
|
||||
const ema = enterpriseManaged === true;
|
||||
const handleAuthorize = () => void onAuthorize();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -79,7 +80,7 @@ export function StepUpAuthModal({
|
|||
<Button variant="default" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => void onAuthorize()}>Authorize</Button>
|
||||
<Button onClick={handleAuthorize}>Authorize</Button>
|
||||
</Actions>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -4,4 +4,17 @@ export const ThemeAlert = Alert.extend({
|
|||
defaultProps: {
|
||||
radius: "md",
|
||||
},
|
||||
styles: (_theme, props) => {
|
||||
// Re-auth banner: body-colored surface with a red hairline border, used
|
||||
// for the persistent mid-session re-authentication banner.
|
||||
if (props.variant === "reauth") {
|
||||
return {
|
||||
root: {
|
||||
backgroundColor: "var(--mantine-color-body)",
|
||||
border: "1px solid var(--mantine-color-red-3)",
|
||||
},
|
||||
};
|
||||
}
|
||||
return { root: {} };
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue