Merge branch 'v2/main' into v2/feat/2068-refresh-token-toggle
This commit is contained in:
commit
40cfb51f70
3 changed files with 493 additions and 43 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Alert, Button, Group, Text } from "@mantine/core";
|
||||
import { Alert, Button, Group, Stack, Text } from "@mantine/core";
|
||||
|
||||
export const DEFAULT_REAUTH_BANNER_TITLE = "Re-authentication required";
|
||||
export const DEFAULT_REAUTH_BANNER_ACTION_LABEL = "Re-authenticate";
|
||||
|
|
@ -24,11 +24,16 @@ const ReAuthAlert = Alert.withProps({
|
|||
closeButtonLabel: "Dismiss",
|
||||
});
|
||||
|
||||
const BannerRow = Group.withProps({
|
||||
justify: "space-between",
|
||||
align: "center",
|
||||
wrap: "nowrap",
|
||||
gap: "md",
|
||||
// Message above the action rather than beside it: this renders in a ~420px
|
||||
// popup, where a side-by-side row squeezes the message into a few words per
|
||||
// line while the button keeps its full width.
|
||||
const BannerBody = Stack.withProps({
|
||||
gap: "xs",
|
||||
});
|
||||
|
||||
const BannerActions = Group.withProps({
|
||||
justify: "flex-end",
|
||||
gap: "xs",
|
||||
});
|
||||
|
||||
const MessageText = Text.withProps({
|
||||
|
|
@ -50,10 +55,12 @@ export function ReAuthBanner({
|
|||
}: ReAuthBannerProps) {
|
||||
return (
|
||||
<ReAuthAlert title={title} onClose={onDismiss}>
|
||||
<BannerRow>
|
||||
<BannerBody>
|
||||
<MessageText>{message}</MessageText>
|
||||
<ReAuthButton onClick={onReauthenticate}>{actionLabel}</ReAuthButton>
|
||||
</BannerRow>
|
||||
<BannerActions>
|
||||
<ReAuthButton onClick={onReauthenticate}>{actionLabel}</ReAuthButton>
|
||||
</BannerActions>
|
||||
</BannerBody>
|
||||
</ReAuthAlert>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1689,6 +1689,87 @@ describe("InspectorView", () => {
|
|||
expect(screen.queryByRole("radio", { name: "Console" })).toBeNull();
|
||||
});
|
||||
|
||||
// An OAuth failure is torn down by the parent, so the session rests at
|
||||
// "disconnected" rather than "error" — the status gate the failure column
|
||||
// used to carry never saw it, and the requests explaining the failure were
|
||||
// unreachable (#2108). The parent's `erroredServerId` is the signal instead.
|
||||
it("opens the monitoring sidebar to Network when OAuth fails without an error status (#2108)", async () => {
|
||||
const { rerender } = renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
activeServer: "beta",
|
||||
connectionStatus: "connecting",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeNull();
|
||||
|
||||
rerender(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
// The failure's teardown clears the active server and settles the
|
||||
// session at "disconnected" — never "error".
|
||||
activeServer: undefined,
|
||||
erroredServerId: "beta",
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
network: [
|
||||
{
|
||||
id: "a1",
|
||||
timestamp: new Date(),
|
||||
method: "GET",
|
||||
url: "https://as.example.com/.well-known/oauth-authorization-server",
|
||||
requestHeaders: {},
|
||||
responseStatus: 404,
|
||||
responseStatusText: "Not Found",
|
||||
category: "auth",
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: "Network" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("does not re-open the sidebar on a mount that starts already flagged (#2108)", () => {
|
||||
// Same props as the failure above, but as the *initial* render: there is
|
||||
// no transition, so a user who closed the column isn't fought on remount.
|
||||
renderWithMantine(
|
||||
<StatefulInspectorViewHost
|
||||
{...makeProps({
|
||||
servers: [httpServer],
|
||||
activeServer: undefined,
|
||||
erroredServerId: "beta",
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
network: [
|
||||
{
|
||||
id: "a1",
|
||||
timestamp: new Date(),
|
||||
method: "GET",
|
||||
url: "https://as.example.com/.well-known/oauth-authorization-server",
|
||||
requestHeaders: {},
|
||||
responseStatus: 404,
|
||||
responseStatusText: "Not Found",
|
||||
category: "auth",
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces Console (stderr), not Network, in the failure column for a stdio server (#1621)", async () => {
|
||||
const stdioErr: ServerEntry = {
|
||||
id: "beta",
|
||||
|
|
|
|||
|
|
@ -388,6 +388,13 @@ export interface InspectorViewProps {
|
|||
* the Servers screen draws a red border until another server is connected or
|
||||
* a new connection is attempted. Independent of `activeServer`, which the
|
||||
* parent clears on the failure's `disconnect` event.
|
||||
*
|
||||
* It is also the sole signal that opens the monitoring sidebar onto the
|
||||
* failure's diagnostics (#2108). That used to be gated on the `"error"`
|
||||
* connection status, which an OAuth failure never reaches — the parent drives
|
||||
* that leg and tears the client down itself, settling the session at
|
||||
* `"disconnected"`. So the parent must set this for *every* connect-attempt
|
||||
* failure, auth legs included, and clear it as each new attempt starts.
|
||||
*/
|
||||
erroredServerId?: string;
|
||||
/**
|
||||
|
|
@ -817,18 +824,33 @@ export function InspectorView({
|
|||
// connecting/disconnected → error, never connected → error. This keeps the
|
||||
// auto-open aligned with the red-border (`erroredServerId`), which the parent
|
||||
// also sets only for connect-attempt failures.
|
||||
//
|
||||
// A connect attempt that fails *during OAuth* (#2108) never reaches the
|
||||
// `"error"` status at all: the auth leg is driven from the parent, which
|
||||
// tears the client down on failure, so the session settles at
|
||||
// `"disconnected"`. So the failure arm also fires on the parent raising
|
||||
// `erroredServerId` — the canonical "this server's last connect attempt
|
||||
// failed" flag, which the parent clears at the start of every new attempt.
|
||||
// Tracked through a ref for the same reason as the status: a mount that
|
||||
// starts already flagged must not re-open a column the user closed.
|
||||
const prevStatusRef = useRef(connectionStatus);
|
||||
const prevErroredServerIdRef = useRef(erroredServerId);
|
||||
useEffect(() => {
|
||||
const prev = prevStatusRef.current;
|
||||
prevStatusRef.current = connectionStatus;
|
||||
const prevErrored = prevErroredServerIdRef.current;
|
||||
prevErroredServerIdRef.current = erroredServerId;
|
||||
const becameConnected =
|
||||
connectionStatus === "connected" && prev !== "connected";
|
||||
const becameError =
|
||||
connectionStatus === "error" && prev !== "error" && prev !== "connected";
|
||||
(connectionStatus === "error" &&
|
||||
prev !== "error" &&
|
||||
prev !== "connected") ||
|
||||
(erroredServerId !== undefined && prevErrored !== erroredServerId);
|
||||
if (becameConnected || becameError) {
|
||||
setMonitorPinned(true);
|
||||
}
|
||||
}, [connectionStatus, setMonitorPinned]);
|
||||
}, [connectionStatus, erroredServerId, setMonitorPinned]);
|
||||
|
||||
const appTools = useMemo<Tool[]>(() => {
|
||||
return tools.filter((tool) => {
|
||||
|
|
@ -924,7 +946,14 @@ export function InspectorView({
|
|||
// (set by the parent only for connect-attempt failures, not mid-session
|
||||
// crashes) so a crash of a previously-connected server doesn't reorganize the
|
||||
// column into the failure tab set — matching the auto-open effect above.
|
||||
const failed = connectionStatus === "error" && erroredServerId !== undefined;
|
||||
//
|
||||
// Keyed on `erroredServerId` alone rather than on the `"error"` status
|
||||
// (#2108): an OAuth leg that fails is torn down by the parent, so the session
|
||||
// rests at `"disconnected"` and a status gate would never see the failure —
|
||||
// which is exactly the case a server developer needs the Network tab for.
|
||||
// `!connected` is the only status condition left, and it holds vacuously
|
||||
// whenever the flag is set (the parent clears it as each new attempt starts).
|
||||
const failed = !connected && erroredServerId !== undefined;
|
||||
const monitorAvailable = useMemo<string[]>(() => {
|
||||
if (connected) return availableTabs.filter((t) => MONITOR_TABS.includes(t));
|
||||
if (failed) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue