feat(web): show Apps, Prompts, Resources, and Tasks tabs only when the server exposes the relevant items (#1450) (#1453)

* feat(web): show Apps tab only when server exposes MCP App tools (#1450)

Gate the "Apps" tab on appTools.length > 0 so it is hidden when the
connected server exposes no MCP App tools. appTools is now computed
before availableTabs, which depends on it — the tab appears/disappears
live as the tool list changes (list-changed refresh, server switch).
The existing activeTab fallback already snaps a user sitting on Apps
back to Servers when the tab disappears. When app tools exist but the
sandbox is unavailable the tab stays visible so the AppsScreen's
"unavailable" message remains reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): content-gate the Prompts and Resources tabs too (#1450)

Extend the Apps tab gating to Prompts and Resources: each tab is hidden
when its list is empty and appears reactively when the list gains an
entry (list-changed refresh, server switch). Resources is gated on
resources OR templates, since a server may expose only templates. The
existing activeTab fallback snaps a user on a now-empty tab back to
Servers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): content-gate the Tasks tab too (#1450)

Extend the content gating to the Tasks tab: hidden when no tasks exist,
appears reactively once a task-augmented tool call creates one and
disappears when all are cleared. Consistent with the Apps/Prompts/
Resources gates — the existing activeTab fallback snaps a user on a now-
empty tab back to Servers. The "run as task" affordance still lives on
the Tools screen (gated by server task support), so discovery is
unaffected; advertised capabilities remain visible in Connection Info.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): fade content-gated tabs in to draw attention on appearance (#1450)

Add a brief 325ms fade-in to navigation tab labels via a new `tabLabel`
Text theme variant + `inspector-fade-in` keyframe. SegmentedControl keys
segments by value, so when a content-gated tab (Apps/Prompts/Resources/
Tasks) appears after a list change its label mounts fresh and fades in to
draw the eye, while existing tabs stay put.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): animate tab-bar width when a tab is added or removed (#1450)

Wrap the tab SegmentedControl in a clip element (new `tabBar` Group
variant) whose width is set to the control's measured border-box size and
transitions over 325ms ease-in. The control is sized `max-content` and
the clip `overflow: hidden`, so when a content-gated tab appears or
disappears the bar grows/shrinks smoothly. Width is measured with
useElementSize (ResizeObserver); it falls back to auto width where the
observer is unavailable, so the bar still renders at its natural size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): lengthen tab-label fade-in to 450ms (#1450)

Bump the `tabLabel` fade from 325ms to 450ms so a newly-appearing tab
name fades in more gently. Also formats the previously-unformatted
ViewHeader width-clip test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): drop tab-label fade-in, keep only the 325ms width animation (#1450)

Remove the per-tab fade-in (tabLabel Text variant + inspector-fade-in
keyframe + label wrapping) so the tab bar's 325ms ease-in width change is
the sole animation when a content-gated tab appears or disappears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): collapse the tab bar to 0 width when it leaves the DOM on disconnect (#1450)

Unify the connected/disconnected ViewHeader into a single tree so the tab
controls can sit in a keep-alive Mantine Transition. On disconnect the
Transition holds the bar mounted for 325ms while the clip's CSS width
transition collapses it measured->0, then unmounts. The connected tab
list/selection is snapshotted (adjust-state-during-render, no refs) so the
bar keeps rendering its tabs as it shrinks. The disconnected header keeps
its logo-left / title-center / theme-right layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): fade the disconnected title in after the tab bar collapses (#1450)

On disconnect, hold the "MCP Inspector" title until the tab bar's
shrink-to-0 exit finishes (tracked via the keep-alive Transition's
onExited), then fade it in over 300ms. Removes the prior overlap where the
title appeared beside the still-collapsing bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): set both disconnect animations to 300ms (#1450)

Drop the tab-bar collapse from 325ms to 300ms so it matches the title
fade (also 300ms), trimming the overall disconnect sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): staggered fade+slide-down crossfade between title and tab bar (#1450)

Replace the tab-bar width/size animation with a staggered crossfade. The
disconnected "MCP Inspector" title and the connected tab bar share one CSS
grid cell, so on connect/disconnect one fades + slides down out while the
other fades + slides down in — the incoming one delayed by half the 300ms
duration so they're staggered and one replaces the other in place.

Both motions travel downward (enter descends from above, exit descends
below) via inspector-fade-slide-in/out keyframes, selected per element with
a data-anim attribute; Mantine Transition provides keep-alive so the exit
plays before unmount. Removes the clip/useElementSize/tabBar-variant
size-animation machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): pulse a red glow on a tab that newly appears mid-session (#1450)

When a content-gated tab is added to the bar while connected, pulse a red
text glow around its label in-then-out over 325ms. New tabs are detected by
comparing the shown set to the previous render's (adjust-state-during-
render), so only mid-session additions glow — not the initial set on
connect, nor anything on disconnect. The glow is a CSS keyframe
(inspector-tab-glow) applied via a `tabGlow` Text variant + `data-glow`
attribute; the red comes from a new --inspector-glow-red token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): start the new-tab glow at full intensity, then fade (#1450)

Change the tab glow from an in-then-out pulse to full-on-appear then fade
over 325ms (ease-out), so a newly added tab flashes brightest the instant
it appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): keep new-tab label text red through the glow, then revert to theme (#1450)

Animate `color` red across the whole glow keyframe so a newly-appeared
tab's text stays red for the 325ms; with no forwards fill it snaps back to
the label's theme color when the animation ends. The glow text-shadow still
fades over the duration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): revert new-tab text color animation, keep glow only (#1450)

Drop the `color: red` keyframe stops so the label text keeps its theme
color; only the red glow (text-shadow) plays, full intensity on appear then
fading over 325ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): make the new-tab glow ease-in over 325ms (#1450)

Change the tab glow timing function from ease-out to ease-in (duration
already 325ms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): animate server name and right-side controls on connect/disconnect (#1450)

Wrap the server name (left) and the status indicator + Disconnect control
(right) in keep-alive Transitions so they fade + slide-down in on connect
and out on disconnect, using the same values as the tab bar. Split the
animation CSS (.header-anim[data-anim]) from the center-stack grid
placement so it's reusable, and broaden the snapshot to retain serverName/
status/latency for rendering during exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): address review — glow grace window, Select comment, key unification (#1450)

- Arm the new-tab glow only after a GLOW_GRACE_MS window post-connect, so
  asynchronously-resolved lists (prompts/resources/tasks) aren't mistaken
  for mid-session additions and don't glow on initial load.
- Document why the narrow-viewport Select doesn't glow.
- Route snapshotKey and the glow key through a shared tabsKey()/TAB_SEP
  helper (also removes a stray control char from the join separator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(web): note the slow-server tail of the glow grace window (#1450)

Acknowledge in a comment that GLOW_GRACE_MS keys on time-since-connect, not
on lists having settled, so a server slower than the window glows on its
tail. Per review follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): honor prefers-reduced-motion for the new header animations (#1450)

Add a reduced-motion media query that disables the header fade/slide
(.header-anim) and the new-tab glow (.tab-glow), leaving elements at their
resting state. Per review accessibility suggestion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-12 18:11:52 -04:00 • committed by GitHub
commit a10ced988c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 855 additions and 85 deletions

View file

@ -1,6 +1,11 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import {
act,
renderWithMantine,
screen,
waitFor,
} from "../../../test/renderWithMantine";
import { ViewHeader } from "./ViewHeader";
// Mock @mantine/hooks so we can control useMediaQuery results per test.
@ -118,6 +123,120 @@ describe("ViewHeader", () => {
expect(radios.length).toBeGreaterThan(0);
});
it("animates the connected tab bar in with the slide-down enter (#1450)", () => {
mediaQueryMock.value = true;
renderWithMantine(<ViewHeader {...connectedProps} />);
// The tab bar lives in a crossfade cell marked for the enter animation.
const cell = screen.getAllByRole("radio")[0]!.closest("[data-anim]");
expect(cell?.getAttribute("data-anim")).toBe("in");
});
it("crossfades on disconnect: tab bar exits while the title enters, then the bar unmounts (#1450)", async () => {
mediaQueryMock.value = true;
const { rerender } = renderWithMantine(
<ViewHeader {...connectedProps} />,
);
expect(screen.getAllByRole("radio").length).toBeGreaterThan(0);
// Disconnect: the connected header is replaced, but the tab bar stays in
// the DOM (now marked for the exit animation) until the keep-alive
// Transition's exit window elapses — so it isn't removed synchronously.
rerender(<ViewHeader connected={false} onToggleTheme={vi.fn()} />);
const exitingCell = screen
.getAllByRole("radio")[0]!
.closest("[data-anim]");
expect(exitingCell?.getAttribute("data-anim")).toBe("out");
// The title enters (staggered) in its own cell.
const title = await screen.findByText("MCP Inspector");
expect(title.closest("[data-anim]")?.getAttribute("data-anim")).toBe(
"in",
);
// After the exit transition the tab bar is removed from the DOM entirely.
await waitFor(() =>
expect(screen.queryAllByRole("radio").length).toBe(0),
);
});
it("glows a tab added after the connect grace window, not during it (#1450)", () => {
vi.useFakeTimers();
try {
mediaQueryMock.value = true;
const { rerender } = renderWithMantine(
<ViewHeader {...connectedProps} />,
);
// During the post-connect grace window an async-resolved list (adding
// "Apps") counts as part of the initial set and does not glow.
rerender(
<ViewHeader
{...connectedProps}
availableTabs={["Tools", "Apps", "Resources", "Prompts"]}
/>,
);
expect(document.querySelector('[data-glow="on"]')).toBeNull();
// Once the grace window elapses, a genuine mid-session addition glows.
act(() => {
vi.advanceTimersByTime(2000);
});
rerender(
<ViewHeader
{...connectedProps}
availableTabs={["Tools", "Apps", "Resources", "Prompts", "Tasks"]}
/>,
);
const glowing = document.querySelectorAll('[data-glow="on"]');
expect(glowing.length).toBe(1);
expect(glowing[0]?.textContent).toBe("Tasks");
} finally {
vi.useRealTimers();
}
});
it("animates the server name and disconnect controls in on connect, out on disconnect (#1450)", async () => {
mediaQueryMock.value = true;
const { rerender } = renderWithMantine(
<ViewHeader {...connectedProps} />,
);
// Connected: the server name and the Disconnect control are in their
// enter cells.
expect(
screen
.getByText("my-mcp-server")
.closest("[data-anim]")
?.getAttribute("data-anim"),
).toBe("in");
expect(
screen
.getByRole("button", { name: "Disconnect" })
.closest("[data-anim]")
?.getAttribute("data-anim"),
).toBe("in");
// Disconnect: both stay mounted (animating out) before unmounting.
rerender(<ViewHeader connected={false} onToggleTheme={vi.fn()} />);
expect(
screen
.getByText("my-mcp-server")
.closest("[data-anim]")
?.getAttribute("data-anim"),
).toBe("out");
expect(
screen
.getByRole("button", { name: "Disconnect" })
.closest("[data-anim]")
?.getAttribute("data-anim"),
).toBe("out");
await waitFor(() =>
expect(screen.queryByText("my-mcp-server")).not.toBeInTheDocument(),
);
expect(
screen.queryByRole("button", { name: "Disconnect" }),
).not.toBeInTheDocument();
});
it("invokes onTabChange when a different tab is picked from the Select", async () => {
const user = userEvent.setup();
const onTabChange = vi.fn();

View file

@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import {
ActionIcon,
Anchor,
Box,
Button,
Group,
Image,
@ -8,6 +10,7 @@ import {
Select,
Text,
Title,
Transition,
useComputedColorScheme,
} from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
@ -37,6 +40,77 @@ interface UnconnectedProps {
export type ViewHeaderProps = ConnectedProps | UnconnectedProps;
// Keep-alive window for the header connect/disconnect animations (#1450): the
// center title↔tab-bar crossfade plus the server name (left) and status +
// Disconnect controls (right) all fade + slide-down, staggered by half this
// duration. The motion itself is CSS (`.header-anim`); keep the 300ms /
// 150ms-stagger there in sync with this value.
const HEADER_ANIM_MS = 300;
// Fixed Select width on narrow viewports (fits the longest tab label).
const SELECT_WIDTH = 140;
// Grace window after a connection is established before the new-tab glow arms
// (#1450). Primitive lists (prompts/resources/tasks) are fetched asynchronously
// just after the handshake, so their tabs appear a few renders into the
// connection; without this window they'd be mistaken for mid-session additions
// and glow on initial load. Tabs that resolve within this window are treated as
// part of the initial set and don't glow.
//
// This keys on time-since-connect, not on lists having settled, so it's a
// heuristic: a server whose lists resolve slower than this still glows on its
// slow tail, and a fast server pays a small arming delay. 1s is a reasonable
// middle ground; if the InspectorClient ever exposes an "initial fetches done"
// signal, gating on that would be more robust than a fixed timer.
const GLOW_GRACE_MS = 1000;
// Tab names are single words, so newline is a safe join separator for the
// "tabs seen / shown" keys.
const TAB_SEP = "\n";
function tabsKey(tabs: string[]): string {
return tabs.join(TAB_SEP);
}
// Connected-header display data retained so each region can keep rendering while
// it animates out after disconnect (the live props are gone by then).
interface HeaderSnapshot {
serverName: string;
status: ConnectionStatus;
latencyMs?: number;
activeTab: string;
availableTabs: string[];
}
// Value-key for a snapshot, so re-snapshotting compares by content (fresh array
// references each render won't trigger an update loop).
function snapshotKey(s: HeaderSnapshot): string {
return [
s.serverName,
s.status,
s.latencyMs ?? "",
s.activeTab,
tabsKey(s.availableTabs),
].join(TAB_SEP);
}
// Tab label that can pulse a red glow when it newly appears (#1450). The glow
// fires only while `data-glow="on"`; the `tabGlow` variant supplies the class
// and the keyframe/trigger live in App.css.
const TabGlowLabel = Text.withProps({ span: true, variant: "tabGlow" });
// SegmentedControl data with each label wrapped so the freshly-added tabs
// (`glowing`) pulse on mount. `value` (used for selection and by tests) stays
// the plain tab string.
function toGlowingTabData(tabs: string[], glowing: string[]) {
return tabs.map((tab) => ({
value: tab,
label: (
<TabGlowLabel data-glow={glowing.includes(tab) ? "on" : undefined}>
{tab}
</TabGlowLabel>
),
}));
}
const HeaderBar = Group.withProps({
h: "100%",
px: "md",
@ -64,7 +138,10 @@ const LogoImage = Image.withProps({
fit: "contain",
});
// Server name carries the `header-anim` class so it fades + slides on
// connect/disconnect (the `data-anim` direction is set per render).
const ServerName = Text.withProps({
className: "header-anim",
fw: 600,
size: "lg",
truncate: "end",
@ -72,11 +149,6 @@ const ServerName = Text.withProps({
flex: 1,
});
const CenterSection = Group.withProps({
wrap: "nowrap",
flex: "0 0 auto",
});
const RightSection = Group.withProps({
gap: "sm",
wrap: "nowrap",
@ -85,6 +157,14 @@ const RightSection = Group.withProps({
justify: "flex-end",
});
// The status indicator + Disconnect control animate as one unit on
// connect/disconnect (the theme toggle stays put outside it).
const RightConnectedGroup = Group.withProps({
className: "header-anim",
gap: "sm",
wrap: "nowrap",
});
const DisconnectButton = Button.withProps({
variant: "subtle",
c: "red",
@ -104,77 +184,196 @@ const ThemeToggle = ActionIcon.withProps({
"aria-label": "Toggle color scheme",
});
const UnconnectedBar = Group.withProps({
h: "100%",
px: "md",
justify: "space-between",
});
export function ViewHeader(props: ViewHeaderProps) {
const colorScheme = useComputedColorScheme();
const ThemeIcon = colorScheme === "dark" ? MdLightMode : MdDarkMode;
const showSegmented = useMediaQuery("(min-width: 992px)");
const showDisconnectLabel = useMediaQuery("(min-width: 768px)");
if (!props.connected) {
return (
<UnconnectedBar>
<LogoLink>
<LogoImage src={colorScheme === "dark" ? mcpLogoDark : mcpLogo} />
</LogoLink>
<Title order={2}>MCP Inspector</Title>
<ThemeToggle onClick={props.onToggleTheme}>
<ThemeIcon size={20} />
</ThemeToggle>
</UnconnectedBar>
// Retain the latest connected display data so each region can keep rendering
// it while animating out after disconnect (#1450). Uses React's "adjust state
// during render" pattern, re-set only when the values change (compared by key)
// so stable inputs don't loop. Callbacks aren't snapshotted — an exiting
// header isn't interactive.
const liveSnapshot: HeaderSnapshot | null = props.connected
? {
serverName: props.serverInfo.name,
status: props.status,
latencyMs: props.latencyMs,
activeTab: props.activeTab,
availableTabs: props.availableTabs,
}
: null;
const [snapshot, setSnapshot] = useState<HeaderSnapshot | null>(liveSnapshot);
if (
liveSnapshot &&
snapshotKey(liveSnapshot) !== (snapshot ? snapshotKey(snapshot) : "")
) {
setSnapshot(liveSnapshot);
}
const headerData = props.connected ? liveSnapshot : snapshot;
const handleTabChange = props.connected ? props.onTabChange : undefined;
const handleDisconnect = props.connected ? props.onDisconnect : undefined;
// The glow only arms once the connection has settled (GLOW_GRACE_MS after
// connect), so tabs whose lists resolve asynchronously just after the
// handshake count as the initial set and don't glow. Reset on disconnect via
// adjust-state-during-render; armed by the timer effect below.
const [glowArmed, setGlowArmed] = useState(false);
if (!props.connected && glowArmed) {
setGlowArmed(false);
}
useEffect(() => {
if (!props.connected) return;
const id = setTimeout(() => setGlowArmed(true), GLOW_GRACE_MS);
return () => clearTimeout(id);
}, [props.connected]);
// Track which tabs newly appeared so their labels pulse a red glow (#1450).
// Compared against the previous shown set via adjust-state-during-render, so
// only tabs added mid-session glow — not the initial set on connect, the
// async-resolved lists during the grace window, nor anything on disconnect.
// `glowing` persists in committed state (it isn't cleared in the same render)
// so the class survives to the DOM.
const liveTabs = props.connected ? props.availableTabs : [];
const liveTabsKey = tabsKey(liveTabs);
const [seenTabsKey, setSeenTabsKey] = useState(liveTabsKey);
const [glowing, setGlowing] = useState<string[]>([]);
if (liveTabsKey !== seenTabsKey) {
const prev = seenTabsKey ? seenTabsKey.split(TAB_SEP) : [];
setSeenTabsKey(liveTabsKey);
setGlowing(
glowArmed && prev.length ? liveTabs.filter((t) => !prev.includes(t)) : [],
);
}
// `in` while present/entering, `out` while exiting — drives the slide-down
// direction for every connect/disconnect-animated region.
const connectedAnim = props.connected ? "in" : "out";
const logoSrc = colorScheme === "dark" ? mcpLogoDark : mcpLogo;
return (
<HeaderBar>
<LeftSection>
<LogoLink>
<LogoImage src={colorScheme === "dark" ? mcpLogoDark : mcpLogo} />
<LogoImage src={logoSrc} />
</LogoLink>
<ServerName>{props.serverInfo.name}</ServerName>
<Transition
mounted={props.connected}
transition="fade"
duration={HEADER_ANIM_MS}
exitDuration={HEADER_ANIM_MS}
>
{() =>
headerData ? (
<ServerName data-anim={connectedAnim}>
{headerData.serverName}
</ServerName>
) : (
<></>
)
}
</Transition>
</LeftSection>
<CenterSection>
{showSegmented ? (
<SegmentedControl
value={props.activeTab}
onChange={props.onTabChange}
data={props.availableTabs}
size="sm"
/>
) : (
<Select
value={props.activeTab}
onChange={(value) => value && props.onTabChange(value)}
data={props.availableTabs}
size="sm"
allowDeselect={false}
// Sized to comfortably fit the longest current tab label
// ("Resources"). Revisit if longer tabs are added.
w={140}
/>
)}
</CenterSection>
{/* CSS grid stack: the title and tab bar cells share one cell (grid-area
1/1 via `.header-stack-cell`), so on connect/disconnect one
fades+slides out as the other fades+slides in, in the same place.
`flex: 0 0 auto` keeps it from stretching within the header. */}
<Box display="grid" flex="0 0 auto">
{/* The Transitions are keep-alive only: when `mounted` flips false the
cell stays in the DOM for `exitDuration` while its CSS exit animation
(`data-anim="out"`) plays, then unmounts. `data-anim` selects the
slide-down direction (in = descend from above, out = descend below);
the incoming cell's CSS delay staggers it behind the outgoing one. */}
<Transition
mounted={props.connected}
transition="fade"
duration={HEADER_ANIM_MS}
exitDuration={HEADER_ANIM_MS}
>
{() =>
headerData ? (
<Box
className="header-anim header-stack-cell"
data-anim={connectedAnim}
>
{showSegmented ? (
<SegmentedControl
value={headerData.activeTab}
onChange={handleTabChange}
data={toGlowingTabData(headerData.availableTabs, glowing)}
size="sm"
/>
) : (
// Narrow viewport: the new-tab glow doesn't apply to the
// dropdown (a collapsed Select can't pulse a single option),
// so the labels stay plain strings here.
<Select
value={headerData.activeTab}
onChange={(value) => value && handleTabChange?.(value)}
data={headerData.availableTabs}
size="sm"
allowDeselect={false}
w={SELECT_WIDTH}
/>
)}
</Box>
) : (
// Unreachable in practice — the Transition only mounts while
// connected, by which point the snapshot is set — but the render
// prop must return an element, never null.
<></>
)
}
</Transition>
<Transition
mounted={!props.connected}
transition="fade"
duration={HEADER_ANIM_MS}
exitDuration={HEADER_ANIM_MS}
>
{() => (
<Box
className="header-anim header-stack-cell"
data-anim={props.connected ? "out" : "in"}
>
<Title order={2}>MCP Inspector</Title>
</Box>
)}
</Transition>
</Box>
<RightSection>
<ServerStatusIndicator
status={props.status}
latencyMs={props.latencyMs}
/>
{showDisconnectLabel ? (
<DisconnectButton onClick={props.onDisconnect}>
Disconnect
</DisconnectButton>
) : (
<DisconnectIcon onClick={props.onDisconnect} title="Disconnect">
<MdLinkOff size={20} />
</DisconnectIcon>
)}
<Transition
mounted={props.connected}
transition="fade"
duration={HEADER_ANIM_MS}
exitDuration={HEADER_ANIM_MS}
>
{() =>
headerData ? (
<RightConnectedGroup data-anim={connectedAnim}>
<ServerStatusIndicator
status={headerData.status}
latencyMs={headerData.latencyMs}
/>
{showDisconnectLabel ? (
<DisconnectButton onClick={handleDisconnect}>
Disconnect
</DisconnectButton>
) : (
<DisconnectIcon onClick={handleDisconnect} title="Disconnect">
<MdLinkOff size={20} />
</DisconnectIcon>
)}
</RightConnectedGroup>
) : (
<></>
)
}
</Transition>
<ThemeToggle onClick={props.onToggleTheme}>
<ThemeIcon size={20} />
</ThemeToggle>