fix(ui): always show the Lens Settings tab

The tab only rendered once GET /lens succeeded for a configuring session, so a
slow or failing list, or a read-only session, silently hid it. Settings now
always renders, and the worker section shows only where it can be used

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-04 13:54:19 -07:00
parent 461a58c40a
commit 60e7726116
4 changed files with 55 additions and 42 deletions

View file

@ -57,15 +57,15 @@ export function LensModeSwitch({
setup?: string;
}) {
const connected = useWorkerConnected(workers);
const settingsTitle = connected ? "Worker connected" : "Connect worker";
const tabs = Object.entries(LENS_TABS).filter(([view]) => view !== "settings" || workers);
const workerTitle = connected ? "Worker connected" : "Connect worker";
const settingsTitle = workers === null ? LENS_TABS.settings : workerTitle;
return (
<div className={frameTab({ session: demo ? "demo" : "live" })}>
<NotchCorner side="left" demo={demo} />
<NotchCorner side="right" demo={demo} />
<TabsPrimitive.List aria-label="Lens" className="relative inline-flex h-9 items-center p-1">
<TabsPrimitive.Indicator className="absolute top-1 bottom-1 left-(--active-tab-left) w-(--active-tab-width) rounded-full bg-muted transition-[left,width] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none" />
{tabs.map(([view, label]) => {
{Object.entries(LENS_TABS).map(([view, label]) => {
const Icon = MODE_ICONS[view as LensTab];
const workerDisconnected = view === "settings" && workers !== null && !connected;
return (

View file

@ -280,8 +280,8 @@ describe("Lens interactive demo", () => {
});
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly={false} />, { onUrlUpdate });
const tabs = within(screen.getByRole("tablist", { name: "Lens" }));
const settings = await tabs.findByRole("tab", { name: "Settings" });
expect(settings).toHaveAttribute("title", "Worker connected");
const settings = tabs.getByRole("tab", { name: "Settings" });
await waitFor(() => expect(settings).toHaveAttribute("title", "Worker connected"));
await user.click(settings);
await expectUrl(onUrlUpdate, (url) => expect(url.get("tab")).toBe("settings"));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@ -357,7 +357,7 @@ describe("Lens interactive demo", () => {
onUrlUpdate,
});
const panel = within(await screen.findByRole("region", { name: "Settings" }));
await user.click(panel.getByText("Advanced options"));
await user.click(await panel.findByText("Advanced options"));
await user.click(panel.getByRole("switch", { name: "Use an existing virtual key" }));
await user.click(panel.getByRole("combobox", { name: "Charge analysis to" }));
await user.click(await screen.findByRole("option", { name: "Analysis" }));
@ -378,18 +378,28 @@ describe("Lens interactive demo", () => {
expect(await screen.findByRole("region", { name: "New investigation" })).toBeVisible();
});
it("hides the Settings tab for read-only sessions", async () => {
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly />);
expect(await screen.findByRole("tablist", { name: "Lens" })).toBeVisible();
await waitFor(() => expect(network).toHaveBeenCalled());
expect(screen.queryByRole("tab", { name: "Settings" })).not.toBeInTheDocument();
});
it("sends read-only sessions following a Settings link to the default tab", async () => {
it("opens Settings for read-only sessions without the worker controls", async () => {
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly />, {
searchParams: "?tab=settings",
});
expect(await screen.findByRole("tab", { name: "Traces", selected: true })).toBeVisible();
expect(await screen.findByRole("tab", { name: "Settings", selected: true })).toBeVisible();
const panel = within(screen.getByRole("region", { name: "Settings" }));
expect(await panel.findByRole("status")).toHaveTextContent("Tracing is not enabled");
expect(panel.getByRole("switch", { name: "Show the introduction on each new session" })).toBeVisible();
expect(panel.queryByRole("heading", { name: "Analysis worker" })).not.toBeInTheDocument();
});
it("shows the Settings tab before /lens answers and when it fails", async () => {
network.mockImplementation(async (input) =>
requestPath(input) === "/lens"
? Response.json({ detail: "boom" }, { status: 500 })
: Response.json({ data: [], traces: true, requests: false }),
);
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly={false} />);
const tabs = within(screen.getByRole("tablist", { name: "Lens" }));
expect(tabs.getByRole("tab", { name: "Settings" })).toHaveAttribute("title", "Settings");
await waitFor(() => expect(network.mock.calls.some(([input]) => requestPath(input) === "/lens")).toBe(true));
expect(tabs.getByRole("tab", { name: "Settings" })).toBeVisible();
});
it("turns the worker health dot off once heartbeats expire even when polling returns unchanged data", async () => {
@ -412,8 +422,8 @@ describe("Lens interactive demo", () => {
});
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly={false} />);
const tabs = within(screen.getByRole("tablist", { name: "Lens" }));
const settings = await tabs.findByRole("tab", { name: "Settings" });
expect(settings).toHaveAttribute("title", "Worker connected");
const settings = tabs.getByRole("tab", { name: "Settings" });
await waitFor(() => expect(settings).toHaveAttribute("title", "Worker connected"));
await vi.advanceTimersByTimeAsync(130000);
await waitFor(() => expect(settings).toHaveAttribute("title", "Connect worker"));
} finally {

View file

@ -86,7 +86,7 @@ function LensContent({ userRole, readOnly }: Omit<WorkspaceProps, "accessToken">
const isAdmin = isProxyAdminRole(userRole);
const canConfigure = canViewInvestigations && !readOnly;
const defaultTab = lensId ? "investigations" : "traces";
const activeTab = tab === "settings" && !canConfigure ? defaultTab : tab ?? defaultTab;
const activeTab = tab ?? defaultTab;
const intro = useLensIntro({ demo, settingUp });
const { activity, list } = useLensOverview(
canViewInvestigations,
@ -192,21 +192,20 @@ function LensContent({ userRole, readOnly }: Omit<WorkspaceProps, "accessToken">
)}
</LensPreviewContext.Provider>
</TabsContent>
{workers && list && (
<TabsContent value="settings" keepMounted className={cn(PANEL, "p-6")}>
<LensSettings
list={list}
workerReadyAction={
list.lenses.length === 0 ? (
<Button className="w-full" onClick={startFirstInvestigation}>
New investigation
</Button>
) : undefined
}
onOpenTraces={() => setTab("traces")}
/>
</TabsContent>
)}
<TabsContent value="settings" keepMounted className={cn(PANEL, "p-6")}>
<LensSettings
tracingEnabled={list?.tracing_enabled}
workers={workers}
workerReadyAction={
list?.lenses.length === 0 ? (
<Button className="w-full" onClick={startFirstInvestigation}>
New investigation
</Button>
) : undefined
}
onOpenTraces={() => setTab("traces")}
/>
</TabsContent>
</div>
</Tabs>
<LensIntroDialog

View file

@ -60,24 +60,28 @@ function IntroductionSection() {
}
export function LensSettings({
list,
tracingEnabled,
workers,
workerReadyAction,
onOpenTraces,
}: {
list: LensList;
tracingEnabled: boolean | undefined;
workers: LensList["workers"] | null;
/** Replaces the worker install card's Done button once the new worker connects. */
workerReadyAction?: ReactNode;
onOpenTraces: () => void;
}) {
return (
<div aria-label="Settings" role="region" className="flex w-full flex-col divide-y divide-border">
<TracingSection enabled={list.tracing_enabled} onOpenTraces={onOpenTraces} />
<SettingsSection
heading="Analysis worker"
description="Runs investigations on your server and bills model usage to an analysis key."
>
<WorkerSettings workers={list.workers} readyAction={workerReadyAction} />
</SettingsSection>
{tracingEnabled !== undefined && <TracingSection enabled={tracingEnabled} onOpenTraces={onOpenTraces} />}
{workers && (
<SettingsSection
heading="Analysis worker"
description="Runs investigations on your server and bills model usage to an analysis key."
>
<WorkerSettings workers={workers} readyAction={workerReadyAction} />
</SettingsSection>
)}
<IntroductionSection />
</div>
);