From f5bcde126cba8edc256d3a72255a885a234634e4 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:32:21 -0300 Subject: [PATCH 1/7] refactor(ui): resolve a device's install key behind one hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviceDetails paired useInstallKeys with resolveEnrollmentSource and repeated the page size the lookup depends on. A second caller was about to repeat both, so the pair moves behind useEnrollmentSource and the page size stops being a literal each caller has to agree on. The lookup is client-side over a single page of keys, so a namespace past that page resolves its tail to null rather than to a name. That limit is why the hook's doc comment states it: it is not visible from the call site, and a caller that assumes otherwise renders a dash. KeyValueChip moves to components/common for the same reason — a second page tree needs it, and reaching across into pages/install-keys/ to get it is not a dependency worth having. --- .../common}/KeyValueChip.tsx | 0 .../console/src/hooks/useEnrollmentSource.ts | 21 +++++++++++++++++++ ui/apps/console/src/pages/DeviceDetails.tsx | 10 ++------- .../src/pages/install-keys/EventPublicKey.tsx | 2 +- .../install-keys/InstallKeyHistoryPage.tsx | 2 +- .../install-keys/installKeyEventColumns.tsx | 2 +- 6 files changed, 26 insertions(+), 11 deletions(-) rename ui/apps/console/src/{pages/install-keys => components/common}/KeyValueChip.tsx (100%) create mode 100644 ui/apps/console/src/hooks/useEnrollmentSource.ts diff --git a/ui/apps/console/src/pages/install-keys/KeyValueChip.tsx b/ui/apps/console/src/components/common/KeyValueChip.tsx similarity index 100% rename from ui/apps/console/src/pages/install-keys/KeyValueChip.tsx rename to ui/apps/console/src/components/common/KeyValueChip.tsx diff --git a/ui/apps/console/src/hooks/useEnrollmentSource.ts b/ui/apps/console/src/hooks/useEnrollmentSource.ts new file mode 100644 index 00000000000..5e212150162 --- /dev/null +++ b/ui/apps/console/src/hooks/useEnrollmentSource.ts @@ -0,0 +1,21 @@ +import { useInstallKeys } from "@/hooks/useInstallKeys"; +import { + resolveEnrollmentSource, + type EnrollmentSource, +} from "@/pages/install-keys/helpers"; + +const INSTALL_KEYS_PER_PAGE = 100; + +/** + * Resolves a device's install key digest to the source that enrolled it, returning null when the + * device carries no digest or the key is not among the namespace's first INSTALL_KEYS_PER_PAGE + * keys — the lookup is client-side over a single page, so a larger namespace resolves its tail to + * null rather than to a name. + */ +export function useEnrollmentSource( + installKeyId: string | undefined, +): EnrollmentSource | null { + const { installKeys } = useInstallKeys({ perPage: INSTALL_KEYS_PER_PAGE }); + + return resolveEnrollmentSource(installKeyId, installKeys); +} diff --git a/ui/apps/console/src/pages/DeviceDetails.tsx b/ui/apps/console/src/pages/DeviceDetails.tsx index 1db8a07f0ea..4392bd1ad18 100644 --- a/ui/apps/console/src/pages/DeviceDetails.tsx +++ b/ui/apps/console/src/pages/DeviceDetails.tsx @@ -16,8 +16,7 @@ import { useRemoveDeviceTag, } from "../hooks/useDeviceMutations"; import { useNamespace } from "../hooks/useNamespaces"; -import { useInstallKeys } from "../hooks/useInstallKeys"; -import { resolveEnrollmentSource } from "@/pages/install-keys/helpers"; +import { useEnrollmentSource } from "@/hooks/useEnrollmentSource"; import { DeprecatedBadge } from "@/pages/install-keys/StatusChip"; import { useAuthStore } from "../stores/authStore"; import { useTerminalStore } from "../stores/terminalStore"; @@ -48,9 +47,9 @@ export default function DeviceDetails() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { device, isLoading, error } = useDevice(uid ?? ""); + const enrollment = useEnrollmentSource(device?.install_key_id); const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); - const { installKeys } = useInstallKeys({ perPage: 100 }); const existingSession = useTerminalStore((s) => s.sessions.find((sess) => sess.deviceUid === uid), ); @@ -108,11 +107,6 @@ export default function DeviceDetails() { const nsName = currentNamespace?.name ?? ""; const sshid = nsName ? buildSshid(nsName, device.name) : device.uid; - const enrollment = resolveEnrollmentSource( - device.install_key_id, - installKeys, - ); - const tags: string[] = Array.isArray(device.tags) ? device.tags.map((t) => typeof t === "object" && t !== null && "name" in t ? t.name : String(t), diff --git a/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx b/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx index c0e7906e9e8..afecdf627e8 100644 --- a/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx +++ b/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx @@ -4,7 +4,7 @@ import { type InstallKeyEvent } from "@/client"; import CopyButton from "@/components/common/CopyButton"; import BaseDialog from "@/components/common/BaseDialog"; import { LABEL } from "@/utils/styles"; -import KeyValueChip from "./KeyValueChip"; +import KeyValueChip from "@/components/common/KeyValueChip"; /** * The registered device's key fingerprint, shown inline in the row; clicking it opens the full device diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx index 13c9ebbede2..803c8da83ed 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx @@ -15,7 +15,7 @@ import InstallKeyEventsTable from "./InstallKeyEventsTable"; import InstallKeyActions from "./InstallKeyActions"; import RevealInstallKeyDialog from "./RevealInstallKeyDialog"; import StatusChip from "./StatusChip"; -import KeyValueChip from "./KeyValueChip"; +import KeyValueChip from "@/components/common/KeyValueChip"; import UsageMeter from "./UsageMeter"; import { modeInfo } from "./constants"; import { installKeyDisplayName, isPairingKey } from "./helpers"; diff --git a/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx b/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx index a7973167788..d0b8be11be6 100644 --- a/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx +++ b/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx @@ -6,7 +6,7 @@ import DistroIcon from "@/components/common/DistroIcon"; import type { EntityBase, EntityOperation } from "@/hooks/useActionDialog"; import EventPublicKey from "./EventPublicKey"; import InstallKeyEventReview from "./InstallKeyEventReview"; -import KeyValueChip from "./KeyValueChip"; +import KeyValueChip from "@/components/common/KeyValueChip"; import StatusChip from "./StatusChip"; /** From 5c1a2b027bb59ca9b52d3cb98ad2b928af6fda0e Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:32:31 -0300 Subject: [PATCH 2/7] feat(ui): add a review queue for devices awaiting a decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acceptance moved to the install keys area, which left no list of what is waiting: a pending registration was reachable only by opening each key's activity in turn and scanning it. The dashboard still counted them, so the count named work with nowhere to go. The queue lists by device status rather than by key, which is what lets it agree with the dashboard count — both now answer the same question of the same table. Each row names the key that enrolled it, so the context acceptance needs survives the change of axis. The route is /pending-devices rather than /devices/pending because NavLink matches by prefix: nested under /devices, the sidebar would light up both entries at once, and giving Devices an exact match instead would stop it highlighting on the device detail and add-device pages. --- ui/apps/console/src/App.tsx | 2 + .../console/src/components/layout/Sidebar.tsx | 6 + .../console/src/hooks/useEnrollmentSource.ts | 28 ++- ui/apps/console/src/pages/PendingDevices.tsx | 230 ++++++++++++++++++ .../pages/__tests__/PendingDevices.test.tsx | 158 ++++++++++++ 5 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 ui/apps/console/src/pages/PendingDevices.tsx create mode 100644 ui/apps/console/src/pages/__tests__/PendingDevices.test.tsx diff --git a/ui/apps/console/src/App.tsx b/ui/apps/console/src/App.tsx index 6332754cda2..39a02956d94 100644 --- a/ui/apps/console/src/App.tsx +++ b/ui/apps/console/src/App.tsx @@ -39,6 +39,7 @@ const SSHIdentities = lazy(() => import("./pages/ssh-identities")); const DeviceDetails = lazy(() => import("./pages/DeviceDetails")); const AddDevice = lazy(() => import("./pages/AddDevice")); const Install = lazy(() => import("./pages/install")); +const PendingDevices = lazy(() => import("./pages/PendingDevices")); const SSHApproval = lazy(() => import("./pages/SSHApproval")); const Team = lazy(() => import("./pages/team")); const InstallKeys = lazy(() => import("./pages/install-keys")); @@ -235,6 +236,7 @@ export default function App() { /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/apps/console/src/components/layout/Sidebar.tsx b/ui/apps/console/src/components/layout/Sidebar.tsx index c93d2b97cbe..45d4005b80e 100644 --- a/ui/apps/console/src/components/layout/Sidebar.tsx +++ b/ui/apps/console/src/components/layout/Sidebar.tsx @@ -8,6 +8,7 @@ import { KeyIcon, Cog6ToothIcon, UsersIcon, + ClockIcon, CpuChipIcon, CommandLineIcon, LockClosedIcon, @@ -44,6 +45,11 @@ function buildSections(isIdentityMode: boolean): NavSection[] { label: "Devices", icon: , }, + { + to: "/pending-devices", + label: "Pending", + icon: , + }, { to: "/containers", label: "Containers", diff --git a/ui/apps/console/src/hooks/useEnrollmentSource.ts b/ui/apps/console/src/hooks/useEnrollmentSource.ts index 5e212150162..173340bc9bf 100644 --- a/ui/apps/console/src/hooks/useEnrollmentSource.ts +++ b/ui/apps/console/src/hooks/useEnrollmentSource.ts @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useInstallKeys } from "@/hooks/useInstallKeys"; import { resolveEnrollmentSource, @@ -7,15 +8,28 @@ import { const INSTALL_KEYS_PER_PAGE = 100; /** - * Resolves a device's install key digest to the source that enrolled it, returning null when the - * device carries no digest or the key is not among the namespace's first INSTALL_KEYS_PER_PAGE - * keys — the lookup is client-side over a single page, so a larger namespace resolves its tail to - * null rather than to a name. + * A resolver from a device's install key digest to the source that enrolled it, for a caller + * resolving more than one device: it subscribes to the namespace's keys once, where calling + * useEnrollmentSource per row would subscribe once per row. + * + * The lookup is client-side over a single page of keys, so a namespace with more than + * INSTALL_KEYS_PER_PAGE of them resolves its tail to null rather than to a name. */ -export function useEnrollmentSource( +export function useEnrollmentSourceResolver(): ( installKeyId: string | undefined, -): EnrollmentSource | null { +) => EnrollmentSource | null { const { installKeys } = useInstallKeys({ perPage: INSTALL_KEYS_PER_PAGE }); - return resolveEnrollmentSource(installKeyId, installKeys); + return useCallback( + (installKeyId: string | undefined) => + resolveEnrollmentSource(installKeyId, installKeys), + [installKeys], + ); +} + +/** The same resolution for a single device. Returns null when the key is not resolvable. */ +export function useEnrollmentSource( + installKeyId: string | undefined, +): EnrollmentSource | null { + return useEnrollmentSourceResolver()(installKeyId); } diff --git a/ui/apps/console/src/pages/PendingDevices.tsx b/ui/apps/console/src/pages/PendingDevices.tsx new file mode 100644 index 00000000000..b893d40bf4b --- /dev/null +++ b/ui/apps/console/src/pages/PendingDevices.tsx @@ -0,0 +1,230 @@ +import { useNavigate, Link } from "react-router-dom"; +import { ClockIcon, TicketIcon } from "@heroicons/react/24/outline"; +import { Button, Callout } from "@shellhub/design-system/primitives"; +import { useDevices, type NormalizedDevice } from "@/hooks/useDevices"; +import { useEnrollmentSourceResolver } from "@/hooks/useEnrollmentSource"; +import { usePaginatedListState } from "@/hooks/usePaginatedListState"; +import { useActionDialog } from "@/hooks/useActionDialog"; +import { useDeviceActionRunner } from "@/hooks/useDeviceActionRunner"; +import PageHeader from "@/components/common/PageHeader"; +import RestrictedAction from "@/components/common/RestrictedAction"; +import ActionDialog from "@/components/common/ActionDialog"; +import DataTable, { type Column } from "@/components/common/DataTable"; +import DistroIcon from "@/components/common/DistroIcon"; +import KeyValueChip from "@/components/common/KeyValueChip"; +import PlatformBadge from "@/components/common/PlatformBadge"; +import { type EnrollmentSource } from "@/pages/install-keys/helpers"; +import { apiErrorMessage } from "@/api/errors"; +import { formatDateFull } from "@/utils/date"; +import { PER_PAGE, pageCount } from "@/utils/pagination"; + +function EnrollmentSourceCell({ + installKeyId, + enrollment, +}: { + installKeyId?: string; + enrollment: EnrollmentSource | null; +}) { + if (!enrollment || !installKeyId) { + return —; + } + + if (enrollment.kind === "key") { + return ( + e.stopPropagation()} + className="text-xs font-medium text-text-primary hover:text-primary hover:underline" + > + {enrollment.name} + + ); + } + + return ( + + {enrollment.kind === "pairing" + ? "Pairing code" + : "Tenant-only registration"} + + ); +} + +type PendingParams = { page: number }; + +const DEFAULTS: PendingParams = { page: 1 }; + +/** + * The review queue: every device in the namespace still awaiting a decision, whichever install key + * enrolled it, so a pending registration is found without opening each key's activity in turn. + */ +export default function PendingDevices() { + const { params, setPage } = usePaginatedListState({ + defaults: DEFAULTS, + }); + const navigate = useNavigate(); + + const { devices, totalCount, isLoading, error } = useDevices({ + page: params.page, + perPage: PER_PAGE, + status: "pending", + sortBy: "created_at", + orderBy: "desc", + }); + + const resolveEnrollment = useEnrollmentSourceResolver(); + const deviceActions = useActionDialog(); + const runDeviceAction = useDeviceActionRunner(); + + const columns: Column[] = [ + { + key: "name", + header: "Device", + render: (device) => ( +
+ +
+
+ {device.name} +
+ {device.info?.pretty_name && ( +
+ {device.info.pretty_name} +
+ )} +
+ {device.identity?.mac && ( + + )} + {device.remote_addr && ( + + )} + {device.info?.arch && ( + + )} + {device.info?.platform && ( + + )} +
+
+
+ ), + }, + { + key: "created_at", + header: "Registered", + render: (device) => ( + + {formatDateFull(device.created_at)} + + ), + }, + { + key: "install_key", + header: "Install Key", + render: (device) => ( + + ), + }, + { + key: "actions", + header: "Review", + headerClassName: "text-right", + render: (device) => ( +
+ + + + + + +
+ ), + }, + ]; + + return ( +
+ } + overline="Provisioning" + title="Pending Devices" + description="Devices that registered and are waiting to be accepted into the namespace." + > + + + + {error ? ( + {apiErrorMessage(error)} + ) : ( + device.uid} + isLoading={isLoading} + loadingMessage="Loading pending devices..." + page={params.page} + totalPages={pageCount(totalCount)} + totalCount={totalCount} + itemLabel="device" + onPageChange={setPage} + onRowClick={(device) => void navigate(`/devices/${device.uid}`)} + rowClassName={() => "[&>td]:py-4"} + emptyState={ +
+ +

+ No devices are waiting for review +

+
+ } + /> + )} + + {deviceActions.action && ( + + )} +
+ ); +} diff --git a/ui/apps/console/src/pages/__tests__/PendingDevices.test.tsx b/ui/apps/console/src/pages/__tests__/PendingDevices.test.tsx new file mode 100644 index 00000000000..bff595f8f55 --- /dev/null +++ b/ui/apps/console/src/pages/__tests__/PendingDevices.test.tsx @@ -0,0 +1,158 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { createTestWrapper } from "@/tests/wrapper"; +import { mockDevice, mockInstallKey, mockNamespace } from "@/tests/factories"; +import { makeSdkError, mockSdkResponse, paginatedResponse } from "@/tests/sdk"; +import { seedAuthStore } from "@/tests/seedAuthStore"; +import PendingDevices from "../PendingDevices"; + +const sdk = vi.hoisted(() => + mockSdkGen({ + getDevices: vi.fn(), + installKeyList: vi.fn(), + getNamespace: vi.fn(), + acceptDevice: vi.fn(), + updateDeviceStatus: vi.fn(), + }), +); + +function pendingDevice(overrides: Parameters[0] = {}) { + return mockDevice({ + uid: "pending-uid-1", + name: "waiting-host", + status: "pending", + online: false, + install_key_id: "key-digest-1", + ...overrides, + }); +} + +function renderPage() { + return render(, { + wrapper: createTestWrapper({ initialEntries: ["/pending-devices"] }), + }); +} + +async function deviceRow(name = "waiting-host") { + return within(await screen.findByRole("row", { name: new RegExp(name) })); +} + +async function openConfirmation(name: RegExp) { + const user = userEvent.setup(); + await user.click((await deviceRow()).getByRole("button", { name })); + + return { user, dialog: await screen.findByRole("dialog") }; +} + +beforeEach(() => { + vi.clearAllMocks(); + seedAuthStore(); + sdk.getDevices.mockResolvedValue(paginatedResponse([pendingDevice()])); + sdk.installKeyList.mockResolvedValue( + paginatedResponse([ + mockInstallKey({ id: "key-digest-1", name: "fleet-key" }), + ]), + ); + sdk.getNamespace.mockResolvedValue(mockSdkResponse(mockNamespace())); + sdk.acceptDevice.mockResolvedValue(mockSdkResponse(undefined)); + sdk.updateDeviceStatus.mockResolvedValue(mockSdkResponse(undefined)); +}); + +describe("Pending devices", () => { + it("asks the API only for devices awaiting review", async () => { + renderPage(); + + await screen.findByText("waiting-host"); + + expect(sdk.getDevices).toHaveBeenCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ status: "pending" }), + }), + ); + }); + + it("names the install key each device registered with", async () => { + renderPage(); + + expect( + await screen.findByRole("link", { name: "fleet-key" }), + ).toHaveAttribute("href", "/install-keys/key-digest-1/activity"); + }); + + it("falls back to a dash when the install key is unknown", async () => { + sdk.installKeyList.mockResolvedValue(paginatedResponse([])); + + renderPage(); + + expect((await deviceRow()).getByText("—")).toBeInTheDocument(); + }); + + it("accepts a device only after the confirmation is confirmed", async () => { + renderPage(); + + const { user, dialog } = await openConfirmation(/^accept$/i); + + expect(within(dialog).getByText(/waiting-host/)).toBeInTheDocument(); + expect(sdk.acceptDevice).not.toHaveBeenCalled(); + + await user.click(within(dialog).getByRole("button", { name: /^accept$/i })); + + expect(sdk.acceptDevice).toHaveBeenCalledWith( + expect.objectContaining({ path: { uid: "pending-uid-1" } }), + ); + }); + + it("rejects a device only after the confirmation is confirmed", async () => { + renderPage(); + + const { user, dialog } = await openConfirmation(/^reject$/i); + + expect(within(dialog).getByText(/waiting-host/)).toBeInTheDocument(); + expect(sdk.updateDeviceStatus).not.toHaveBeenCalled(); + + await user.click(within(dialog).getByRole("button", { name: /^reject$/i })); + + expect(sdk.updateDeviceStatus).toHaveBeenCalledWith( + expect.objectContaining({ + path: { uid: "pending-uid-1", status: "reject" }, + }), + ); + }); + + it("marks the decision unavailable for a member without permission", async () => { + seedAuthStore({ role: "observer" }); + + renderPage(); + + const restricted = (await deviceRow()).getAllByTitle( + /don't have permission/i, + ); + + expect(restricted.length).toBeGreaterThan(0); + for (const wrapper of restricted) { + expect(wrapper).toHaveAttribute("aria-disabled", "true"); + } + }); + + it("reports a failure to load rather than an empty queue", async () => { + sdk.getDevices.mockRejectedValue(makeSdkError(500)); + + renderPage(); + + expect(await screen.findByRole("alert")).toBeInTheDocument(); + expect( + screen.queryByText(/no devices are waiting for review/i), + ).not.toBeInTheDocument(); + }); + + it("says the queue is clear when nothing is pending", async () => { + sdk.getDevices.mockResolvedValue(paginatedResponse([])); + + renderPage(); + + expect( + await screen.findByText(/no devices are waiting for review/i), + ).toBeInTheDocument(); + }); +}); From d6926e393be47d24502e0a0632153002c9684cbb Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:32:46 -0300 Subject: [PATCH 3/7] fix(ui): send the pending-device links to a page that exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both links pointed at /devices?status=pending. The devices list hides every status but accepted and forces the parameter back, so the click landed on the accepted list with no error and no explanation — the count on the card was real, the destination was not. AddDevice's link also called the destination a tab. There has not been one since acceptance moved to the install keys area, so the word was describing the page it failed to reach. Fixes: shellhub-io/shellhub#7043 --- ui/apps/console/src/pages/AddDevice.tsx | 4 +- ui/apps/console/src/pages/Dashboard.tsx | 10 +--- .../src/pages/__tests__/Dashboard.test.tsx | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 ui/apps/console/src/pages/__tests__/Dashboard.test.tsx diff --git a/ui/apps/console/src/pages/AddDevice.tsx b/ui/apps/console/src/pages/AddDevice.tsx index 170248461e7..ebcca2d34b8 100644 --- a/ui/apps/console/src/pages/AddDevice.tsx +++ b/ui/apps/console/src/pages/AddDevice.tsx @@ -529,10 +529,10 @@ export default function AddDevice() {
After installing, your device will appear in the{" "} - Pending tab + pending list {" "} and must be accepted before you can connect to it.
diff --git a/ui/apps/console/src/pages/Dashboard.tsx b/ui/apps/console/src/pages/Dashboard.tsx index 7c291339935..06cdc708503 100644 --- a/ui/apps/console/src/pages/Dashboard.tsx +++ b/ui/apps/console/src/pages/Dashboard.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from "react-router-dom"; import { ClockIcon, Squares2X2Icon, @@ -24,7 +23,6 @@ export default function Dashboard() { const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); const { stats, isLoading: statsLoading, error: statsError } = useStats(); - const navigate = useNavigate(); if (statsLoading) return null; @@ -32,10 +30,6 @@ export default function Dashboard() { return ; } - const goToPending = () => { - void navigate("/devices?status=pending"); - }; - return (
} title="Pending Devices" value={stats?.pending_devices ?? "--"} - linkLabel="View pending" - onClick={goToPending} + linkLabel="Review pending" + linkTo="/pending-devices" accent="text-accent-yellow" />
diff --git a/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx b/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx new file mode 100644 index 00000000000..489675d0f6a --- /dev/null +++ b/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { createTestWrapper } from "@/tests/wrapper"; +import { mockNamespace } from "@/tests/factories"; +import { mockSdkResponse, paginatedResponse } from "@/tests/sdk"; +import { seedAuthStore } from "@/tests/seedAuthStore"; +import Dashboard from "../Dashboard"; + +const sdk = vi.hoisted(() => + mockSdkGen({ + getStatusDevices: vi.fn(), + getNamespace: vi.fn(), + getSessions: vi.fn(), + }), +); + +vi.mock("@/components/common/CopyButton", async () => ({ + default: (await import("@/tests/mocks")).MockCopyButton, +})); + +function renderDashboard() { + return render(, { + wrapper: createTestWrapper({ initialEntries: ["/dashboard"] }), + }); +} + +beforeEach(() => { + vi.clearAllMocks(); + seedAuthStore(); + sdk.getNamespace.mockResolvedValue(mockSdkResponse(mockNamespace())); + sdk.getSessions.mockResolvedValue(paginatedResponse([])); + sdk.getStatusDevices.mockResolvedValue( + mockSdkResponse({ + registered_devices: 4, + online_devices: 2, + pending_devices: 3, + rejected_devices: 0, + }), + ); +}); + +describe("Dashboard", () => { + it("sends the pending devices card to the pending review queue", async () => { + renderDashboard(); + + const link = await screen.findByRole("link", { name: /review pending/i }); + + expect(link).toHaveAttribute("href", "/pending-devices"); + }); +}); From 4b0b34300e93b9ee71b1f792b5c946825d6e6c5b Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:32:54 -0300 Subject: [PATCH 4/7] refactor(ui): drop the devices list's unreachable status filter An effect forced the status back to accepted on every render, so the pending and rejected column sets, their tabs, the status constraint and the handler that switched between them could not be reached. The accept, reject and remove actions went with them: nothing could open the confirmation dialog, so the dialog and the two hooks behind it were dead as well. Keeping them was not free. The pending column set duplicated, line for line, the one the new review queue renders, so the same markup existed twice with only one copy reachable. The tab that used to switch status now links out: to the queue for what is waiting, and to install keys for how devices get admitted. The list is the accepted fleet and says so. --- .../pages/devices/__tests__/Devices.test.tsx | 66 ++-- ui/apps/console/src/pages/devices/index.tsx | 298 +++++------------- 2 files changed, 101 insertions(+), 263 deletions(-) diff --git a/ui/apps/console/src/pages/devices/__tests__/Devices.test.tsx b/ui/apps/console/src/pages/devices/__tests__/Devices.test.tsx index d88977fd8cc..24e8de42d2e 100644 --- a/ui/apps/console/src/pages/devices/__tests__/Devices.test.tsx +++ b/ui/apps/console/src/pages/devices/__tests__/Devices.test.tsx @@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, act, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; -import type { UseActionDialogResult } from "@/hooks/useActionDialog"; import { createTestWrapper } from "@/tests/wrapper"; import { mockDevice, mockNamespace } from "@/tests/factories"; import { mockSdkResponse, paginatedResponse } from "@/tests/sdk"; @@ -88,22 +87,6 @@ vi.mock("@/components/common/TagsPopover", () => ({ ), })); -vi.mock("@/components/common/ActionDialog", () => ({ - default: () => null, -})); - -const mockRequestAction = vi.fn(); -const mockDeviceActionsController: UseActionDialogResult = { - action: undefined, - actionKey: "closed", - requestAction: mockRequestAction, - close: vi.fn(), - handleSuccess: vi.fn(), -}; -vi.mock("@/hooks/useActionDialog", () => ({ - useActionDialog: vi.fn(() => mockDeviceActionsController), -})); - vi.mock("@/components/common/RestrictedAction", () => ({ default: ({ children }: { children: React.ReactNode }) => <>{children}, })); @@ -133,7 +116,6 @@ beforeEach(() => { sdk.pullTagFromDevice.mockResolvedValue(mockSdkResponse(undefined)); mockNavigate.mockReset(); mockManageTagsDrawer.mockReset(); - mockRequestAction.mockReset(); }); describe("Devices list", () => { @@ -145,22 +127,31 @@ describe("Devices list", () => { ).toBeInTheDocument(); }); - it("renders the Accepted tab and the Install Keys link, not the pending/rejected tabs", async () => { + it("shows the list is the accepted devices, with no status to switch", async () => { renderPage(); - expect( - await screen.findByRole("button", { name: "Accepted" }), - ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: "Install Keys" }), - ).toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: "Pending" }), - ).not.toBeInTheDocument(); + + expect(await screen.findByText("Accepted")).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "Rejected" }), ).not.toBeInTheDocument(); }); + it("links out to the pending queue", async () => { + renderPage(); + + expect( + await screen.findByRole("link", { name: "Pending" }), + ).toHaveAttribute("href", "/pending-devices"); + }); + + it("links out to the install keys", async () => { + renderPage(); + + expect( + await screen.findByRole("link", { name: "Install Keys" }), + ).toHaveAttribute("href", "/install-keys"); + }); + it("renders the search input", async () => { renderPage(); expect( @@ -278,12 +269,12 @@ describe("Devices list", () => { }); describe("URL hydration — URL params seed page state on mount", () => { - it("passes status from URL to the SDK", async () => { - renderPage(["/?status=pending&tags=a&tags=b&page=2"]); + it("always asks for accepted devices, whatever the URL says", async () => { + renderPage(["/?status=pending"]); await waitFor(() => { expect(sdk.getDevices).toHaveBeenCalledWith( expect.objectContaining({ - query: expect.objectContaining({ status: "pending" }), + query: expect.objectContaining({ status: "accepted" }), }), ); }); @@ -312,7 +303,7 @@ describe("Devices list", () => { }); }); - it("falls back to status=accepted and page=1 when URL has no params", async () => { + it("defaults to accepted devices and page 1 when the URL has no params", async () => { renderPage(["/"]); await waitFor(() => { expect(sdk.getDevices).toHaveBeenCalledWith( @@ -323,17 +314,6 @@ describe("Devices list", () => { }); }); - it("falls back to status=accepted for an invalid status value", async () => { - renderPage(["/?status=invalid"]); - await waitFor(() => { - expect(sdk.getDevices).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ status: "accepted" }), - }), - ); - }); - }); - it("passes no tag filter when no tags param is present", async () => { renderPage(["/"]); await waitFor(() => { diff --git a/ui/apps/console/src/pages/devices/index.tsx b/ui/apps/console/src/pages/devices/index.tsx index 4b7f85ce798..06b2bd4d9e8 100644 --- a/ui/apps/console/src/pages/devices/index.tsx +++ b/ui/apps/console/src/pages/devices/index.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, useEffect } from "react"; +import { useState, useMemo, useCallback } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useDevices, type NormalizedDevice } from "@/hooks/useDevices"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; @@ -7,7 +7,6 @@ import { usePaginatedListState } from "@/hooks/usePaginatedListState"; import { useNamespace } from "@/hooks/useNamespaces"; import { useAuthStore } from "@/stores/authStore"; import { useTerminalStore } from "@/stores/terminalStore"; -import { useActionDialog } from "@/hooks/useActionDialog"; import PageHeader from "@/components/common/PageHeader"; import ConnectDrawer from "@/components/ConnectDrawer"; import ManageTagsDrawer from "@/components/ManageTagsDrawer"; @@ -24,8 +23,6 @@ import { useAddDeviceTag, useRemoveDeviceTag, } from "@/hooks/useDeviceMutations"; -import ActionDialog from "@/components/common/ActionDialog"; -import { useDeviceActionRunner } from "@/hooks/useDeviceActionRunner"; import { PlusIcon, TagIcon, @@ -39,38 +36,26 @@ import { Callout, IconButton, } from "@shellhub/design-system/primitives"; -import { cn } from "@shellhub/design-system/cn"; import RestrictedAction from "@/components/common/RestrictedAction"; import { apiErrorMessage } from "@/api/errors"; import { PER_PAGE, pageCount } from "@/utils/pagination"; const SEARCH_DEBOUNCE_MS = 300; -const VALID_STATUSES = ["accepted", "pending", "rejected"] as const; - -/** Stable module-level constant — avoids a new object identity every render, - * which would invalidate the `update` useCallback in usePaginatedListState. */ -const CONSTRAINTS = { status: VALID_STATUSES } as const; - -type ValidStatus = (typeof VALID_STATUSES)[number]; - -const statusTabs: { label: string; value: ValidStatus }[] = [ - { label: "Accepted", value: "accepted" }, - { label: "Pending", value: "pending" }, - { label: "Rejected", value: "rejected" }, +const relatedPages: { label: string; to: string }[] = [ + { label: "Pending", to: "/pending-devices" }, + { label: "Install Keys", to: "/install-keys" }, ]; type DevicesParams = { page: number; search: string; - status: ValidStatus; tags: string[]; }; const DEFAULTS: DevicesParams = { page: 1, search: "", - status: "accepted", tags: [], }; @@ -81,17 +66,10 @@ type SortField = "name" | "last_seen"; * filter state held in the URL so a view can be shared. */ export default function Devices() { - const { - params, - setPage, - setSearch, - setFilter, - setArrayFilter, - mapArrayFilter, - } = usePaginatedListState({ - defaults: DEFAULTS, - constraints: CONSTRAINTS, - }); + const { params, setPage, setSearch, setArrayFilter, mapArrayFilter } = + usePaginatedListState({ + defaults: DEFAULTS, + }); const debouncedSearch = useDebouncedValue( params.search.trim(), @@ -100,9 +78,6 @@ export default function Devices() { const addDeviceTag = useAddDeviceTag(); const removeDeviceTag = useRemoveDeviceTag(); - const runDeviceAction = useDeviceActionRunner(); - const deviceActions = useActionDialog(); - const { requestAction: requestDeviceAction } = deviceActions; const [connectTarget, setConnectTarget] = useState<{ uid: string; name: string; @@ -117,7 +92,7 @@ export default function Devices() { const { devices, totalCount, isLoading, error, refetch } = useDevices({ page: params.page, perPage: PER_PAGE, - status: params.status, + status: "accepted", search: debouncedSearch, filterTags: params.tags, sortBy, @@ -131,18 +106,6 @@ export default function Devices() { const totalPages = pageCount(totalCount); const nsName = currentNamespace?.name ?? ""; - const visibleTabs = statusTabs.filter((tab) => tab.value === "accepted"); - - const handleStatusChange = (newStatus: ValidStatus) => { - setFilter("status", newStatus); - }; - - useEffect(() => { - if (params.status !== "accepted") { - setFilter("status", "accepted"); - } - }, [params.status, setFilter]); - const addFilterTag = useCallback( (tag: string) => { mapArrayFilter("tags", (tags) => @@ -161,7 +124,7 @@ export default function Devices() { }; const columns = useMemo[]>(() => { - const baseColumns: Column[] = [ + const [hostnameColumn, ...detailColumns]: Column[] = [ { key: "name", header: "Hostname", @@ -207,163 +170,80 @@ export default function Devices() { }, ]; - if (params.status === "accepted") { - return [ - { - key: "online", - header: "", - headerClassName: "w-12", - render: (device) => , - }, - baseColumns[0], // hostname - { - key: "sshid", - header: "SSHID", - render: (device) => { - const sshid = nsName - ? buildSshid(nsName, device.name) - : device.uid.substring(0, 8); - return ( -
- - {sshid} - - -
- ); - }, - }, - ...baseColumns.slice(1), // os, tags, last_seen - { - key: "connect", - header: "", - headerClassName: "w-20", - render: (device) => - device.online ? ( - - - - ) : ( - - Offline - - ), - }, - ]; - } - - if (params.status === "pending") { - return [ - ...baseColumns, - { - key: "actions", - header: "Actions", - headerClassName: "text-right", - render: (device) => ( -
- - - - - - + return [ + { + key: "online", + header: "", + headerClassName: "w-12", + render: (device) => , + }, + hostnameColumn, + { + key: "sshid", + header: "SSHID", + render: (device) => { + const sshid = nsName + ? buildSshid(nsName, device.name) + : device.uid.substring(0, 8); + return ( +
+ + {sshid} + +
- ), + ); }, - ]; - } - - return [ - ...baseColumns, + }, + ...detailColumns, { - key: "actions", - header: "Actions", - headerClassName: "text-right", - render: (device) => ( -
- + key: "connect", + header: "", + headerClassName: "w-20", + render: (device) => + device.online ? ( + - - - -
- ), + ) : ( + + Offline + + ), }, ]; }, [ - params.status, nsName, addFilterTag, - requestDeviceAction, addDeviceTag.mutateAsync, removeDeviceTag.mutateAsync, ]); @@ -391,30 +271,19 @@ export default function Devices() {
- {visibleTabs.map((tab) => ( - + {link.label} + + ))} - {/* Acceptance moved to the install-keys area, so the tab that used to be Pending now - navigates there instead of filtering (the arrow signals it leaves the list). */} - - Install Keys - -
@@ -503,17 +372,6 @@ export default function Devices() { } /> - {deviceActions.action && ( - - )} - setConnectTarget(null)} From c8f4c932becc04e7d8bfe515f022f7c27db3f3ea Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:33:04 -0300 Subject: [PATCH 5/7] fix(server): leave containers out of the pending device count The count fed a dashboard card whose only destination was the device list, and that list excludes connectors. A pending container was therefore counted and then absent from the page the count sends you to, which is the one thing a count next to a link must not do. The comparison is <> rather than IS DISTINCT FROM, and the difference matters: the device list builds its filter from the ne operator, which emits <>, and platform is nullable. A null-safe comparison here would count a row the list does not show, restoring the disagreement this removes. Matching the list exactly is the point, quirk included. Only the pending count moves. The registered count answers the same question for licence and firewall evaluation, so narrowing it would move a limit rather than a label. Pending containers keep their own path: the containers list still has a tab for them. --- openapi/spec/paths/admin@api@stats.yaml | 4 +++- openapi/spec/paths/api@stats.yaml | 4 +++- server/api/store/pg/stats.go | 3 ++- server/api/store/storetest/helpers.go | 8 ++++++++ server/api/store/storetest/stats_tests.go | 14 ++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/openapi/spec/paths/admin@api@stats.yaml b/openapi/spec/paths/admin@api@stats.yaml index 21845bd90ae..6a958e95e4a 100644 --- a/openapi/spec/paths/admin@api@stats.yaml +++ b/openapi/spec/paths/admin@api@stats.yaml @@ -34,7 +34,9 @@ get: type: integer minimum: 0 pending_devices: - description: Number of pending devices. + description: >- + Number of devices awaiting a decision, excluding containers, so the count + matches the device list it is read alongside. type: integer minimum: 0 rejected_devices: diff --git a/openapi/spec/paths/api@stats.yaml b/openapi/spec/paths/api@stats.yaml index 49f6a553a6f..d443ed35531 100644 --- a/openapi/spec/paths/api@stats.yaml +++ b/openapi/spec/paths/api@stats.yaml @@ -25,7 +25,9 @@ get: type: integer minimum: 0 pending_devices: - description: Number of pending devices + description: >- + Number of devices awaiting a decision, excluding containers, so the count + matches the device list this endpoint's consumers page through. type: integer minimum: 0 rejected_devices: diff --git a/server/api/store/pg/stats.go b/server/api/store/pg/stats.go index b597455a80d..a61580a4c92 100644 --- a/server/api/store/pg/stats.go +++ b/server/api/store/pg/stats.go @@ -98,7 +98,8 @@ func buildRegisteredDevicesQuery(db bun.IDB) *bun.SelectQuery { func buildPendingDevicesQuery(db bun.IDB) *bun.SelectQuery { return db.NewSelect(). Model((*entity.Device)(nil)). - Where("status = ?", "pending") + Where("status = ?", "pending"). + Where("platform <> ?", "connector") } func buildRejectedDevicesQuery(db bun.IDB) *bun.SelectQuery { diff --git a/server/api/store/storetest/helpers.go b/server/api/store/storetest/helpers.go index 7a9289de159..9190a025e2b 100644 --- a/server/api/store/storetest/helpers.go +++ b/server/api/store/storetest/helpers.go @@ -187,6 +187,14 @@ func WithDeviceStatus(status models.DeviceStatus) DeviceOption { } } +// WithDevicePlatform sets the device platform, which is what distinguishes a container +// (platform "connector") from a plain device. +func WithDevicePlatform(platform string) DeviceOption { + return func(d *models.Device) { + d.Info.Platform = platform + } +} + // WithDevicePublicKey sets the device public key func WithDevicePublicKey(publicKey string) DeviceOption { return func(d *models.Device) { diff --git a/server/api/store/storetest/stats_tests.go b/server/api/store/storetest/stats_tests.go index 3326ecc5713..1da03bf9ed0 100644 --- a/server/api/store/storetest/stats_tests.go +++ b/server/api/store/storetest/stats_tests.go @@ -37,6 +37,20 @@ func (s *Suite) TestGetStats(t *testing.T) { assert.Equal(t, 1, stats.PendingDevices) }) + t.Run("counts pending devices without pending containers", func(t *testing.T) { + require.NoError(t, s.provider.CleanDatabase(t)) + + tenant := s.CreateNamespace(t) + s.CreateDevice(t, WithTenantID(tenant), WithDeviceStatus("pending")) + s.CreateDevice(t, WithTenantID(tenant), WithDeviceStatus("pending"), WithDevicePlatform("connector")) + + stats, err := st.GetStats(ctx, scope.MustBounded(tenant)) + require.NoError(t, err) + require.NotNil(t, stats) + + assert.Equal(t, 1, stats.PendingDevices) + }) + t.Run("succeeds with specific tenantID", func(t *testing.T) { require.NoError(t, s.provider.CleanDatabase(t)) From daa319231dc0f0a5370d396722d6d2b95bed48d3 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:48:44 -0300 Subject: [PATCH 6/7] fix(ui): keep the first-run wizard off a namespace holding a pending container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wizard shows when the device counts are all zero. Pending stopped counting containers one commit ago, and registered and rejected still do, so the counts can now read empty for a namespace whose only entity is a pending container — and the dashboard would offer to connect a first device to someone who already has one waiting. Containers are asked for only once the counts already say empty, so the common path keeps the single stats request it had. --- ui/apps/console/src/hooks/useContainers.ts | 3 ++ ui/apps/console/src/pages/Dashboard.tsx | 9 ++++- .../src/pages/__tests__/Dashboard.test.tsx | 38 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/ui/apps/console/src/hooks/useContainers.ts b/ui/apps/console/src/hooks/useContainers.ts index 31d80e88b8d..b02cfc99d01 100644 --- a/ui/apps/console/src/hooks/useContainers.ts +++ b/ui/apps/console/src/hooks/useContainers.ts @@ -38,6 +38,7 @@ interface UseContainersParams { filterTags?: string[]; sortBy?: string; orderBy?: "asc" | "desc"; + enabled?: boolean; } /** @@ -51,6 +52,7 @@ export function useContainers({ filterTags = [], sortBy = "last_seen", orderBy = "desc", + enabled = true, }: UseContainersParams = {}) { const query: GetContainersData["query"] = { page, per_page: perPage }; if (status) query.status = status; @@ -64,6 +66,7 @@ export function useContainers({ const result = useQuery>({ queryKey: getContainersQueryKey(options), queryFn: paginatedQueryFn(getContainersSdk, options), + enabled, }); const containers = useMemo( diff --git a/ui/apps/console/src/pages/Dashboard.tsx b/ui/apps/console/src/pages/Dashboard.tsx index 06cdc708503..6932eebf830 100644 --- a/ui/apps/console/src/pages/Dashboard.tsx +++ b/ui/apps/console/src/pages/Dashboard.tsx @@ -7,6 +7,7 @@ import { import { useNamespace } from "@/hooks/useNamespaces"; import { useAuthStore } from "@/stores/authStore"; import { useStats } from "@/hooks/useStats"; +import { useContainers } from "@/hooks/useContainers"; import { hasAnyDevices } from "@/utils/stats"; import PageHeader from "@/components/common/PageHeader"; import StatCard from "@/components/common/StatCard"; @@ -24,9 +25,13 @@ export default function Dashboard() { const { namespace: currentNamespace } = useNamespace(tenantId); const { stats, isLoading: statsLoading, error: statsError } = useStats(); - if (statsLoading) return null; + const looksEmpty = !statsError && !!stats && !hasAnyDevices(stats); + const { totalCount: containerCount, isLoading: containersLoading } = + useContainers({ perPage: 1, enabled: looksEmpty }); - if (!statsError && stats && !hasAnyDevices(stats) && currentNamespace) { + if (statsLoading || (looksEmpty && containersLoading)) return null; + + if (looksEmpty && containerCount === 0 && currentNamespace) { return ; } diff --git a/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx b/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx index 489675d0f6a..bee584c5bfe 100644 --- a/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx +++ b/ui/apps/console/src/pages/__tests__/Dashboard.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import { createTestWrapper } from "@/tests/wrapper"; -import { mockNamespace } from "@/tests/factories"; +import { mockDevice, mockNamespace } from "@/tests/factories"; import { mockSdkResponse, paginatedResponse } from "@/tests/sdk"; import { seedAuthStore } from "@/tests/seedAuthStore"; import Dashboard from "../Dashboard"; @@ -11,6 +11,7 @@ const sdk = vi.hoisted(() => getStatusDevices: vi.fn(), getNamespace: vi.fn(), getSessions: vi.fn(), + getContainers: vi.fn(), }), ); @@ -29,6 +30,7 @@ beforeEach(() => { seedAuthStore(); sdk.getNamespace.mockResolvedValue(mockSdkResponse(mockNamespace())); sdk.getSessions.mockResolvedValue(paginatedResponse([])); + sdk.getContainers.mockResolvedValue(paginatedResponse([])); sdk.getStatusDevices.mockResolvedValue( mockSdkResponse({ registered_devices: 4, @@ -40,6 +42,40 @@ beforeEach(() => { }); describe("Dashboard", () => { + it("keeps the first-run wizard away from a namespace whose only device is a pending container", async () => { + sdk.getStatusDevices.mockResolvedValue( + mockSdkResponse({ + registered_devices: 0, + online_devices: 0, + pending_devices: 0, + rejected_devices: 0, + }), + ); + sdk.getContainers.mockResolvedValue(paginatedResponse([mockDevice()], 1)); + + renderDashboard(); + + expect(await screen.findByText(/pending devices/i)).toBeInTheDocument(); + expect( + screen.queryByText(/let's connect your first device/i), + ).not.toBeInTheDocument(); + }); + + it("shows the first-run wizard for a namespace with nothing in it", async () => { + sdk.getStatusDevices.mockResolvedValue( + mockSdkResponse({ + registered_devices: 0, + online_devices: 0, + pending_devices: 0, + rejected_devices: 0, + }), + ); + + renderDashboard(); + + expect(await screen.findByText(/let's connect your first device/i)).toBeInTheDocument(); + }); + it("sends the pending devices card to the pending review queue", async () => { renderDashboard(); From 251bba1dc99eaa88a41e9728c0359c2d649f0e81 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 5 Sep 2026 12:33:13 -0300 Subject: [PATCH 7/7] docs: show the pending queue where enrollments are reviewed Reviewing a pending enrollment described the key's activity page as the place it happens, "rather than in the device list". That was true when it was written and stops being true one commit earlier: the queue lists every device waiting on a decision across all keys, so the two are now alternatives rather than one correct answer. Troubleshooting still told readers to check a Pending tab, which has not existed since acceptance moved to install keys. --- .../docs/public/img/shots/pending-devices.png | Bin 0 -> 90783 bytes .../manage/devices/device-not-appearing.mdx | 2 +- .../src/pages/manage/devices/install-keys.mdx | 9 +++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 ui/apps/docs/public/img/shots/pending-devices.png diff --git a/ui/apps/docs/public/img/shots/pending-devices.png b/ui/apps/docs/public/img/shots/pending-devices.png new file mode 100644 index 0000000000000000000000000000000000000000..25965c302577d8ad0ef96f5f59aacdbdfcbb84f8 GIT binary patch literal 90783 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 XNgj5Y`Y+`Fa=|5)|# literal 0 HcmV?d00001 diff --git a/ui/apps/docs/src/pages/manage/devices/device-not-appearing.mdx b/ui/apps/docs/src/pages/manage/devices/device-not-appearing.mdx index c711feae7d3..fd0025fd09e 100644 --- a/ui/apps/docs/src/pages/manage/devices/device-not-appearing.mdx +++ b/ui/apps/docs/src/pages/manage/devices/device-not-appearing.mdx @@ -13,7 +13,7 @@ goes from most to least common. ## Is it in Pending? -Check the **Pending** tab before anything else. A device enrolled with only a tenant ID lands +Check the **Pending** list before anything else. A device enrolled with only a tenant ID lands there and waits to be accepted. It is not broken; it is queued. Accept it and it comes online. To stop needing to, use an diff --git a/ui/apps/docs/src/pages/manage/devices/install-keys.mdx b/ui/apps/docs/src/pages/manage/devices/install-keys.mdx index ece5367c107..9c5168a178f 100644 --- a/ui/apps/docs/src/pages/manage/devices/install-keys.mdx +++ b/ui/apps/docs/src/pages/manage/devices/install-keys.mdx @@ -123,8 +123,13 @@ ran. ## Reviewing a pending enrollment -A key in manual mode, and a key whose webhook deferred, leaves devices waiting. They are reviewed -on the key's activity page rather than in the device list. +A key in manual mode, and a key whose webhook deferred, leaves devices waiting. **Pending** in the +sidebar lists every device waiting on a decision, whichever key enrolled it, and names that key on +each row. The dashboard's pending count links to it. + + + +To see one key on its own — every device it enrolled, decided or not — open that key's activity.