fix(settings): show application names and separate scrolling

This commit is contained in:
Amruth Pillai
2026-10-02 00:29:29 +02:00
parent 78f4f81831
commit 0a0970160b
3 changed files with 61 additions and 5 deletions
@@ -0,0 +1,49 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const oauth2 = vi.hoisted(() => ({ getConsents: vi.fn(), publicClient: vi.fn(), deleteConsent: vi.fn() }));
vi.mock("@/libs/auth/client", () => ({ authClient: { oauth2 } }));
i18n.loadAndActivate({ locale: "en", messages: {} });
const { ConnectedAppsSection } = await import("./connected-apps");
it("shows registered application names, keeps unnamed or unavailable clients revocable, and revokes the consent", async () => {
oauth2.getConsents.mockResolvedValue({
data: [
{ id: "consent-1", clientId: "codex-client-id", scopes: ["api:read"] },
{ id: "consent-2", clientId: "unnamed-client-id", scopes: ["api:read"] },
{ id: "consent-3", clientId: "unavailable-client-id", scopes: ["api:read"] },
],
error: null,
});
oauth2.publicClient.mockImplementation(({ query }: { query: { client_id: string } }) => {
if (query.client_id === "unavailable-client-id") return Promise.reject(new Error("Network unavailable"));
return Promise.resolve({
data: { client_name: query.client_id === "codex-client-id" ? "Codex" : null },
error: null,
});
});
oauth2.deleteConsent.mockResolvedValue({ error: null });
render(
<I18nProvider i18n={i18n}>
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<ConnectedAppsSection />
</QueryClientProvider>
</I18nProvider>,
);
expect(await screen.findByText("Codex")).toBeTruthy();
expect(screen.queryByText("codex-client-id")).toBeNull();
expect(screen.getByText("unnamed-client-id")).toBeTruthy();
expect(screen.getByText("unavailable-client-id")).toBeTruthy();
const connection = screen.getByText("Codex").closest("li");
if (!connection) throw new Error("Application name is missing its connection row");
fireEvent.click(within(connection).getByRole("button", { name: "Revoke access" }));
await waitFor(() => expect(oauth2.deleteConsent).toHaveBeenCalledWith({ id: "consent-1" }));
});
@@ -18,7 +18,14 @@ export function ConnectedAppsSection() {
queryFn: async () => {
const { data, error } = await authClient.oauth2.getConsents();
if (error) throw new Error(t`Could not load connected applications.`);
return data ?? [];
return Promise.all(
(data ?? []).map(async (connection) => {
const client = await authClient.oauth2
.publicClient({ query: { client_id: connection.clientId } })
.catch(() => null);
return { ...connection, clientName: client?.data?.client_name?.trim() || connection.clientId };
}),
);
},
});
const revoke = useMutation({
@@ -48,7 +55,7 @@ export function ConnectedAppsSection() {
{connections.map((connection) => (
<li key={connection.id} className="flex items-center justify-between gap-3 py-3">
<div className="min-w-0 text-sm">
<p className="font-medium break-all">{connection.clientId}</p>
<p className="font-medium break-all">{connection.clientName}</p>
<p className="break-words text-ink-3">{connection.scopes.join(", ")}</p>
</div>
<Button
@@ -35,10 +35,10 @@ function RouteComponent() {
const isRoot = pathname.replace(/\/$/, "") === "/dashboard/settings";
return (
<div className="grid min-h-full content-start lg:grid-cols-[220px_minmax(0,1fr)] lg:content-stretch">
<div className="grid min-h-full content-start sm:h-svh sm:min-h-0 sm:grid-rows-[auto_minmax(0,1fr)] sm:overflow-hidden lg:grid-cols-[220px_minmax(0,1fr)] lg:grid-rows-1 lg:content-stretch">
<nav
aria-label={t`Settings`}
className="flex flex-col gap-1 border-line [view-transition-name:settings-nav] max-lg:border-b max-sm:hidden lg:border-e lg:py-7 lg:ps-6 lg:pe-3"
className="flex flex-col gap-1 border-line [view-transition-name:settings-nav] max-lg:border-b max-sm:hidden lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain lg:border-e lg:py-7 lg:ps-6 lg:pe-3"
>
<h1 className="ms-2 mb-3.5 font-display text-[26px] leading-8 font-medium max-lg:hidden">
<Trans>Settings</Trans>
@@ -70,7 +70,7 @@ function RouteComponent() {
</p>
</nav>
<div className="min-w-0 px-12 pt-8 pb-16 max-lg:px-6 max-sm:px-4 max-sm:pt-4">
<div className="min-w-0 px-12 pt-8 pb-16 max-lg:px-6 max-sm:px-4 max-sm:pt-4 sm:min-h-0 sm:overflow-y-auto sm:overscroll-contain">
{!isRoot && (
<Link
to="/dashboard/settings"