From 441f60537200b3921b5d07711c89ff5e5c9735be Mon Sep 17 00:00:00 2001 From: luizhf42 Date: Mon, 31 Aug 2026 16:50:01 -0300 Subject: [PATCH 1/5] chore(ui): add orval codegen with custom mutator and faker/msw output Orval replaces `@hey-api/openapi-ts` as the OpenAPI client generator. The config enables react-query hooks, and MSW/faker mock generation from the spec. The custom mutator merges `fetchClient` and `fetchInterceptors` into a single function that handles auth, JWT expiry, 401/MFA, connectivity tracking, and error enrichment. `fetchWithResponse` is exported alongside for paginated hooks that need access to response headers. --- .gitignore | 1 + ui/apps/console/openapi-ts.config.ts | 27 - ui/apps/console/orval.config.ts | 36 + ui/apps/console/package.json | 5 +- .../api/__tests__/fetchInterceptors.test.ts | 246 -- .../src/api/__tests__/pagination.test.ts | 43 +- ui/apps/console/src/api/customInstance.ts | 177 ++ ui/apps/console/src/api/errors.ts | 4 +- ui/apps/console/src/api/fetchClient.ts | 10 - ui/apps/console/src/api/fetchInterceptors.ts | 99 - ui/apps/console/src/api/pagination.ts | 30 +- .../__tests__/useSupportIdentifier.test.ts | 83 - ui/apps/console/src/main.tsx | 1 - ui/apps/console/tsconfig.node.json | 2 +- ui/package-lock.json | 2214 +++++++++++++---- ui/scripts/generate-client.sh | 2 +- 16 files changed, 1906 insertions(+), 1074 deletions(-) delete mode 100644 ui/apps/console/openapi-ts.config.ts create mode 100644 ui/apps/console/orval.config.ts delete mode 100644 ui/apps/console/src/api/__tests__/fetchInterceptors.test.ts create mode 100644 ui/apps/console/src/api/customInstance.ts delete mode 100644 ui/apps/console/src/api/fetchClient.ts delete mode 100644 ui/apps/console/src/api/fetchInterceptors.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useSupportIdentifier.test.ts diff --git a/.gitignore b/.gitignore index a164ef7e8d3..48601173587 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ docker-compose.override.yml **/*.tsbuildinfo **/vite.config.d.ts **/openapi-ts.config.d.ts +**/orval.config.d.ts api_private_key api_public_key diff --git a/ui/apps/console/openapi-ts.config.ts b/ui/apps/console/openapi-ts.config.ts deleted file mode 100644 index f82e080081e..00000000000 --- a/ui/apps/console/openapi-ts.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig } from "@hey-api/openapi-ts"; - -const input = process.env.OPENAPI_SPEC_PATH; -if (!input) { - throw new Error( - "OPENAPI_SPEC_PATH is not set; run `npm run generate -w @shellhub/console`.", - ); -} - -export default defineConfig({ - input, - output: "src/client", - plugins: [ - "@hey-api/typescript", - "@hey-api/sdk", - { - name: "@hey-api/client-fetch", - runtimeConfigPath: "./src/api/fetchClient", - }, - { - name: "@tanstack/react-query", - queryOptions: true, - mutationOptions: true, - includeInEntry: true, - }, - ], -}); diff --git a/ui/apps/console/orval.config.ts b/ui/apps/console/orval.config.ts new file mode 100644 index 00000000000..222a3725b3c --- /dev/null +++ b/ui/apps/console/orval.config.ts @@ -0,0 +1,36 @@ +import { defineConfig } from "orval"; + +const input = process.env.OPENAPI_SPEC_PATH; +if (!input) { + throw new Error( + "OPENAPI_SPEC_PATH is not set; run `npm run generate -w @shellhub/console`.", + ); +} + +export default defineConfig({ + shellhub: { + input: { + target: input, + }, + output: { + target: "./src/client/api.ts", + schemas: "./src/client/model", + client: "react-query", + httpClient: "fetch", + mode: "single", + clean: true, + override: { + mutator: { + path: "./src/api/customInstance.ts", + name: "customInstance", + }, + fetch: { + includeHttpResponseReturnType: false, + }, + query: { + signal: true, + }, + }, + }, + }, +}); diff --git a/ui/apps/console/package.json b/ui/apps/console/package.json index 11ca00e0d77..c9bb4a24d5a 100644 --- a/ui/apps/console/package.json +++ b/ui/apps/console/package.json @@ -16,7 +16,6 @@ "@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", - "@hey-api/openapi-ts": "^0.99.0", "@hookform/resolvers": "^3.10.0", "@shellhub/design-system": "*", "@stripe/react-stripe-js": "^3.1.1", @@ -44,6 +43,8 @@ "@tanstack/react-query-devtools": "^5.102.3", "@types/node-rsa": "^1.1.4", "@types/sshpk": "^1.17.5", - "ansi-styles": "^5.2.0" + "ansi-styles": "^5.2.0", + "msw": "^2.15.0", + "orval": "^8.27.0" } } diff --git a/ui/apps/console/src/api/__tests__/fetchInterceptors.test.ts b/ui/apps/console/src/api/__tests__/fetchInterceptors.test.ts deleted file mode 100644 index 80ca0c57e4a..00000000000 --- a/ui/apps/console/src/api/__tests__/fetchInterceptors.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { client } from "@/client/client.gen"; -import { useAuthStore } from "@/stores/authStore"; -import { useConnectivityStore } from "@/stores/connectivityStore"; -import { isSdkError } from "@/api/errors"; -import "@/api/fetchInterceptors"; - -const GRACE_PERIOD_MS = 5000; - -function makeJwt(exp: number): string { - const header = btoa(JSON.stringify({ alg: "HS256" })); - const payload = btoa(JSON.stringify({ exp })); - return `${header}.${payload}.sig`; -} - -function futureExp() { - return Math.floor(Date.now() / 1000) + 3600; -} - -function pastExp() { - return Math.floor(Date.now() / 1000) - 60; -} - -function respondWith(status: number, headers: Record = {}) { - const fetchMock = vi.fn().mockImplementation((request: Request) => { - const response = new Response(JSON.stringify({}), { - status, - headers: { "Content-Type": "application/json", ...headers }, - }); - Object.defineProperty(response, "url", { value: request.url }); - return Promise.resolve(response); - }); - vi.stubGlobal("fetch", fetchMock); - return fetchMock; -} - -function failToConnect() { - const fetchMock = vi.fn().mockRejectedValue(new TypeError("Failed to fetch")); - vi.stubGlobal("fetch", fetchMock); - return fetchMock; -} - -function setLocation(search = "") { - Object.defineProperty(window, "location", { - writable: true, - value: { href: "", search, replace: vi.fn() }, - }); -} - -beforeEach(() => { - useAuthStore.setState({ - token: null, - user: null, - userId: null, - email: null, - tenant: null, - role: null, - name: null, - loading: false, - error: null, - mfaToken: null, - }); - - useConnectivityStore.getState().markUp(); - setLocation(); -}); - -afterEach(() => { - if (vi.isFakeTimers()) vi.advanceTimersByTime(GRACE_PERIOD_MS); - vi.useRealTimers(); - vi.unstubAllGlobals(); - vi.restoreAllMocks(); -}); - -describe("request interceptor", () => { - it("attaches the bearer token when the token is valid", async () => { - const token = makeJwt(futureExp()); - useAuthStore.setState({ token }); - const fetchMock = respondWith(200); - - await client.get({ url: "/test" }); - - const request = fetchMock.mock.calls[0][0] as Request; - expect(request.headers.get("Authorization")).toBe(`Bearer ${token}`); - }); - - it("sends no Authorization header when there is no token", async () => { - const fetchMock = respondWith(200); - - await client.get({ url: "/test" }); - - const request = fetchMock.mock.calls[0][0] as Request; - expect(request.headers.get("Authorization")).toBeNull(); - }); - - it.each([ - ["expired", () => makeJwt(pastExp())], - ["malformed", () => "not-a-jwt"], - ])("rejects a %s token before it reaches the network, and logs out", async (_label, makeToken) => { - vi.useFakeTimers(); - useAuthStore.setState({ token: makeToken() }); - const fetchMock = respondWith(200); - - await expect(client.get({ url: "/test", throwOnError: true })).rejects.toThrow("Token expired"); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(useAuthStore.getState().token).toBeNull(); - expect(window.location.href).toBe("/login"); - }); - - it("keeps the session on an expired token during a token login", async () => { - vi.useFakeTimers(); - setLocation("?token=abc"); - useAuthStore.setState({ token: makeJwt(pastExp()) }); - respondWith(200); - - await expect(client.get({ url: "/test", throwOnError: true })).rejects.toThrow("Token expired"); - - expect(useAuthStore.getState().token).not.toBeNull(); - expect(window.location.href).toBe(""); - }); - - it("leaves the API marked up when the token is rejected before the network", async () => { - vi.useFakeTimers(); - useAuthStore.setState({ token: makeJwt(pastExp()) }); - respondWith(200); - - await client.get({ url: "/test" }); - - vi.advanceTimersByTime(GRACE_PERIOD_MS); - expect(useConnectivityStore.getState().apiReachable).toBe(true); - }); -}); - -describe("response interceptor", () => { - it("logs out and redirects on 401 from a non-login route", async () => { - useAuthStore.setState({ token: makeJwt(futureExp()) }); - respondWith(401); - - await client.get({ url: "/test" }); - - expect(useAuthStore.getState().token).toBeNull(); - expect(window.location.href).toBe("/login"); - }); - - it("keeps the session on 401 from the login route", async () => { - useAuthStore.setState({ token: makeJwt(futureExp()) }); - respondWith(401); - - await client.post({ url: "/api/login" }); - - expect(useAuthStore.getState().token).not.toBeNull(); - expect(window.location.href).not.toBe("/login"); - }); - - it("marks the API up again on a successful response", async () => { - useConnectivityStore.getState().markDown(); - respondWith(200); - - await client.get({ url: "/test" }); - - expect(useConnectivityStore.getState().apiReachable).toBe(true); - }); - - it("stores the MFA token a 401 carries instead of logging out", async () => { - useAuthStore.setState({ token: makeJwt(futureExp()) }); - respondWith(401, { "x-mfa-token": "mfa-temp-token-456" }); - - await client.get({ url: "/test" }); - - expect(useAuthStore.getState().mfaToken).toBe("mfa-temp-token-456"); - expect(useAuthStore.getState().token).not.toBeNull(); - }); - - it("ignores an MFA token on a status other than 401", async () => { - const token = makeJwt(futureExp()); - useAuthStore.setState({ token }); - respondWith(403, { "x-mfa-token": "should-be-ignored" }); - - await client.get({ url: "/test" }); - - expect(useAuthStore.getState().mfaToken).toBeNull(); - expect(useAuthStore.getState().token).toBe(token); - }); -}); - -describe("connectivity tracking", () => { - it("marks the API down after the grace period when the request cannot connect", async () => { - vi.useFakeTimers(); - failToConnect(); - - await client.get({ url: "/test" }); - - expect(useConnectivityStore.getState().apiReachable).toBe(true); - - vi.advanceTimersByTime(GRACE_PERIOD_MS); - expect(useConnectivityStore.getState().apiReachable).toBe(false); - }); - - it.each([502, 503, 504])("marks the API down after the grace period on %i", async (status) => { - vi.useFakeTimers(); - respondWith(status); - - await client.get({ url: "/test" }); - - vi.advanceTimersByTime(GRACE_PERIOD_MS); - expect(useConnectivityStore.getState().apiReachable).toBe(false); - }); - - it.each([400, 404, 422])("leaves the API marked up on %i", async (status) => { - vi.useFakeTimers(); - respondWith(status); - - await client.get({ url: "/test" }); - - vi.advanceTimersByTime(GRACE_PERIOD_MS); - expect(useConnectivityStore.getState().apiReachable).toBe(true); - }); - - it("cancels the pending mark-down when a success arrives inside the grace period", async () => { - vi.useFakeTimers(); - failToConnect(); - await client.get({ url: "/test" }); - - vi.advanceTimersByTime(2000); - expect(useConnectivityStore.getState().apiReachable).toBe(true); - - respondWith(200); - await client.get({ url: "/test" }); - - vi.advanceTimersByTime(GRACE_PERIOD_MS); - expect(useConnectivityStore.getState().apiReachable).toBe(true); - }); -}); - -describe("error interceptor", () => { - it("attaches the status and headers that isSdkError reads", async () => { - respondWith(409, { "x-account-lockout": "60" }); - - const { error } = await client.get({ url: "/test" }); - - expect(isSdkError(error)).toBe(true); - expect((error as { status: number }).status).toBe(409); - expect((error as { headers: Headers }).headers.get("x-account-lockout")).toBe("60"); - }); -}); diff --git a/ui/apps/console/src/api/__tests__/pagination.test.ts b/ui/apps/console/src/api/__tests__/pagination.test.ts index ce426b71907..2928169dd56 100644 --- a/ui/apps/console/src/api/__tests__/pagination.test.ts +++ b/ui/apps/console/src/api/__tests__/pagination.test.ts @@ -1,36 +1,21 @@ -import { describe, it, expect, vi } from "vitest"; -import { paginatedQueryFn } from "../pagination"; +import { describe, it, expect } from "vitest"; +import { totalCount } from "../pagination"; -function mockSdkFn(data: unknown[], headers: Record) { - return vi.fn().mockResolvedValue({ - data, - response: { headers: new Headers(headers) }, +describe("totalCount", () => { + it("returns 0 for undefined", () => { + expect(totalCount(undefined)).toBe(0); }); -} -describe("paginatedQueryFn", () => { - it("returns data and totalCount from X-Total-Count header", async () => { - const devices = [{ uid: "1" }, { uid: "2" }]; - const sdkFn = mockSdkFn(devices, { "X-Total-Count": "42" }); - - const queryFn = paginatedQueryFn(sdkFn, { query: { page: 1 } }); - const result = await queryFn(); - - expect(result).toEqual({ data: devices, totalCount: 42 }); - expect(sdkFn).toHaveBeenCalledWith({ query: { page: 1 }, throwOnError: true }); + it("returns 0 when the property is absent", () => { + expect(totalCount([1, 2, 3])).toBe(0); }); - it("defaults totalCount to 0 when header is missing", async () => { - const sdkFn = mockSdkFn([], {}); - - const result = await paginatedQueryFn(sdkFn, {})(); - - expect(result.totalCount).toBe(0); - }); - - it("propagates SDK errors thrown with throwOnError", async () => { - const sdkFn = vi.fn().mockRejectedValue(new Error("network failure")); - - await expect(paginatedQueryFn(sdkFn, {})()).rejects.toThrow("network failure"); + it("reads a non-enumerable totalCount property", () => { + const data = [1, 2, 3]; + Object.defineProperty(data, "totalCount", { + value: 42, + enumerable: false, + }); + expect(totalCount(data)).toBe(42); }); }); diff --git a/ui/apps/console/src/api/customInstance.ts b/ui/apps/console/src/api/customInstance.ts new file mode 100644 index 00000000000..b84fba932b0 --- /dev/null +++ b/ui/apps/console/src/api/customInstance.ts @@ -0,0 +1,177 @@ +import type { SdkHttpError } from "./errors"; +import { useAuthStore } from "@/stores/authStore"; +import { useConnectivityStore } from "@/stores/connectivityStore"; + +/** Error shape threaded through every generated hook. */ +export type ErrorType<_Error> = SdkHttpError; + +/** Request body passthrough — Orval requires this export. */ +export type BodyType = BodyData; + +const baseURL = window.location.origin; + +function isTokenExpired(token: string): boolean { + try { + const payload: unknown = JSON.parse(atob(token.split(".")[1])); + if (typeof payload === "object" && payload !== null && "exp" in payload) { + const { exp } = payload; + return typeof exp === "number" && exp * 1000 < Date.now(); + } + return false; + } catch { + return true; + } +} + +const GRACE_PERIOD_MS = 5000; +let downTimer: ReturnType | null = null; + +function scheduleMarkDown() { + if (downTimer) return; + downTimer = setTimeout(() => { + downTimer = null; + useConnectivityStore.getState().markDown(); + }, GRACE_PERIOD_MS); +} + +function cancelMarkDown() { + if (downTimer) { + clearTimeout(downTimer); + downTimer = null; + } +} + +function isApiDown(status: number): boolean { + return status === 502 || status === 503 || status === 504; +} + +interface MutatorOptions { + method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + params?: Record; + body?: BodyType; + headers?: Record; + signal?: AbortSignal; +} + +async function doFetch( + url: string, + { method, params, body, headers, signal }: MutatorOptions, +): Promise { + let targetUrl = `${baseURL}${url}`; + + if (params) { + const search = new URLSearchParams(params).toString(); + if (search) targetUrl += `?${search}`; + } + + const requestHeaders: Record = { ...headers }; + + if (typeof body === "string") { + requestHeaders["Content-Type"] ??= "application/json"; + } + + const token = useAuthStore.getState().token; + const isTokenLogin = new URLSearchParams(window.location.search).has("token"); + if (token) { + if (isTokenExpired(token)) { + if (!isTokenLogin) { + useAuthStore.getState().logout(); + window.location.href = "/login"; + } + throw new Error("Token expired"); + } + requestHeaders["Authorization"] = `Bearer ${token}`; + } + + let response: Response; + try { + response = await fetch(targetUrl, { + method, + headers: requestHeaders, + body, + signal, + }); + } catch (error) { + scheduleMarkDown(); + throw error; + } + + if (!isApiDown(response.status)) { + cancelMarkDown(); + if (!useConnectivityStore.getState().apiReachable) { + useConnectivityStore.getState().markUp(); + } + } + + if (response.status === 401) { + const mfaToken = response.headers.get("x-mfa-token"); + if (mfaToken) { + useAuthStore.getState().setMfaToken(mfaToken); + } else { + const isLoginRequest = response.url.includes("/api/login"); + if (!isLoginRequest && !isTokenLogin) { + useAuthStore.getState().logout(); + window.location.href = "/login"; + } + } + } else if (isApiDown(response.status)) { + scheduleMarkDown(); + } + + if (!response.ok) { + const errorBody: unknown = await response.json().catch(() => ({})); + const fields = + typeof errorBody === "object" && errorBody !== null + ? (errorBody as Record) + : {}; + const error = Object.assign(new Error(String(response.status)), { + ...fields, + status: response.status, + headers: response.headers, + }); + throw error; + } + + return response; +} + +/** + * Orval custom mutator — every generated SDK function calls this. `body` is sent as given, so a + * JSON payload must arrive already serialized; the generated functions stringify their own. + */ +export const customInstance = async ( + url: string, + options: MutatorOptions, +): Promise => { + const response = await doFetch(url, options); + if (response.status === 204) return undefined as T; + const ct = response.headers.get("content-type") ?? ""; + if (ct.includes("application/json")) { + const data = (await response.json()) as T; + if (Array.isArray(data)) { + const tc = response.headers.get("X-Total-Count"); + if (tc) { + Object.defineProperty(data, "totalCount", { + value: parseInt(tc, 10), + enumerable: false, + }); + } + } + return data; + } + return (await response.text()) as unknown as T; +}; + +export default customInstance; + +/** Like `customInstance` but also exposes the response headers (for `X-Total-Count`). */ +export async function fetchWithHeaders( + url: string, + options: MutatorOptions, +): Promise<{ data: T; headers: Headers }> { + const response = await doFetch(url, options); + if (response.status === 204) + return { data: undefined as T, headers: response.headers }; + const data = (await response.json()) as T; + return { data, headers: response.headers }; +} diff --git a/ui/apps/console/src/api/errors.ts b/ui/apps/console/src/api/errors.ts index 2eaf87650e0..652eb9c9134 100644 --- a/ui/apps/console/src/api/errors.ts +++ b/ui/apps/console/src/api/errors.ts @@ -1,7 +1,5 @@ /** - * Shape attached to errors by the fetch error interceptor in fetchInterceptors.ts. - * The interceptor monkey-patches `.status` and `.headers` onto the parsed - * response body before it is thrown by the SDK with `throwOnError: true`. + * Shape attached to errors thrown by the custom fetch mutator in customInstance.ts. * * `message` and `fields` come from the body the API sends. `message` is for API clients — the * console renders its own copy, keyed by status; see `apiErrorMessage`. diff --git a/ui/apps/console/src/api/fetchClient.ts b/ui/apps/console/src/api/fetchClient.ts deleted file mode 100644 index eed10b2a660..00000000000 --- a/ui/apps/console/src/api/fetchClient.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CreateClientConfig } from "../client/client.gen"; - -/** - * Points the generated SDK at the origin serving the console. The API is behind the same gateway - * as the UI, so there is no separate host to configure and none to get wrong. - */ -export const createClientConfig: CreateClientConfig = (config) => ({ - ...config, - baseUrl: `${window.location.protocol}//${window.location.host}`, -}); diff --git a/ui/apps/console/src/api/fetchInterceptors.ts b/ui/apps/console/src/api/fetchInterceptors.ts deleted file mode 100644 index faf35a67c64..00000000000 --- a/ui/apps/console/src/api/fetchInterceptors.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { client } from "../client/client.gen"; -import { useAuthStore } from "../stores/authStore"; -import { useConnectivityStore } from "../stores/connectivityStore"; - -function isTokenExpired(token: string): boolean { - try { - const payload: unknown = JSON.parse(atob(token.split(".")[1])); - if (typeof payload === "object" && payload !== null && "exp" in payload) { - const { exp } = payload; - return typeof exp === "number" && exp * 1000 < Date.now(); - } - return false; - } catch { - return true; - } -} - -class ExpiredTokenError extends Error {} - -const GRACE_PERIOD_MS = 5000; -let downTimer: ReturnType | null = null; - -function scheduleMarkDown() { - if (downTimer) return; - downTimer = setTimeout(() => { - downTimer = null; - useConnectivityStore.getState().markDown(); - }, GRACE_PERIOD_MS); -} - -function cancelMarkDown() { - if (downTimer) { - clearTimeout(downTimer); - downTimer = null; - } -} - -function isApiDown(status: number): boolean { - return status === 502 || status === 503 || status === 504; -} - -client.interceptors.request.use((request) => { - const token = useAuthStore.getState().token; - if (token) { - if (isTokenExpired(token)) { - const isTokenLogin = new URLSearchParams(window.location.search).has( - "token", - ); - if (!isTokenLogin) { - useAuthStore.getState().logout(); - window.location.href = "/login"; - } - throw new ExpiredTokenError("Token expired"); - } - request.headers.set("Authorization", `Bearer ${token}`); - } - return request; -}); - -client.interceptors.response.use((response) => { - if (!isApiDown(response.status)) { - cancelMarkDown(); - if (!useConnectivityStore.getState().apiReachable) { - useConnectivityStore.getState().markUp(); - } - } - - if (response.status === 401) { - const mfaToken = response.headers.get("x-mfa-token"); - if (mfaToken) { - useAuthStore.getState().setMfaToken(mfaToken); - } else { - const isLoginRequest = response.url.includes("/api/login"); - const isTokenLogin = new URLSearchParams(window.location.search).has( - "token", - ); - if (!isLoginRequest && !isTokenLogin) { - useAuthStore.getState().logout(); - window.location.href = "/login"; - } - } - } else if (isApiDown(response.status)) { - scheduleMarkDown(); - } - - return response; -}); - -client.interceptors.error.use((error, response) => { - if (!response) { - if (!(error instanceof ExpiredTokenError)) scheduleMarkDown(); - return error; - } - - const enriched = typeof error === "object" && error !== null ? error : {}; - (enriched as Record).status = response.status; - (enriched as Record).headers = response.headers; - return enriched; -}); diff --git a/ui/apps/console/src/api/pagination.ts b/ui/apps/console/src/api/pagination.ts index a7ad4c1831f..5ef8422fd83 100644 --- a/ui/apps/console/src/api/pagination.ts +++ b/ui/apps/console/src/api/pagination.ts @@ -1,28 +1,8 @@ /** - * A page of results together with the total the filter matched, which is what the pager needs - * and the page itself cannot say. + * Reads the non-enumerable `totalCount` that `customInstance` attaches to array responses carrying + * an `X-Total-Count` header. Returns 0 when the data is undefined or the property is absent. */ -export interface PaginatedResult { - data: T[]; - totalCount: number; -} - -type SdkListFn = ( - options: O & { throwOnError: true }, -) => Promise<{ data: T[]; response: Response }>; - -/** - * Wraps a generated list call as a query function that also reads the total from X-Total-Count. - * The count lives in a header rather than the body, so a plain SDK call cannot page; every - * paginated hook goes through here instead of parsing the header again. - */ -export function paginatedQueryFn( - sdkFn: SdkListFn, - options: O, -): () => Promise> { - return async () => { - const { data, response } = await sdkFn({ ...options, throwOnError: true }); - const totalCount = parseInt(response.headers.get("X-Total-Count") ?? "0", 10); - return { data, totalCount }; - }; +export function totalCount(data: unknown[] | undefined): number { + if (!data) return 0; + return (data as unknown as { totalCount?: number }).totalCount ?? 0; } diff --git a/ui/apps/console/src/hooks/__tests__/useSupportIdentifier.test.ts b/ui/apps/console/src/hooks/__tests__/useSupportIdentifier.test.ts deleted file mode 100644 index 8d86fddcf82..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useSupportIdentifier.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { useSupportIdentifier } from "../useSupportIdentifier"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getNamespaceSupport: vi.fn(), - }), -); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useSupportIdentifier", () => { - describe("when enabled=false", () => { - it("never fires the query and returns null identifier", () => { - const { result } = renderHookWithClient(() => - useSupportIdentifier("tenant-123", false), - ); - - expect(sdk.getNamespaceSupport).not.toHaveBeenCalled(); - expect(result.current.identifier).toBeNull(); - expect(result.current.isLoading).toBe(false); - expect(result.current.isError).toBe(false); - }); - }); - - describe("when tenantId is empty", () => { - it("does not fire the query when tenantId is empty string", () => { - renderHookWithClient(() => useSupportIdentifier("", true)); - - expect(sdk.getNamespaceSupport).not.toHaveBeenCalled(); - }); - - it("does not fire the query when tenantId is null", () => { - renderHookWithClient(() => useSupportIdentifier(null, true)); - - expect(sdk.getNamespaceSupport).not.toHaveBeenCalled(); - }); - - it("returns null identifier when disabled by empty tenantId", () => { - const { result } = renderHookWithClient(() => - useSupportIdentifier("", true), - ); - - expect(result.current.identifier).toBeNull(); - expect(result.current.isLoading).toBe(false); - }); - }); - - describe("when enabled with a valid tenant", () => { - it("returns the identifier from the response", async () => { - sdk.getNamespaceSupport.mockResolvedValue( - mockSdkResponse({ identifier: "abc123" }), - ); - - const { result } = renderHookWithClient(() => - useSupportIdentifier("tenant-123", true), - ); - - await waitFor(() => expect(result.current.identifier).toBe("abc123")); - expect(result.current.isLoading).toBe(false); - expect(result.current.isError).toBe(false); - }); - }); - - describe("retry policy", () => { - it("retries the query exactly once on failure (transient blip recovery)", async () => { - sdk.getNamespaceSupport.mockRejectedValue(new Error("network error")); - - const { result } = renderHookWithClient(() => - useSupportIdentifier("tenant-123", true), - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(sdk.getNamespaceSupport).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/ui/apps/console/src/main.tsx b/ui/apps/console/src/main.tsx index e7fcb50e7a1..5f14f2407e2 100644 --- a/ui/apps/console/src/main.tsx +++ b/ui/apps/console/src/main.tsx @@ -9,7 +9,6 @@ import { ClipboardProvider } from "./components/common/ClipboardProvider"; import { loadConfig } from "./env"; import { queryClient } from "./api/queryClient"; import "./stores/themeStore"; -import "./api/fetchInterceptors"; import "@xterm/xterm/css/xterm.css"; import "font-logos/assets/font-logos.css"; import "./index.css"; diff --git a/ui/apps/console/tsconfig.node.json b/ui/apps/console/tsconfig.node.json index 40b9b5f38d0..ba54fd5e2ba 100644 --- a/ui/apps/console/tsconfig.node.json +++ b/ui/apps/console/tsconfig.node.json @@ -18,5 +18,5 @@ "noFallthroughCasesInSwitch": true, "types": ["node"] }, - "include": ["vite.config.ts", "openapi-ts.config.ts"] + "include": ["vite.config.ts", "orval.config.ts"] } diff --git a/ui/package-lock.json b/ui/package-lock.json index 9450f7835c9..971b2d006a5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -61,7 +61,6 @@ "@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", - "@hey-api/openapi-ts": "^0.99.0", "@hookform/resolvers": "^3.10.0", "@shellhub/design-system": "*", "@stripe/react-stripe-js": "^3.1.1", @@ -89,7 +88,9 @@ "@tanstack/react-query-devtools": "^5.102.3", "@types/node-rsa": "^1.1.4", "@types/sshpk": "^1.17.5", - "ansi-styles": "^5.2.0" + "ansi-styles": "^5.2.0", + "msw": "^2.15.0", + "orval": "^8.27.0" } }, "apps/console/node_modules/@hookform/resolvers": { @@ -2782,141 +2783,71 @@ "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, - "node_modules/@hey-api/codegen-core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.9.1.tgz", - "integrity": "sha512-s97jL1dgTMuiMHv2BZ1X4Tgd99Mf9GOvGdNqNcGwIMmnR+PgYNoraj4Zvp134MKsNCap/m7k0r0vKKnl56pj4w==", + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "dev": true, "license": "MIT", "dependencies": { - "@hey-api/types": "0.1.4", - "ansi-colors": "4.1.3", - "c12": "3.3.4", - "color-support": "1.1.3" - }, - "engines": { - "node": ">=22.18.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "@shikijs/engine-oniguruma": "^3.23.0", + "@shikijs/langs": "^3.23.0", + "@shikijs/themes": "^3.23.0", + "@shikijs/types": "^3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@hey-api/json-schema-ref-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.4.4.tgz", - "integrity": "sha512-otmd+zCxbYVBIp/mlMTnGkvlNYLkVKgs3VOIq0kSnenhB1+fRwLPQIeSwyWM6E51oXhUedkYjVsVpkVexeuJOA==", + "node_modules/@gerrit0/mini-shiki/node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "7.1.3", - "@types/json-schema": "7.0.15", - "js-yaml": "4.2.0" - }, - "engines": { - "node": ">=22.18.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@hey-api/openapi-ts": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.99.0.tgz", - "integrity": "sha512-SePU/5oEWWkvUBYmvzdYRctseoLuskyhs4ET0RvLIcmzc8yLQoA2R+KtBIQ8bPsoSUB0m4E5SmBnl6aGSA0szQ==", + "node_modules/@gerrit0/mini-shiki/node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.9.1", - "@hey-api/json-schema-ref-parser": "1.4.4", - "@hey-api/shared": "0.5.0", - "@hey-api/spec-types": "0.2.0", - "@hey-api/types": "0.1.4", - "@lukeed/ms": "2.0.2", - "ansi-colors": "4.1.3", - "color-support": "1.1.3", - "commander": "15.0.0", - "get-tsconfig": "4.14.0" - }, - "bin": { - "openapi-ts": "bin/run.js" - }, - "engines": { - "node": ">=22.18.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" - }, - "peerDependencies": { - "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" + "@shikijs/types": "3.23.0" } }, - "node_modules/@hey-api/openapi-ts/node_modules/commander": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "node_modules/@gerrit0/mini-shiki/node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=22.12.0" + "dependencies": { + "@shikijs/types": "3.23.0" } }, - "node_modules/@hey-api/shared": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.5.0.tgz", - "integrity": "sha512-JN/j4Ebh4cJGYIQ5cwWuqe7GeSUyQoz7oC51WqyhKOcrejK6DKZMDkshc5d1eKTRuRL+rjozuRcoUaZZn2DGPw==", + "node_modules/@gerrit0/mini-shiki/node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.9.1", - "@hey-api/json-schema-ref-parser": "1.4.4", - "@hey-api/spec-types": "0.2.0", - "@hey-api/types": "0.1.4", - "ansi-colors": "4.1.3", - "cross-spawn": "7.0.6", - "open": "11.0.0", - "semver": "7.8.4" - }, - "engines": { - "node": ">=22.18.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" - } - }, - "node_modules/@hey-api/shared/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/@hey-api/spec-types": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@hey-api/spec-types/-/spec-types-0.2.0.tgz", - "integrity": "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==", + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", "license": "MIT", - "dependencies": { - "@hey-api/types": "0.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" } }, - "node_modules/@hey-api/types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.4.tgz", - "integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==", - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -3532,6 +3463,93 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3673,21 +3691,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", - "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@mdx-js/mdx": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", @@ -4740,6 +4743,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -5048,102 +5076,383 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", - "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, "license": "MIT" }, - "node_modules/@oxc-project/types": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", - "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@orval/angular": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-8.33.0.tgz", + "integrity": "sha512-LwvVt4nTBD9gUvW+78Aby5bG77A4IYDcNjauLABYOzQqne2pXA/KToZtQUXxDWYHqWm4myFV/qFQHh0gxHTRvQ==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@orval/core": "8.33.0" } }, - "node_modules/@redocly/cli": { - "version": "2.51.2", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.51.2.tgz", - "integrity": "sha512-pviW1gfsjCAuIVutmQcihlhlgoivfNzisBIR61EwSSREHGZ08Sbtjp/h/HPDkVavWwdzR/gs2wgHrdXLd6qU1A==", + "node_modules/@orval/axios": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-8.33.0.tgz", + "integrity": "sha512-63TcFKE2x5S7Zz1aHowTfJ6UElkFJXpjQfReF4GSrwXOrSUvBooufuUzCultl54i864KxRvH4nXCi+yL3GS4+w==", "dev": true, "license": "MIT", - "bin": { - "openapi": "bin/cli.js", - "redocly": "bin/cli.js" - }, - "engines": { - "node": ">=22.12.0 || >=20.19.0 <21.0.0", - "npm": ">=10" + "dependencies": { + "@orval/core": "8.33.0" } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", - "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", - "cpu": [ - "arm" - ], + "node_modules/@orval/core": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/core/-/core-8.33.0.tgz", + "integrity": "sha512-sqfTwjQIc2ti/+Tl0WuUnt4ZAhLFTvC1L8vkLCzM1bzZdityD79JeIu2BjkhIm4/nG7BIVLbZkMDyC0zUvatlg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@scalar/openapi-types": "0.9.4", + "acorn": "^8.15.0", + "compare-versions": "^6.1.1", + "debug": "^4.4.3", + "esbuild": "^0.28.0", + "esutils": "2.0.3", + "fs-extra": "^11.3.2", + "jiti": "^2.6.1", + "jsesc": "^3.0.0", + "remeda": "^2.33.6", + "tinyglobby": "^0.2.16", + "typedoc": "^0.28.19" + }, + "peerDependencies": { + "@faker-js/faker": ">=8" + }, + "peerDependenciesMeta": { + "@faker-js/faker": { + "optional": true + } } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", - "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", - "cpu": [ - "arm64" - ], + "node_modules/@orval/core/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", - "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", - "cpu": [ - "arm64" - ], + "node_modules/@orval/effect": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/effect/-/effect-8.33.0.tgz", + "integrity": "sha512-L0uKhGXFvoNbBxs3ESz5LALDb7HvyBYttg90gDPBHUhr9BiDNa//7Um/81URI+mSkGum3otdH5hGVnC0gU0QOg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@orval/core": "8.33.0", + "remeda": "^2.33.6" + }, + "peerDependencies": { + "effect": ">=3" + }, + "peerDependenciesMeta": { + "effect": { + "optional": true + } } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", - "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", - "cpu": [ - "x64" - ], + "node_modules/@orval/fetch": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-8.33.0.tgz", + "integrity": "sha512-HtcqG/MuF6hwB6wGXIbBivXN9++hK3PYc11a6VTL4DT9tC9xqVD9QeWWpdhGuc1IM4xiivWGEFmjixGbiYrxDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0" + } + }, + "node_modules/@orval/hono": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-8.33.0.tgz", + "integrity": "sha512-H8qZsa7HGNljvs2FAqNfU+i+01H1SaSTHHMnoIHACYYDm5amxS2WIj4g4Jc76tRLZdGKMbovHn2j47/yxY+2gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "@orval/zod": "8.33.0", + "fs-extra": "^11.3.2" + }, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@orval/mcp": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/mcp/-/mcp-8.33.0.tgz", + "integrity": "sha512-HJSjaGet8CBpOsWzWC22GQnfwXC0X99gStdhOnkjHRqtinRHI21MdUc4WdEYQRM7Wf2WK5+1iJVr0KitGcbNAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "@orval/fetch": "8.33.0", + "@orval/zod": "8.33.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": ">=1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@orval/mock": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-8.33.0.tgz", + "integrity": "sha512-q2wLFwz0ps/PIDw42eNS00YXoSh1ZB9SrflAllVuLWLdAwsamj9J63t0vRMHSSxy9L6g8B1uhSFmJ/TB8lynmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "remeda": "^2.33.6" + }, + "peerDependencies": { + "@faker-js/faker": ">=8", + "msw": ">=2" + }, + "peerDependenciesMeta": { + "@faker-js/faker": { + "optional": true + }, + "msw": { + "optional": true + } + } + }, + "node_modules/@orval/pinia-colada": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/pinia-colada/-/pinia-colada-8.33.0.tgz", + "integrity": "sha512-A55+pkK7rWHlN4Vb39dEikhNAnhzTr2C7V5OEYaLHnoSnwFlN7U02cbCr7VUHuQxgsrmRL36wWSuy9z3jdttjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/axios": "8.33.0", + "@orval/core": "8.33.0", + "@orval/fetch": "8.33.0" + }, + "peerDependencies": { + "@pinia/colada": "^1.4.4" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + } + } + }, + "node_modules/@orval/query": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/query/-/query-8.33.0.tgz", + "integrity": "sha512-0tnJpS1A63KsvKG4pUkF1VXO7Jq0V6L4wYYwM+2kJmKL38pQkm93ue88wRKGsNhJT/omI8v3/D1wd16qBOQquw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "@orval/fetch": "8.33.0", + "remeda": "^2.33.6" + }, + "peerDependencies": { + "@tanstack/angular-query-experimental": ">=5", + "@tanstack/react-query": ">=4", + "@tanstack/solid-query": ">=4", + "@tanstack/svelte-query": ">=4", + "@tanstack/vue-query": ">=4" + }, + "peerDependenciesMeta": { + "@tanstack/angular-query-experimental": { + "optional": true + }, + "@tanstack/react-query": { + "optional": true + }, + "@tanstack/solid-query": { + "optional": true + }, + "@tanstack/svelte-query": { + "optional": true + }, + "@tanstack/vue-query": { + "optional": true + } + } + }, + "node_modules/@orval/solid-start": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/solid-start/-/solid-start-8.33.0.tgz", + "integrity": "sha512-fRjRRCsL8LdCKzJ15/vOzcRd6cocX00iH2oOPN428eHsVS7ZuvBGJ9sqQ4Fzgy0vMkxi5hNj59IIr+wRquDJag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0" + } + }, + "node_modules/@orval/swr": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-8.33.0.tgz", + "integrity": "sha512-jnr/HWGXRakhv92R4vL1b11Kz1hqrY81m/1fra2d80h1zwSJGnTPrszWSOrI9pBviAQYFktJy/yWGmBdYeD66Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "@orval/fetch": "8.33.0" + }, + "peerDependencies": { + "swr": ">=2" + }, + "peerDependenciesMeta": { + "swr": { + "optional": true + } + } + }, + "node_modules/@orval/zod": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-8.33.0.tgz", + "integrity": "sha512-m98RJzpNxrooSqeAilY+PlArZ49jyxBXQGvZGK7hX5tDuTAg6i4iXxvpEuPZ/uVrW04tMcRBoSiREbl5NjYzYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@orval/core": "8.33.0", + "jsesc": "^3.0.0", + "remeda": "^2.33.6" + }, + "peerDependencies": { + "zod": ">=3" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redocly/cli": { + "version": "2.51.2", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.51.2.tgz", + "integrity": "sha512-pviW1gfsjCAuIVutmQcihlhlgoivfNzisBIR61EwSSREHGZ08Sbtjp/h/HPDkVavWwdzR/gs2wgHrdXLd6qU1A==", + "dev": true, + "license": "MIT", + "bin": { + "openapi": "bin/cli.js", + "redocly": "bin/cli.js" + }, + "engines": { + "node": ">=22.12.0 || >=20.19.0 <21.0.0", + "npm": ">=10" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "cpu": [ + "x64" + ], "license": "MIT", "optional": true, "os": [ @@ -5763,6 +6072,167 @@ "win32" ] }, + "node_modules/@scalar/helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.10.0.tgz", + "integrity": "sha512-IAfnpIZnXY6ni+zyFMwWHBa5b/7yr4mCSvoTGR84oS9q4Quy2wjgafnr7CyfL65ney3iilBZHigZYLYqdqCu3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/json-magic": { + "version": "0.12.20", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.12.20.tgz", + "integrity": "sha512-5JawJ1L3+ddDoKefkXsklKs6CmmTIcPDmkJPpAkbRi9QQiU9MUONQ4VSi2z3ij+8Ka4neNhsXBNG4itPjurBLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.10.0", + "pathe": "^2.0.3", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser": { + "version": "0.28.16", + "resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.28.16.tgz", + "integrity": "sha512-zHEQExXo58Nk7/Tju4y4HLVJ+B5nnDmR7vPhhHLVinL0z99aGrd6S/8HREupVpfAsim326pfzIbSM+UOvEbnOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.11.1", + "@scalar/json-magic": "0.13.2", + "@scalar/openapi-types": "0.9.5", + "@scalar/openapi-upgrader": "0.2.15", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "jsonpointer": "^5.0.1", + "leven": "^4.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/@scalar/helpers": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.11.1.tgz", + "integrity": "sha512-Knwbe0IYqFk0PPDoOKLasqglBHfyf9/zwWWqFsSNi/AtdjM29wSZXN6p8DFid6iB5B9epYH9YiSgJ6tpD00TEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/@scalar/json-magic": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.13.2.tgz", + "integrity": "sha512-T8rQw5u7+MSTDpUcd5ShX1taOUxpZMv2b/P6xsahdlv/u68VX/Bq/+uzuAf2xW8IIOy7BEP4MBggle/vMDgAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.11.1", + "pathe": "^2.0.3", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/@scalar/openapi-types": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.5.tgz", + "integrity": "sha512-czrz/zkVm1oPzrpYo3hI/iymfiw1s4dgJiQtwNi2U77Sqf3EQOGKsif4VNRa5suWMYRuPaFx5EiFM1FNEV4Whg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@scalar/openapi-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@scalar/openapi-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scalar/openapi-types": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.4.tgz", + "integrity": "sha512-eUSIjZEBLEF2i2pvcNdzXhzlKx6qt+hZsNklLmCyzPBoXWxHcQaEClgMFavXDRRvJDdi6LkyjqGUqqf0XgRgFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.2.15.tgz", + "integrity": "sha512-yqROK9U96ElasEL4Wl/+PIjQZlqrQXVUUpXk9PA6xBnrp8KdEv7at8pR5gsZkvddJfYVwLILPlctyqUg4u4YZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.9.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader/node_modules/@scalar/openapi-types": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.5.tgz", + "integrity": "sha512-czrz/zkVm1oPzrpYo3hI/iymfiw1s4dgJiQtwNi2U77Sqf3EQOGKsif4VNRa5suWMYRuPaFx5EiFM1FNEV4Whg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@shellhub/console": { "resolved": "apps/console", "link": true @@ -5892,6 +6362,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@solid-primitives/refs": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.1.4.tgz", @@ -6667,6 +7150,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/katex": { @@ -6763,6 +7247,16 @@ "@types/node": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sshpk": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.17.5.tgz", @@ -6774,6 +7268,13 @@ "@types/node": "*" } }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", @@ -7330,6 +7831,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/am-i-vibing": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", @@ -7342,15 +7885,6 @@ "am-i-vibing": "dist/cli.mjs" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -8392,88 +8926,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c12": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", - "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "confbox": "^0.2.4", - "defu": "^6.1.6", - "dotenv": "^17.3.1", - "exsolve": "^1.0.8", - "giget": "^3.2.0", - "jiti": "^2.6.1", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^2.1.0", - "pkg-types": "^2.3.0", - "rc9": "^3.0.1" - }, - "peerDependencies": { - "magicast": "*" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/c12/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/c12/node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "dev": true, + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.9", @@ -8750,6 +9204,16 @@ "node": ">= 0.10" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -8799,15 +9263,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -8861,6 +9316,13 @@ "node": ">= 18" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8884,12 +9346,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "license": "MIT" - }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -8993,6 +9449,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9582,34 +10039,6 @@ "dev": true, "license": "MIT" }, - "node_modules/default-browser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", - "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -9628,18 +10057,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -9866,18 +10283,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -10864,6 +11269,33 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -10874,12 +11306,6 @@ "node": ">=12.0.0" } }, - "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -10961,6 +11387,23 @@ "fast-string-truncated-width": "^3.0.2" } }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-wrap-ansi": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", @@ -11011,6 +11454,22 @@ } } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -11176,6 +11635,21 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -11324,6 +11798,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -11346,6 +11837,7 @@ "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -11363,15 +11855,6 @@ "assert-plus": "^1.0.0" } }, - "node_modules/giget": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", - "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", - "license": "MIT", - "bin": { - "giget": "dist/cli.mjs" - } - }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -11515,6 +11998,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/h3": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", @@ -12454,6 +12954,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/headers-polyfill/node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -12590,6 +13108,16 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -13067,51 +13595,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -13155,6 +13638,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -13272,6 +13762,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -13323,6 +13826,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -13369,21 +13885,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -13395,6 +13896,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-timers-promises": { @@ -13618,6 +14120,29 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13701,6 +14226,19 @@ "node": ">=0.10" } }, + "node_modules/leven": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-4.1.0.tgz", + "integrity": "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -13989,13 +14527,33 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/linkifyjs": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", @@ -14072,6 +14630,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -14116,6 +14681,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -14649,6 +15255,13 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/mdx2vast": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/mdx2vast/-/mdx2vast-0.5.0.tgz", @@ -15615,6 +16228,149 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz", + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz", + "integrity": "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/msw/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/msw/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/msw/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/msw/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15916,6 +16672,36 @@ "node": ">=10" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -16111,54 +16897,226 @@ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.2", - "regex": "^6.1.0", - "regex-recursion": "^6.0.2" + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/orval": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/orval/-/orval-8.33.0.tgz", + "integrity": "sha512-O3ENCt9k0HTGBCPH05W5YmQqaco0kNJmDL7bb5LLT0DN/eRlKekNYtmdq2zYrZBn84+dRt+9Lv+eMuSUt3tY4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^15.0.0", + "@orval/angular": "8.33.0", + "@orval/axios": "8.33.0", + "@orval/core": "8.33.0", + "@orval/effect": "8.33.0", + "@orval/fetch": "8.33.0", + "@orval/hono": "8.33.0", + "@orval/mcp": "8.33.0", + "@orval/mock": "8.33.0", + "@orval/pinia-colada": "8.33.0", + "@orval/query": "8.33.0", + "@orval/solid-start": "8.33.0", + "@orval/swr": "8.33.0", + "@orval/zod": "8.33.0", + "@scalar/json-magic": "^0.12.19", + "@scalar/openapi-parser": "^0.28.11", + "@scalar/openapi-types": "0.9.4", + "chokidar": "^5.0.0", + "commander": "^15.0.0", + "execa": "^9.6.1", + "find-up": "8.0.0", + "fs-extra": "^11.3.2", + "get-tsconfig": "^4.14.0", + "jiti": "^2.6.1", + "js-yaml": "4.3.2", + "json5": "2.2.3", + "remeda": "^2.33.6", + "string-argv": "^0.3.2", + "typedoc": "^0.28.19", + "typedoc-plugin-coverage": "^4.0.2", + "typedoc-plugin-markdown": "^4.10.0" + }, + "bin": { + "orval": "dist/bin/orval.mjs" + }, + "engines": { + "node": ">=22.18.0" + }, + "peerDependencies": { + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/orval/node_modules/@commander-js/extra-typings": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-15.0.0.tgz", + "integrity": "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "commander": "~15.0.0" + } + }, + "node_modules/orval/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/orval/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/orval/node_modules/find-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz", + "integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^8.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/orval/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/orval/node_modules/locate-path": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz", + "integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "node_modules/orval/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.4.0", - "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", - "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=20" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/orval/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "p-limit": "^4.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/orderedmap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", - "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", - "license": "MIT" + "node_modules/orval/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/orval/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/os-browserify": { "version": "0.3.0", @@ -16167,6 +17125,13 @@ "dev": true, "license": "MIT" }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", @@ -16372,6 +17337,19 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-statements": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", @@ -16423,6 +17401,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16459,6 +17438,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/path/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -16480,6 +17466,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, "license": "MIT" }, "node_modules/pbkdf2": { @@ -16500,12 +17487,6 @@ "node": ">= 0.10" } }, - "node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "license": "MIT" - }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -16575,23 +17556,6 @@ "node": ">=10" } }, - "node_modules/pkg-types": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.2.tgz", - "integrity": "sha512-v0sVXzj7oPGysr543YYZLYbcJNJsKikSsp/fFzoxQ12ewY3ZZr7oCPC8y7OlmxfYB3QPvriXmuPD8KZggE1vqg==", - "license": "MIT", - "dependencies": { - "confbox": "^0.3.0", - "exsolve": "^1.1.1", - "pathe": "^2.0.3" - } - }, - "node_modules/pkg-types/node_modules/confbox": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", - "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", - "license": "MIT" - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -16805,18 +17769,6 @@ "dev": true, "license": "MIT" }, - "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16855,6 +17807,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -17119,6 +18087,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -17210,16 +18188,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/rc9": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", - "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", - "license": "MIT", - "dependencies": { - "defu": "^6.1.7", - "destr": "^2.0.5" - } - }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -18258,6 +19226,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remeda": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.50.0.tgz", + "integrity": "sha512-pzljnP7Gnl/lLz5WcOSkxEGCjcK4dteQihQjRrtKCGqn8J6jD0BHwfU6pRrXjvQV2EWe+HdgtfZW7rDdmKn8uQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -18364,6 +19345,13 @@ "node": ">= 4" } }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -18542,18 +19530,6 @@ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "license": "MIT" }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18910,6 +19886,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -18922,6 +19899,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19230,6 +20208,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -19275,6 +20263,13 @@ "xtend": "^4.0.2" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -19285,6 +20280,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -19482,6 +20487,19 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -19614,6 +20632,19 @@ "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", @@ -20046,10 +21077,61 @@ "dev": true, "license": "MIT" }, + "node_modules/typedoc": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, + "node_modules/typedoc-plugin-coverage": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-coverage/-/typedoc-plugin-coverage-4.0.3.tgz", + "integrity": "sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.13.0.tgz", + "integrity": "sha512-OHaOLoMTS0wL2ud73WXvxv486mrUEtgXxW4iKiTOGJVipT/VBWY76rlVRMQGes7aVonoIf5roWRe1UvHbufLlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -20083,6 +21165,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ufo": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", @@ -20136,6 +21225,19 @@ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -20460,6 +21562,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unstorage": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", @@ -20593,6 +21705,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", @@ -21192,6 +22314,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -21358,22 +22481,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wsl-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", - "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", @@ -21548,6 +22655,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", diff --git a/ui/scripts/generate-client.sh b/ui/scripts/generate-client.sh index a474e93f23c..ee19996cbe7 100755 --- a/ui/scripts/generate-client.sh +++ b/ui/scripts/generate-client.sh @@ -11,4 +11,4 @@ if [ -z "$OPENAPI_SPEC_PATH" ]; then fi cd "$(dirname "$0")/../apps/console" -exec npx openapi-ts +exec npx orval From 1d06576d6b764852ab3a7aca845f67450da3c16f Mon Sep 17 00:00:00 2001 From: luizhf42 Date: Thu, 3 Sep 2026 15:05:09 -0300 Subject: [PATCH 2/5] refactor(ui): migrate hooks, stores and utils to orval generated client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hey-api SDK barrel (`@/client`) is replaced by orval's generated `@/client/api` and `@/client/model` imports. Thin single-consumer wrappers are deleted and inlined into their call sites in the next commits. Paginated hooks now use orval-generated hooks directly — `customInstance` attaches `X-Total-Count` as a non-enumerable property on array responses, and a `totalCount()` accessor reads it. --- ui/apps/console/orval.config.ts | 169 ++++++++++++ .../__tests__/useAdminFirewallRules.test.ts | 239 ----------------- .../hooks/__tests__/useAdminSessions.test.ts | 97 ------- .../__tests__/useAdminUserMutations.test.ts | 253 ------------------ .../src/hooks/__tests__/useAdminUsers.test.ts | 223 --------------- .../src/hooks/__tests__/useBilling.test.ts | 190 ------------- .../hooks/__tests__/useDeviceChooser.test.ts | 154 ----------- .../__tests__/useInvitationMutations.test.ts | 241 ----------------- .../hooks/__tests__/useInvitations.test.ts | 201 -------------- .../__tests__/useLatestAnnouncement.test.ts | 141 ---------- .../src/hooks/__tests__/usePublicKeys.test.ts | 134 ---------- .../hooks/__tests__/useUploadLicense.test.ts | 71 ----- .../src/hooks/useAcceptDeviceByCode.ts | 39 ++- .../console/src/hooks/useAccessPolicies.ts | 22 -- .../src/hooks/useAccessPolicyMutations.ts | 40 --- .../hooks/useAdminAccountRequestMutations.ts | 16 -- .../src/hooks/useAdminAccountRequests.ts | 63 ----- .../hooks/useAdminAnnouncementMutations.ts | 46 ---- .../src/hooks/useAdminAnnouncements.ts | 68 ----- ui/apps/console/src/hooks/useAdminDevices.ts | 101 ------- .../src/hooks/useAdminFirewallRules.ts | 70 ----- ui/apps/console/src/hooks/useAdminLicense.ts | 26 +- .../src/hooks/useAdminNamespaceMutations.ts | 35 --- .../console/src/hooks/useAdminNamespaces.ts | 77 ------ .../src/hooks/useAdminSessionDetail.ts | 26 -- ui/apps/console/src/hooks/useAdminSessions.ts | 37 --- ui/apps/console/src/hooks/useAdminStats.ts | 28 -- .../src/hooks/useAdminUserMutations.ts | 53 ---- ui/apps/console/src/hooks/useAdminUsers.ts | 77 ------ .../console/src/hooks/useApiKeyMutations.ts | 42 --- ui/apps/console/src/hooks/useApiKeys.ts | 39 --- ui/apps/console/src/hooks/useBilling.ts | 119 -------- ui/apps/console/src/hooks/useChatwoot.ts | 15 +- ui/apps/console/src/hooks/useContainer.ts | 20 -- .../src/hooks/useContainerActionRunner.ts | 16 +- .../src/hooks/useContainerMutations.ts | 81 +----- ui/apps/console/src/hooks/useContainers.ts | 81 ------ ui/apps/console/src/hooks/useDevice.ts | 19 -- .../src/hooks/useDeviceActionRunner.ts | 19 +- ui/apps/console/src/hooks/useDeviceChooser.ts | 41 --- ui/apps/console/src/hooks/useDeviceCode.ts | 39 --- .../console/src/hooks/useDeviceMutations.ts | 136 +--------- ui/apps/console/src/hooks/useDevices.ts | 37 +-- .../src/hooks/useFirewallRuleMutations.ts | 40 --- ui/apps/console/src/hooks/useFirewallRules.ts | 40 --- .../console/src/hooks/useInstallKeyEvents.ts | 44 --- .../src/hooks/useInstallKeyMutations.ts | 29 -- ui/apps/console/src/hooks/useInstallKeys.ts | 41 --- .../src/hooks/useInstanceApiKeyMutations.ts | 32 --- .../console/src/hooks/useInstanceApiKeys.ts | 39 --- .../console/src/hooks/useInvalidateQueries.ts | 31 +-- .../src/hooks/useInvitationMutations.ts | 51 ---- ui/apps/console/src/hooks/useInvitations.ts | 82 ------ .../src/hooks/useLatestAnnouncement.ts | 39 --- ui/apps/console/src/hooks/useLoginAsUser.ts | 9 +- .../console/src/hooks/useMemberMutations.ts | 72 ----- .../src/hooks/useNamespaceMutations.ts | 97 +------ ui/apps/console/src/hooks/useNamespaces.ts | 100 ++----- .../src/hooks/usePublicKeyMutations.ts | 40 --- ui/apps/console/src/hooks/usePublicKeys.ts | 64 ----- .../console/src/hooks/useRevealInstallKey.ts | 23 -- ui/apps/console/src/hooks/useSSHApproval.ts | 72 +++-- ui/apps/console/src/hooks/useSSHIdentities.ts | 23 -- .../src/hooks/useSSHIdentityMutations.ts | 62 ----- .../src/hooks/useServiceAccountMutations.ts | 34 --- .../console/src/hooks/useServiceAccounts.ts | 22 -- ui/apps/console/src/hooks/useSession.ts | 19 -- .../console/src/hooks/useSessionMutations.ts | 29 -- .../console/src/hooks/useSessionRecording.ts | 11 +- ui/apps/console/src/hooks/useSessions.ts | 33 --- ui/apps/console/src/hooks/useStats.ts | 17 -- .../console/src/hooks/useSupportIdentifier.ts | 23 -- ui/apps/console/src/hooks/useTagMutations.ts | 41 --- ui/apps/console/src/hooks/useTags.ts | 36 +-- ui/apps/console/src/hooks/useUploadLicense.ts | 15 -- .../src/hooks/useWebEndpointMutations.ts | 28 -- ui/apps/console/src/hooks/useWebEndpoints.ts | 58 ---- .../src/stores/__tests__/authStore.test.ts | 2 +- ui/apps/console/src/stores/authStore.ts | 56 ++-- .../console/src/stores/connectivityStore.ts | 6 +- ui/apps/console/src/stores/mfaResetStore.ts | 17 +- ui/apps/console/src/stores/signUpStore.ts | 18 +- ui/apps/console/src/tests/factories.ts | 6 +- ui/apps/console/src/tests/mockNamespaces.ts | 2 +- .../src/utils/__tests__/deviceTags.test.ts | 2 +- .../console/src/utils/__tests__/stats.test.ts | 2 +- ui/apps/console/src/utils/billing.ts | 2 +- ui/apps/console/src/utils/deviceTags.ts | 2 +- ui/apps/console/src/utils/invitations.ts | 2 +- ui/apps/console/src/utils/license.ts | 2 +- ui/apps/console/src/utils/session.ts | 2 +- ui/apps/console/src/utils/sshIdentity.ts | 2 +- ui/apps/console/src/utils/stats.ts | 2 +- .../console/src/utils/vault-backend-server.ts | 73 ++--- 94 files changed, 436 insertions(+), 4799 deletions(-) delete mode 100644 ui/apps/console/src/hooks/__tests__/useAdminFirewallRules.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useAdminSessions.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useAdminUserMutations.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useAdminUsers.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useBilling.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useDeviceChooser.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useInvitationMutations.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useInvitations.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useLatestAnnouncement.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/usePublicKeys.test.ts delete mode 100644 ui/apps/console/src/hooks/__tests__/useUploadLicense.test.ts delete mode 100644 ui/apps/console/src/hooks/useAccessPolicies.ts delete mode 100644 ui/apps/console/src/hooks/useAccessPolicyMutations.ts delete mode 100644 ui/apps/console/src/hooks/useAdminAccountRequestMutations.ts delete mode 100644 ui/apps/console/src/hooks/useAdminAccountRequests.ts delete mode 100644 ui/apps/console/src/hooks/useAdminAnnouncementMutations.ts delete mode 100644 ui/apps/console/src/hooks/useAdminAnnouncements.ts delete mode 100644 ui/apps/console/src/hooks/useAdminDevices.ts delete mode 100644 ui/apps/console/src/hooks/useAdminFirewallRules.ts delete mode 100644 ui/apps/console/src/hooks/useAdminNamespaceMutations.ts delete mode 100644 ui/apps/console/src/hooks/useAdminNamespaces.ts delete mode 100644 ui/apps/console/src/hooks/useAdminSessionDetail.ts delete mode 100644 ui/apps/console/src/hooks/useAdminSessions.ts delete mode 100644 ui/apps/console/src/hooks/useAdminStats.ts delete mode 100644 ui/apps/console/src/hooks/useAdminUserMutations.ts delete mode 100644 ui/apps/console/src/hooks/useAdminUsers.ts delete mode 100644 ui/apps/console/src/hooks/useApiKeyMutations.ts delete mode 100644 ui/apps/console/src/hooks/useApiKeys.ts delete mode 100644 ui/apps/console/src/hooks/useBilling.ts delete mode 100644 ui/apps/console/src/hooks/useContainer.ts delete mode 100644 ui/apps/console/src/hooks/useContainers.ts delete mode 100644 ui/apps/console/src/hooks/useDevice.ts delete mode 100644 ui/apps/console/src/hooks/useDeviceChooser.ts delete mode 100644 ui/apps/console/src/hooks/useDeviceCode.ts delete mode 100644 ui/apps/console/src/hooks/useFirewallRuleMutations.ts delete mode 100644 ui/apps/console/src/hooks/useFirewallRules.ts delete mode 100644 ui/apps/console/src/hooks/useInstallKeyEvents.ts delete mode 100644 ui/apps/console/src/hooks/useInstallKeyMutations.ts delete mode 100644 ui/apps/console/src/hooks/useInstallKeys.ts delete mode 100644 ui/apps/console/src/hooks/useInstanceApiKeyMutations.ts delete mode 100644 ui/apps/console/src/hooks/useInstanceApiKeys.ts delete mode 100644 ui/apps/console/src/hooks/useInvitationMutations.ts delete mode 100644 ui/apps/console/src/hooks/useInvitations.ts delete mode 100644 ui/apps/console/src/hooks/useLatestAnnouncement.ts delete mode 100644 ui/apps/console/src/hooks/useMemberMutations.ts delete mode 100644 ui/apps/console/src/hooks/usePublicKeyMutations.ts delete mode 100644 ui/apps/console/src/hooks/usePublicKeys.ts delete mode 100644 ui/apps/console/src/hooks/useRevealInstallKey.ts delete mode 100644 ui/apps/console/src/hooks/useSSHIdentities.ts delete mode 100644 ui/apps/console/src/hooks/useSSHIdentityMutations.ts delete mode 100644 ui/apps/console/src/hooks/useServiceAccountMutations.ts delete mode 100644 ui/apps/console/src/hooks/useServiceAccounts.ts delete mode 100644 ui/apps/console/src/hooks/useSession.ts delete mode 100644 ui/apps/console/src/hooks/useSessionMutations.ts delete mode 100644 ui/apps/console/src/hooks/useSessions.ts delete mode 100644 ui/apps/console/src/hooks/useStats.ts delete mode 100644 ui/apps/console/src/hooks/useSupportIdentifier.ts delete mode 100644 ui/apps/console/src/hooks/useTagMutations.ts delete mode 100644 ui/apps/console/src/hooks/useUploadLicense.ts delete mode 100644 ui/apps/console/src/hooks/useWebEndpointMutations.ts delete mode 100644 ui/apps/console/src/hooks/useWebEndpoints.ts diff --git a/ui/apps/console/orval.config.ts b/ui/apps/console/orval.config.ts index 222a3725b3c..ee954d48c02 100644 --- a/ui/apps/console/orval.config.ts +++ b/ui/apps/console/orval.config.ts @@ -29,6 +29,175 @@ export default defineConfig({ }, query: { signal: true, + useInvalidate: true, + mutationInvalidates: [ + { + onMutations: ["apiKeyCreate", "apiKeyUpdate", "apiKeyDelete"], + invalidates: ["apiKeyList"], + }, + { + onMutations: [ + "createPublicKey", + "updatePublicKey", + "deletePublicKey", + ], + invalidates: ["getPublicKeys"], + }, + { + onMutations: [ + "createFirewallRule", + "updateFirewallRule", + "deleteFirewallRule", + ], + invalidates: ["getFirewallRules"], + }, + { + onMutations: [ + "createAccessPolicy", + "updateAccessPolicy", + "deleteAccessPolicy", + ], + invalidates: ["listAccessPolicies"], + }, + { + onMutations: ["createWebEndpoint", "deleteWebEndpoint"], + invalidates: ["listWebEndpoints"], + }, + { + onMutations: ["installKeyCreate", "installKeyUpdate"], + invalidates: ["installKeyList"], + }, + { + onMutations: ["createTag", "updateTag", "deleteTag"], + invalidates: ["getTags", "getDevices"], + }, + { + onMutations: [ + "addNamespaceMember", + "updateNamespaceMember", + "removeNamespaceMember", + ], + invalidates: ["getNamespaces"], + }, + { + onMutations: ["approveUser"], + invalidates: ["getNamespaces", "getUsers"], + }, + { + onMutations: ["createUserAdmin"], + invalidates: ["getUsers"], + }, + { + onMutations: [ + "adminUpdateUser", + "adminDeleteUser", + "adminResetUserPassword", + ], + invalidates: ["getUsers"], + }, + { + onMutations: ["editNamespaceAdmin", "deleteNamespaceAdmin"], + invalidates: ["getNamespacesAdmin"], + }, + { + onMutations: ["createAnnouncement"], + invalidates: ["listAnnouncementsAdmin"], + }, + { + onMutations: ["updateAnnouncement", "deleteAnnouncement"], + invalidates: ["listAnnouncementsAdmin"], + }, + { + onMutations: ["createServiceAccount", "deleteServiceAccount"], + invalidates: ["listServiceAccounts", "listSshIdentities"], + }, + { + onMutations: ["acceptInvite"], + invalidates: ["getMembershipInvitationList", "getNamespaces"], + }, + { + onMutations: ["generateInvitationLink"], + invalidates: ["getNamespaces"], + }, + { + onMutations: ["cancelMembershipInvitation"], + invalidates: ["getNamespaces"], + }, + { + onMutations: ["sendLicense"], + invalidates: ["getLicense"], + }, + { + onMutations: [ + "acceptDevice", + "deleteDevice", + "acceptDevicePairing", + ], + invalidates: [ + "getDevices", + "getStatusDevices", + "getStats", + "installKeyList", + ], + }, + { + onMutations: [ + "updateDeviceStatus", + "updateDevice", + "pullTagFromDevice", + "choiceDevices", + ], + invalidates: ["getDevices", "getStatusDevices", "installKeyList"], + }, + { + onMutations: ["setDeviceCustomField", "deleteDeviceCustomField"], + invalidates: ["getDevices"], + }, + { + onMutations: [ + "updateContainerStatus", + "deleteContainer", + "updateContainer", + "pullTagFromContainer", + ], + invalidates: ["getContainers"], + }, + { + onMutations: ["clsoeSession"], + invalidates: ["getSessions", "getStatusDevices"], + }, + { + onMutations: ["deleteSessionRecord"], + invalidates: ["getSessions"], + }, + { + onMutations: [ + "confirmSshApproval", + "createSshIdentity", + "renameSshIdentity", + "deleteSshIdentity", + ], + invalidates: ["listSshIdentities"], + }, + { + onMutations: ["editNamespace", "setSshAccessMode"], + invalidates: ["getNamespaces"], + }, + { + onMutations: ["createInstanceAPIKey", "deleteInstanceAPIKey"], + invalidates: ["listInstanceAPIKeys"], + }, + { + onMutations: [ + "createCustomer", + "createSubscription", + "attachPaymentMethod", + "detachPaymentMethod", + "setDefaultPaymentMethod", + ], + invalidates: ["getCustomer", "getSubscription"], + }, + ], }, }, }, diff --git a/ui/apps/console/src/hooks/__tests__/useAdminFirewallRules.test.ts b/ui/apps/console/src/hooks/__tests__/useAdminFirewallRules.test.ts deleted file mode 100644 index 51d5ef22855..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useAdminFirewallRules.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { - useAdminFirewallRules, - useAdminFirewallRule, -} from "../useAdminFirewallRules"; -import { useAuthStore } from "@/stores/authStore"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getFirewallRulesAdmin: vi.fn(), - getFirewallRuleAdmin: vi.fn(), - }), -); - -beforeEach(() => { - vi.clearAllMocks(); - useAuthStore.setState({ isAdmin: true }); -}); - -describe("useAdminFirewallRules", () => { - describe("when user is admin", () => { - it("returns rules from the paginated query result", async () => { - const rules = [ - { - id: "rule-1", - tenant_id: "tenant-abc", - priority: 1, - action: "allow", - active: true, - source_ip: ".*", - username: ".*", - filter: { hostname: ".*", tags: [] }, - }, - { - id: "rule-2", - tenant_id: "tenant-abc", - priority: 2, - action: "deny", - active: false, - source_ip: "192.168.1.0/24", - username: "admin", - filter: { hostname: "my-host", tags: [] }, - }, - ]; - sdk.getFirewallRulesAdmin.mockResolvedValue( - mockSdkResponse(rules, { "X-Total-Count": "2" }), - ); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.rules).toHaveLength(2); - expect(result.current.rules[0].id).toBe("rule-1"); - expect(result.current.rules[1].id).toBe("rule-2"); - }); - - it("returns totalCount from the X-Total-Count header", async () => { - sdk.getFirewallRulesAdmin.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "42" }), - ); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.totalCount).toBe(42); - }); - - it("defaults rules to empty array while loading", () => { - sdk.getFirewallRulesAdmin.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - expect(result.current.rules).toEqual([]); - }); - - it("defaults totalCount to 0 while loading", () => { - sdk.getFirewallRulesAdmin.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - expect(result.current.totalCount).toBe(0); - }); - - it("returns isLoading true initially", () => { - sdk.getFirewallRulesAdmin.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes error when query fails", async () => { - const networkError = new Error("network failure"); - sdk.getFirewallRulesAdmin.mockRejectedValue(networkError); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - await waitFor(() => expect(result.current.error).toBeTruthy()); - expect(result.current.error).toBe(networkError); - }); - }); - - describe("when user is not admin", () => { - it("does not execute the query", () => { - useAuthStore.setState({ isAdmin: false }); - - const { result } = renderHookWithClient(() => useAdminFirewallRules()); - - expect(result.current.isLoading).toBe(false); - expect(result.current.rules).toEqual([]); - expect(sdk.getFirewallRulesAdmin).not.toHaveBeenCalled(); - }); - }); - - describe("pagination defaults", () => { - it("uses page 1 and perPage 10 as defaults", async () => { - sdk.getFirewallRulesAdmin.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useAdminFirewallRules()); - - await waitFor(() => expect(sdk.getFirewallRulesAdmin).toHaveBeenCalled()); - const [opts] = sdk.getFirewallRulesAdmin.mock.calls[0]; - expect(opts.query.page).toBe(1); - expect(opts.query.per_page).toBe(10); - }); - - it("forwards custom page and perPage", async () => { - sdk.getFirewallRulesAdmin.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => - useAdminFirewallRules({ page: 3, perPage: 25 }), - ); - - await waitFor(() => expect(sdk.getFirewallRulesAdmin).toHaveBeenCalled()); - const [opts] = sdk.getFirewallRulesAdmin.mock.calls[0]; - expect(opts.query.page).toBe(3); - expect(opts.query.per_page).toBe(25); - }); - }); -}); - -describe("useAdminFirewallRule", () => { - describe("when user is admin", () => { - it("returns query data for the given rule id", async () => { - const rawRule = { - id: "rule-1", - tenant_id: "tenant-abc", - priority: 1, - action: "allow", - active: true, - source_ip: ".*", - username: ".*", - filter: { hostname: ".*", tags: [] }, - }; - sdk.getFirewallRuleAdmin.mockResolvedValue(mockSdkResponse(rawRule)); - - const { result } = renderHookWithClient(() => - useAdminFirewallRule("rule-1"), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.data?.id).toBe("rule-1"); - }); - - it("passes through the filter unchanged", async () => { - const rawRule = { - id: "rule-1", - tenant_id: "tenant-abc", - priority: 1, - action: "allow", - active: true, - source_ip: ".*", - username: ".*", - filter: { hostname: "my-host", tags: [] }, - }; - sdk.getFirewallRuleAdmin.mockResolvedValue(mockSdkResponse(rawRule)); - - const { result } = renderHookWithClient(() => - useAdminFirewallRule("rule-1"), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.data?.filter).toEqual({ - hostname: "my-host", - tags: [], - }); - }); - - it("is loading initially when id is provided", () => { - sdk.getFirewallRuleAdmin.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => - useAdminFirewallRule("rule-1"), - ); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes error when query fails", async () => { - const err = new Error("not found"); - sdk.getFirewallRuleAdmin.mockRejectedValue(err); - - const { result } = renderHookWithClient(() => - useAdminFirewallRule("rule-1"), - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - }); - }); - - describe("when id is empty", () => { - it("does not execute the query", () => { - const { result } = renderHookWithClient(() => useAdminFirewallRule("")); - - expect(result.current.isLoading).toBe(false); - expect(sdk.getFirewallRuleAdmin).not.toHaveBeenCalled(); - }); - }); - - describe("when user is not admin", () => { - it("does not execute the query even when id is provided", () => { - useAuthStore.setState({ isAdmin: false }); - - const { result } = renderHookWithClient(() => - useAdminFirewallRule("rule-1"), - ); - - expect(result.current.isLoading).toBe(false); - expect(sdk.getFirewallRuleAdmin).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useAdminSessions.test.ts b/ui/apps/console/src/hooks/__tests__/useAdminSessions.test.ts deleted file mode 100644 index 3b8f55a5e44..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useAdminSessions.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { useAuthStore } from "@/stores/authStore"; -import { useAdminSessions } from "../useAdminSessions"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getSessionsAdmin: vi.fn(), - }), -); - -const mockSession = { - uid: "session-1", - device_uid: "device-1", - username: "root", - ip_address: "192.168.0.1", - started_at: "2024-01-01T00:00:00Z", - last_seen: "2024-01-01T01:00:00Z", - active: true, - authenticated: true, -}; - -function renderAdminSessions() { - return renderHookWithClient(() => useAdminSessions({ page: 1, perPage: 10 })); -} - -describe("useAdminSessions", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("when user is not an admin", () => { - it("returns empty sessions and zero totalCount without fetching", () => { - useAuthStore.setState({ isAdmin: false }); - - const { result } = renderAdminSessions(); - - expect(result.current.sessions).toEqual([]); - expect(result.current.totalCount).toBe(0); - expect(result.current.isLoading).toBe(false); - expect(result.current.error).toBeNull(); - expect(sdk.getSessionsAdmin).not.toHaveBeenCalled(); - }); - }); - - describe("when user is an admin", () => { - beforeEach(() => { - useAuthStore.setState({ isAdmin: true }); - }); - - it("returns sessions and totalCount on success", async () => { - sdk.getSessionsAdmin.mockResolvedValue( - mockSdkResponse([mockSession], { "X-Total-Count": "1" }), - ); - - const { result } = renderAdminSessions(); - - await waitFor(() => expect(result.current.sessions).toHaveLength(1)); - - expect(result.current.sessions[0]).toMatchObject({ uid: "session-1" }); - expect(result.current.totalCount).toBe(1); - expect(result.current.error).toBeNull(); - }); - - it("returns empty arrays when the API returns no sessions", async () => { - sdk.getSessionsAdmin.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - const { result } = renderAdminSessions(); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.sessions).toEqual([]); - expect(result.current.totalCount).toBe(0); - }); - - it("is loading while the query is in-flight", () => { - sdk.getSessionsAdmin.mockReturnValue(new Promise(() => {})); - - const { result } = renderAdminSessions(); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes the raw error on fetch failure", async () => { - sdk.getSessionsAdmin.mockRejectedValue(new Error("Network timeout")); - - const { result } = renderAdminSessions(); - - await waitFor(() => expect(result.current.error).not.toBeNull()); - expect(result.current.error?.message).toBe("Network timeout"); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useAdminUserMutations.test.ts b/ui/apps/console/src/hooks/__tests__/useAdminUserMutations.test.ts deleted file mode 100644 index dc0939fea0e..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useAdminUserMutations.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor, act } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { - useCreateUser, - useUpdateUser, - useDeleteUser, - useResetUserPassword, -} from "../useAdminUserMutations"; - -const mockInvalidate = vi.fn(); - -const sdk = vi.hoisted(() => - mockSdkGen({ - createUserAdmin: vi.fn(), - adminUpdateUser: vi.fn(), - adminDeleteUser: vi.fn(), - adminResetUserPassword: vi.fn(), - }), -); - -vi.mock("../useInvalidateQueries", () => ({ - useInvalidateByIds: vi.fn(() => mockInvalidate), -})); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useCreateUser", () => { - describe("mutation call", () => { - it("calls createUserAdmin with the provided body", async () => { - sdk.createUserAdmin.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useCreateUser()); - - const body = { - body: { - name: "Alice", - username: "alice", - email: "alice@example.com", - password: "pass1", - }, - }; - await act(() => result.current.mutateAsync(body)); - - expect(sdk.createUserAdmin).toHaveBeenCalledWith( - expect.objectContaining({ ...body, throwOnError: true }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful mutation", async () => { - sdk.createUserAdmin.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useCreateUser()); - - await act(() => result.current.mutateAsync({})); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when mutation fails", async () => { - const error = new Error("create failed"); - sdk.createUserAdmin.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useCreateUser()); - - act(() => result.current.mutate({})); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when mutation fails", async () => { - sdk.createUserAdmin.mockRejectedValue(new Error("create failed")); - const { result } = renderHookWithClient(() => useCreateUser()); - - act(() => result.current.mutate({})); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); - -describe("useUpdateUser", () => { - describe("mutation call", () => { - it("calls adminUpdateUser with path and body", async () => { - sdk.adminUpdateUser.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useUpdateUser()); - - const vars = { path: { id: "u1" }, body: { name: "Bob" } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.adminUpdateUser).toHaveBeenCalledWith( - expect.objectContaining({ ...vars, throwOnError: true }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful update", async () => { - sdk.adminUpdateUser.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useUpdateUser()); - - await act(() => result.current.mutateAsync({ path: { id: "u1" } })); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when update fails", async () => { - const error = new Error("update failed"); - sdk.adminUpdateUser.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useUpdateUser()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when update fails", async () => { - sdk.adminUpdateUser.mockRejectedValue(new Error("update failed")); - const { result } = renderHookWithClient(() => useUpdateUser()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); - -describe("useDeleteUser", () => { - describe("mutation call", () => { - it("calls adminDeleteUser with the path", async () => { - sdk.adminDeleteUser.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useDeleteUser()); - - const vars = { path: { id: "u1" } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.adminDeleteUser).toHaveBeenCalledWith( - expect.objectContaining({ ...vars, throwOnError: true }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful delete", async () => { - sdk.adminDeleteUser.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useDeleteUser()); - - await act(() => result.current.mutateAsync({ path: { id: "u1" } })); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when delete fails", async () => { - const error = new Error("delete failed"); - sdk.adminDeleteUser.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useDeleteUser()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when delete fails", async () => { - sdk.adminDeleteUser.mockRejectedValue(new Error("delete failed")); - const { result } = renderHookWithClient(() => useDeleteUser()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); - -describe("useResetUserPassword", () => { - describe("mutation call", () => { - it("calls adminResetUserPassword with the path", async () => { - sdk.adminResetUserPassword.mockResolvedValue( - mockSdkResponse({ password: "generated-pw" }), - ); - const { result } = renderHookWithClient(() => useResetUserPassword()); - - const vars = { path: { id: "u1" } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.adminResetUserPassword).toHaveBeenCalledWith( - expect.objectContaining({ ...vars, throwOnError: true }), - ); - }); - - it("returns the generated password from the mutation", async () => { - sdk.adminResetUserPassword.mockResolvedValue( - mockSdkResponse({ password: "s3cr3t-pass" }), - ); - const { result } = renderHookWithClient(() => useResetUserPassword()); - - const data = await act(() => - result.current.mutateAsync({ path: { id: "u1" } }), - ); - - expect(data).toEqual({ password: "s3cr3t-pass" }); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful password reset", async () => { - sdk.adminResetUserPassword.mockResolvedValue( - mockSdkResponse({ password: "pw" }), - ); - const { result } = renderHookWithClient(() => useResetUserPassword()); - - await act(() => result.current.mutateAsync({ path: { id: "u1" } })); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when reset fails", async () => { - const error = new Error("reset failed"); - sdk.adminResetUserPassword.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useResetUserPassword()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when reset fails", async () => { - sdk.adminResetUserPassword.mockRejectedValue(new Error("reset failed")); - const { result } = renderHookWithClient(() => useResetUserPassword()); - - act(() => result.current.mutate({ path: { id: "u1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useAdminUsers.test.ts b/ui/apps/console/src/hooks/__tests__/useAdminUsers.test.ts deleted file mode 100644 index efb6d6a87ff..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useAdminUsers.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { decodeB64url } from "@/tests/decodeB64url"; -import { useAdminUsers, useAdminUser } from "../useAdminUsers"; -import { useAuthStore } from "@/stores/authStore"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getUsers: vi.fn(), - getUser: vi.fn(), - }), -); - -beforeEach(() => { - vi.clearAllMocks(); - useAuthStore.setState({ isAdmin: true }); -}); - -describe("useAdminUsers", () => { - describe("when user is admin", () => { - it("returns users from the paginated query result", async () => { - const users = [ - { id: "u1", username: "alice" }, - { id: "u2", username: "bob" }, - ]; - sdk.getUsers.mockResolvedValue( - mockSdkResponse(users, { "X-Total-Count": "2" }), - ); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.users).toEqual(users); - }); - - it("returns totalCount from the X-Total-Count header", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "99" }), - ); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.totalCount).toBe(99); - }); - - it("defaults users to empty array while loading", () => { - sdk.getUsers.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - expect(result.current.users).toEqual([]); - }); - - it("defaults totalCount to 0 while loading", () => { - sdk.getUsers.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - expect(result.current.totalCount).toBe(0); - }); - - it("returns isLoading true initially", () => { - sdk.getUsers.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes error when query fails", async () => { - const networkError = new Error("network failure"); - sdk.getUsers.mockRejectedValue(networkError); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - await waitFor(() => expect(result.current.error).toBeTruthy()); - expect(result.current.error).toBe(networkError); - }); - - it("exposes refetch function", () => { - sdk.getUsers.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - expect(typeof result.current.refetch).toBe("function"); - }); - }); - - describe("when user is not admin", () => { - it("does not execute the query", () => { - useAuthStore.setState({ isAdmin: false }); - - const { result } = renderHookWithClient(() => useAdminUsers()); - - expect(result.current.isLoading).toBe(false); - expect(result.current.users).toEqual([]); - expect(sdk.getUsers).not.toHaveBeenCalled(); - }); - }); - - describe("search filter", () => { - it("passes search parameter to the query options", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - const { result } = renderHookWithClient(() => - useAdminUsers({ search: "alice" }), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(sdk.getUsers).toHaveBeenCalled(); - }); - - it("does not pass filter when search is empty", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useAdminUsers({ search: "" })); - - await waitFor(() => expect(sdk.getUsers).toHaveBeenCalled()); - const [opts] = sdk.getUsers.mock.calls[0]; - expect(opts.query.filter).toBeUndefined(); - }); - - it("includes a base64-encoded filter when search is non-empty", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useAdminUsers({ search: " >" })); - - await waitFor(() => expect(sdk.getUsers).toHaveBeenCalled()); - const [opts] = sdk.getUsers.mock.calls[0]; - expect(typeof opts.query.filter).toBe("string"); - const decoded = decodeB64url(opts.query.filter as string) as unknown[]; - expect(JSON.stringify(decoded)).toContain(" >"); - }); - }); - - describe("pagination defaults", () => { - it("uses page 1 and perPage 10 as defaults", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useAdminUsers()); - - await waitFor(() => expect(sdk.getUsers).toHaveBeenCalled()); - const [opts] = sdk.getUsers.mock.calls[0]; - expect(opts.query.page).toBe(1); - expect(opts.query.per_page).toBe(10); - }); - - it("forwards custom page and perPage", async () => { - sdk.getUsers.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useAdminUsers({ page: 3, perPage: 25 })); - - await waitFor(() => expect(sdk.getUsers).toHaveBeenCalled()); - const [opts] = sdk.getUsers.mock.calls[0]; - expect(opts.query.page).toBe(3); - expect(opts.query.per_page).toBe(25); - }); - }); -}); - -describe("useAdminUser", () => { - describe("when user is admin", () => { - it("returns query data for the given user id", async () => { - const user = { id: "u1", username: "alice" }; - sdk.getUser.mockResolvedValue(mockSdkResponse(user)); - - const { result } = renderHookWithClient(() => useAdminUser("u1")); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.data).toEqual(user); - }); - - it("is loading initially when id is provided", () => { - sdk.getUser.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useAdminUser("u1")); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes error when query fails", async () => { - const err = new Error("not found"); - sdk.getUser.mockRejectedValue(err); - - const { result } = renderHookWithClient(() => useAdminUser("u1")); - - await waitFor(() => expect(result.current.isError).toBe(true)); - }); - }); - - describe("when id is empty", () => { - it("does not execute the query", () => { - const { result } = renderHookWithClient(() => useAdminUser("")); - - expect(result.current.isLoading).toBe(false); - expect(sdk.getUser).not.toHaveBeenCalled(); - }); - }); - - describe("when user is not admin", () => { - it("does not execute the query even when id is provided", () => { - useAuthStore.setState({ isAdmin: false }); - - const { result } = renderHookWithClient(() => useAdminUser("u1")); - - expect(result.current.isLoading).toBe(false); - expect(sdk.getUser).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useBilling.test.ts b/ui/apps/console/src/hooks/__tests__/useBilling.test.ts deleted file mode 100644 index 748dc89a131..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useBilling.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { renderHook, waitFor, act } from "@testing-library/react"; -import { createTestWrapper, renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; - -const mockInvalidate = vi.fn(); - -const sdk = vi.hoisted(() => - mockSdkGen({ - getCustomer: vi.fn(), - getSubscription: vi.fn(), - createCustomer: vi.fn(), - createSubscription: vi.fn(), - attachPaymentMethod: vi.fn(), - detachPaymentMethod: vi.fn(), - setDefaultPaymentMethod: vi.fn(), - createBillingPortalSession: vi.fn(), - }), -); - -vi.mock("../useInvalidateQueries", () => ({ - useInvalidateByIds: vi.fn(() => mockInvalidate), -})); - -async function importHooks() { - return await import("../useBilling"); -} - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useBilling mutations", () => { - it("invalidates billing queries on customer creation", async () => { - sdk.createCustomer.mockResolvedValue(mockSdkResponse(undefined)); - const { useCreateCustomer } = await importHooks(); - - const { result } = renderHookWithClient(() => useCreateCustomer()); - - await act(() => result.current.mutateAsync({})); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - expect(sdk.createCustomer).toHaveBeenCalled(); - }); - - it("invalidates billing queries on subscription creation", async () => { - sdk.createSubscription.mockResolvedValue(mockSdkResponse(undefined)); - const { useCreateSubscription } = await importHooks(); - - const { result } = renderHookWithClient(() => useCreateSubscription()); - - await act(() => result.current.mutateAsync({})); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - - it("propagates 402 errors from subscription creation", async () => { - const err = Object.assign(new Error("payment required"), { - isAxiosError: true, - response: { status: 402 }, - }); - sdk.createSubscription.mockRejectedValue(err); - const { useCreateSubscription } = await importHooks(); - - const { result } = renderHookWithClient(() => useCreateSubscription()); - - await expect(result.current.mutateAsync({})).rejects.toBe(err); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - - it("attach/detach/default run through the SDK mutations", async () => { - sdk.attachPaymentMethod.mockResolvedValue(mockSdkResponse(undefined)); - sdk.detachPaymentMethod.mockResolvedValue(mockSdkResponse(undefined)); - sdk.setDefaultPaymentMethod.mockResolvedValue(mockSdkResponse(undefined)); - const { - useAttachPaymentMethod, - useDetachPaymentMethod, - useSetDefaultPaymentMethod, - } = await importHooks(); - - const wrapper = createTestWrapper(); - const attachHook = renderHook(() => useAttachPaymentMethod(), { wrapper }); - await act(() => - attachHook.result.current.mutateAsync({ body: { id: "pm_1" } }), - ); - - const detachHook = renderHook(() => useDetachPaymentMethod(), { wrapper }); - await act(() => - detachHook.result.current.mutateAsync({ body: { id: "pm_1" } }), - ); - - const defHook = renderHook(() => useSetDefaultPaymentMethod(), { wrapper }); - await act(() => - defHook.result.current.mutateAsync({ body: { id: "pm_1" } }), - ); - - expect(sdk.attachPaymentMethod).toHaveBeenCalled(); - expect(sdk.detachPaymentMethod).toHaveBeenCalled(); - expect(sdk.setDefaultPaymentMethod).toHaveBeenCalled(); - }); -}); - -describe("useCreateSubscription (query key coverage)", () => { - it("calls the mutation fn and then invalidates on success", async () => { - sdk.createSubscription.mockResolvedValue(mockSdkResponse(undefined)); - const { useCreateSubscription } = await importHooks(); - - const { result } = renderHookWithClient(() => useCreateSubscription()); - - await act(() => result.current.mutateAsync({})); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - expect(sdk.createSubscription).toHaveBeenCalled(); - }); -}); - -describe("useCustomer", () => { - it("does not call the SDK when enabled=false", async () => { - const { useCustomer } = await importHooks(); - - renderHookWithClient(() => useCustomer(false)); - - expect(sdk.getCustomer).not.toHaveBeenCalled(); - }); - - it("returns undefined customer when the query has no data", async () => { - const { useCustomer } = await importHooks(); - - const { result } = renderHookWithClient(() => useCustomer(false)); - - expect(result.current.customer).toBeUndefined(); - }); -}); - -describe("useSubscription", () => { - it("does not call the SDK when enabled=false", async () => { - const { useSubscription } = await importHooks(); - - renderHookWithClient(() => useSubscription(false)); - - expect(sdk.getSubscription).not.toHaveBeenCalled(); - }); - - it("exposes a refetch function even when disabled", async () => { - const { useSubscription } = await importHooks(); - - const { result } = renderHookWithClient(() => useSubscription(false)); - - expect(typeof result.current.refetch).toBe("function"); - }); - - it("returns undefined subscription when query has no data", async () => { - const { useSubscription } = await importHooks(); - - const { result } = renderHookWithClient(() => useSubscription(false)); - - expect(result.current.subscription).toBeUndefined(); - }); -}); - -describe("useOpenBillingPortal", () => { - it("opens the URL the billing portal route returns", async () => { - const openSpy = vi.spyOn(window, "open").mockReturnValue(null); - sdk.createBillingPortalSession.mockResolvedValue( - mockSdkResponse({ url: "https://billing.stripe.com/session/abc" }), - ); - const { useOpenBillingPortal } = await importHooks(); - - const { result } = renderHookWithClient(() => useOpenBillingPortal()); - - await act(() => result.current.mutateAsync()); - - expect(sdk.createBillingPortalSession).toHaveBeenCalled(); - expect(openSpy).toHaveBeenCalledWith( - "https://billing.stripe.com/session/abc", - "_blank", - "noopener,noreferrer", - ); - openSpy.mockRestore(); - }); - - it("throws when the response is missing a URL", async () => { - sdk.createBillingPortalSession.mockResolvedValue(mockSdkResponse({})); - const { useOpenBillingPortal } = await importHooks(); - - const { result } = renderHookWithClient(() => useOpenBillingPortal()); - - await expect(result.current.mutateAsync()).rejects.toThrow(/portal URL/i); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useDeviceChooser.test.ts b/ui/apps/console/src/hooks/__tests__/useDeviceChooser.test.ts deleted file mode 100644 index 0bb20617667..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useDeviceChooser.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor, act } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { useSuggestedDevices, useChoiceDevices } from "../useDeviceChooser"; -import { useInvalidateByIds } from "../useInvalidateQueries"; - -const mockInvalidate = vi.fn(); - -const sdk = vi.hoisted(() => - mockSdkGen({ - getDevicesMostUsed: vi.fn(), - choiceDevices: vi.fn(), - }), -); - -vi.mock("../useInvalidateQueries", () => ({ - useInvalidateByIds: vi.fn(() => mockInvalidate), -})); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useSuggestedDevices", () => { - describe("on success", () => { - it("returns the device list from the query response", async () => { - const devices = [ - { uid: "d1", name: "host-1" }, - { uid: "d2", name: "host-2" }, - ]; - sdk.getDevicesMostUsed.mockResolvedValue(mockSdkResponse(devices)); - - const { result } = renderHookWithClient(() => useSuggestedDevices()); - - await waitFor(() => - expect(result.current.devices).toEqual([ - { uid: "d1", name: "host-1", tags: [] }, - { uid: "d2", name: "host-2", tags: [] }, - ]), - ); - }); - - it("returns an empty array when data is undefined before load completes", () => { - sdk.getDevicesMostUsed.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useSuggestedDevices()); - - expect(result.current.devices).toEqual([]); - }); - - it("exposes isLoading=true while the query is in flight", () => { - sdk.getDevicesMostUsed.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useSuggestedDevices()); - - expect(result.current.isLoading).toBe(true); - }); - }); - - describe("when enabled=false", () => { - it("does not call the SDK function", () => { - renderHookWithClient(() => useSuggestedDevices(false)); - - expect(sdk.getDevicesMostUsed).not.toHaveBeenCalled(); - }); - - it("returns an empty devices array", () => { - const { result } = renderHookWithClient(() => useSuggestedDevices(false)); - - expect(result.current.devices).toEqual([]); - }); - }); - - describe("on error", () => { - it("exposes the error on failure", async () => { - const err = new Error("network failure"); - sdk.getDevicesMostUsed.mockRejectedValue(err); - - const { result } = renderHookWithClient(() => useSuggestedDevices()); - - await waitFor(() => expect(result.current.error).toBe(err)); - }); - }); -}); - -describe("useChoiceDevices", () => { - describe("mutation call", () => { - it("calls the SDK function with the choices body", async () => { - sdk.choiceDevices.mockResolvedValue(mockSdkResponse(undefined)); - - const { result } = renderHookWithClient(() => useChoiceDevices()); - - const vars = { body: { choices: ["uid1", "uid2"] } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.choiceDevices).toHaveBeenCalledWith( - expect.objectContaining({ - body: { choices: ["uid1", "uid2"] }, - throwOnError: true, - }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate once after the mutation succeeds", async () => { - sdk.choiceDevices.mockResolvedValue(mockSdkResponse(undefined)); - - const { result } = renderHookWithClient(() => useChoiceDevices()); - - await act(() => - result.current.mutateAsync({ body: { choices: ["uid1"] } }), - ); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - - it("registers invalidate using useInvalidateByIds with the correct query ids", () => { - renderHookWithClient(() => useChoiceDevices()); - - expect(useInvalidateByIds).toHaveBeenCalledWith( - "getDevices", - "getDevice", - "getStatusDevices", - ); - }); - }); - - describe("on failure", () => { - it("exposes error when the mutation fails", async () => { - const err = new Error("server error"); - sdk.choiceDevices.mockRejectedValue(err); - - const { result } = renderHookWithClient(() => useChoiceDevices()); - - act(() => result.current.mutate({ body: { choices: ["uid1"] } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(err); - }); - - it("does not call invalidate when the mutation fails", async () => { - sdk.choiceDevices.mockRejectedValue(new Error("server error")); - - const { result } = renderHookWithClient(() => useChoiceDevices()); - - act(() => result.current.mutate({ body: { choices: ["uid1"] } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useInvitationMutations.test.ts b/ui/apps/console/src/hooks/__tests__/useInvitationMutations.test.ts deleted file mode 100644 index ecf6bbfd16f..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useInvitationMutations.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor, act } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { - useAcceptInvite, - useGenerateInvitationLink, - useCancelMembershipInvitation, -} from "../useInvitationMutations"; - -const mockInvalidate = vi.fn(); - -const sdk = vi.hoisted(() => - mockSdkGen({ - acceptInvite: vi.fn(), - generateInvitationLink: vi.fn(), - cancelMembershipInvitation: vi.fn(), - }), -); - -vi.mock("../useInvalidateQueries", () => ({ - useInvalidateByIds: vi.fn(() => mockInvalidate), -})); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useAcceptInvite", () => { - describe("mutation call", () => { - it("calls acceptInvite with the provided path", async () => { - sdk.acceptInvite.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useAcceptInvite()); - - const vars = { path: { tenant: "t1" } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.acceptInvite).toHaveBeenCalledWith( - expect.objectContaining({ path: { tenant: "t1" }, throwOnError: true }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful mutation", async () => { - sdk.acceptInvite.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useAcceptInvite()); - - await act(() => result.current.mutateAsync({ path: { tenant: "t1" } })); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when mutation fails", async () => { - const error = new Error("accept failed"); - sdk.acceptInvite.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useAcceptInvite()); - - act(() => result.current.mutate({ path: { tenant: "t1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when mutation fails", async () => { - sdk.acceptInvite.mockRejectedValue(new Error("accept failed")); - const { result } = renderHookWithClient(() => useAcceptInvite()); - - act(() => result.current.mutate({ path: { tenant: "t1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); - -describe("useGenerateInvitationLink", () => { - describe("mutation call", () => { - it("calls generateInvitationLink with path and body", async () => { - sdk.generateInvitationLink.mockResolvedValue( - mockSdkResponse({ link: "https://example.com/invite/abc" }), - ); - const { result } = renderHookWithClient(() => - useGenerateInvitationLink(), - ); - - const vars = { - path: { tenant: "t1" }, - body: { email: "bob@example.com", role: "operator" as const }, - }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.generateInvitationLink).toHaveBeenCalledWith( - expect.objectContaining({ - path: { tenant: "t1" }, - body: { email: "bob@example.com", role: "operator" }, - throwOnError: true, - }), - ); - }); - - it("returns the generated link from the mutation", async () => { - const link = "https://example.com/invite/xyz"; - sdk.generateInvitationLink.mockResolvedValue(mockSdkResponse({ link })); - const { result } = renderHookWithClient(() => - useGenerateInvitationLink(), - ); - - const data = await act(() => - result.current.mutateAsync({ path: { tenant: "t1" } }), - ); - - expect(data).toEqual({ link }); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful mutation", async () => { - sdk.generateInvitationLink.mockResolvedValue( - mockSdkResponse({ link: "https://example.com/invite/abc" }), - ); - const { result } = renderHookWithClient(() => - useGenerateInvitationLink(), - ); - - await act(() => result.current.mutateAsync({ path: { tenant: "t1" } })); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when mutation fails", async () => { - const error = new Error("generate link failed"); - sdk.generateInvitationLink.mockRejectedValue(error); - const { result } = renderHookWithClient(() => - useGenerateInvitationLink(), - ); - - act(() => result.current.mutate({ path: { tenant: "t1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when mutation fails", async () => { - sdk.generateInvitationLink.mockRejectedValue( - new Error("generate link failed"), - ); - const { result } = renderHookWithClient(() => - useGenerateInvitationLink(), - ); - - act(() => result.current.mutate({ path: { tenant: "t1" } })); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); - -describe("useCancelMembershipInvitation", () => { - describe("mutation call", () => { - it("calls cancelMembershipInvitation with path", async () => { - sdk.cancelMembershipInvitation.mockResolvedValue( - mockSdkResponse(undefined), - ); - const { result } = renderHookWithClient(() => - useCancelMembershipInvitation(), - ); - - const vars = { path: { tenant: "t1", "user-id": "u1" } }; - await act(() => result.current.mutateAsync(vars)); - - expect(sdk.cancelMembershipInvitation).toHaveBeenCalledWith( - expect.objectContaining({ - path: { tenant: "t1", "user-id": "u1" }, - throwOnError: true, - }), - ); - }); - }); - - describe("on success", () => { - it("calls invalidate after successful mutation", async () => { - sdk.cancelMembershipInvitation.mockResolvedValue( - mockSdkResponse(undefined), - ); - const { result } = renderHookWithClient(() => - useCancelMembershipInvitation(), - ); - - await act(() => - result.current.mutateAsync({ - path: { tenant: "t1", "user-id": "u1" }, - }), - ); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("exposes error when mutation fails", async () => { - const error = new Error("cancel failed"); - sdk.cancelMembershipInvitation.mockRejectedValue(error); - const { result } = renderHookWithClient(() => - useCancelMembershipInvitation(), - ); - - act(() => - result.current.mutate({ - path: { tenant: "t1", "user-id": "u1" }, - }), - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when mutation fails", async () => { - sdk.cancelMembershipInvitation.mockRejectedValue( - new Error("cancel failed"), - ); - const { result } = renderHookWithClient(() => - useCancelMembershipInvitation(), - ); - - act(() => - result.current.mutate({ - path: { tenant: "t1", "user-id": "u1" }, - }), - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useInvitations.test.ts b/ui/apps/console/src/hooks/__tests__/useInvitations.test.ts deleted file mode 100644 index 038e5abd780..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useInvitations.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { - useNamespaceInvitations, - useResolveInvitation, -} from "../useInvitations"; -import type { MembershipInvitation } from "@/client"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getNamespaceMembershipInvitationList: vi.fn(), - resolveInvitation: vi.fn(), - }), -); - -function makeInvitation( - overrides: Partial = {}, -): MembershipInvitation { - return { - namespace: { tenant_id: "t1", name: "my-ns" }, - user: { id: "u1", email: "alice@example.com" }, - invited_by: "owner@example.com", - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - expires_at: "2024-01-08T00:00:00Z", - status: "pending", - status_updated_at: "2024-01-01T00:00:00Z", - role: "operator", - ...overrides, - }; -} - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useNamespaceInvitations", () => { - describe("returns", () => { - it("returns invitations for the given tenant", async () => { - const inv = makeInvitation({ status: "pending" }); - sdk.getNamespaceMembershipInvitationList.mockResolvedValue( - mockSdkResponse([inv], { "X-Total-Count": "1" }), - ); - - const { result } = renderHookWithClient(() => - useNamespaceInvitations({ tenantId: "t1" }), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.invitations).toHaveLength(1); - }); - - it("returns totalCount from the X-Total-Count header", async () => { - sdk.getNamespaceMembershipInvitationList.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "7" }), - ); - - const { result } = renderHookWithClient(() => - useNamespaceInvitations({ tenantId: "t1" }), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.totalCount).toBe(7); - }); - - it("defaults invitations to empty array while loading", () => { - sdk.getNamespaceMembershipInvitationList.mockReturnValue( - new Promise(() => {}), - ); - - const { result } = renderHookWithClient(() => - useNamespaceInvitations({ tenantId: "t1" }), - ); - - expect(result.current.invitations).toEqual([]); - }); - - it("exposes error when query fails", async () => { - const err = new Error("fetch failed"); - sdk.getNamespaceMembershipInvitationList.mockRejectedValue(err); - - const { result } = renderHookWithClient(() => - useNamespaceInvitations({ tenantId: "t1" }), - ); - - await waitFor(() => expect(result.current.error).toBeTruthy()); - expect(result.current.error).toBe(err); - }); - }); - - describe("enabled flag", () => { - it("does not fetch when enabled is false", () => { - sdk.getNamespaceMembershipInvitationList.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => - useNamespaceInvitations({ tenantId: "t1", enabled: false }), - ); - - expect(sdk.getNamespaceMembershipInvitationList).not.toHaveBeenCalled(); - }); - - it("does not fetch when tenantId is empty", () => { - sdk.getNamespaceMembershipInvitationList.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useNamespaceInvitations({ tenantId: "" })); - - expect(sdk.getNamespaceMembershipInvitationList).not.toHaveBeenCalled(); - }); - }); - - describe("pagination", () => { - it("uses page 1 and perPage 10 as defaults", async () => { - sdk.getNamespaceMembershipInvitationList.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => useNamespaceInvitations({ tenantId: "t1" })); - - await waitFor(() => - expect(sdk.getNamespaceMembershipInvitationList).toHaveBeenCalled(), - ); - const [opts] = sdk.getNamespaceMembershipInvitationList.mock.calls[0]; - expect(opts.query.page).toBe(1); - expect(opts.query.per_page).toBe(10); - }); - }); -}); - -describe("useResolveInvitation", () => { - it("normalizes wire fields to camelCase", async () => { - sdk.resolveInvitation.mockResolvedValue( - mockSdkResponse({ - tenant_id: "t1", - user_id: "u1", - email: "alice@example.com", - status: "confirmed", - }), - ); - - const { result } = renderHookWithClient(() => useResolveInvitation("CODE")); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.resolved).toEqual({ - tenantId: "t1", - userId: "u1", - email: "alice@example.com", - status: "confirmed", - }); - }); - - it.each(["tenant_id", "user_id", "status"])( - "returns null when %s is missing", - async (field) => { - const data = { - tenant_id: "t1", - user_id: "u1", - email: "a@b.com", - status: "confirmed", - [field]: null, - }; - sdk.resolveInvitation.mockResolvedValue(mockSdkResponse(data)); - - const { result } = renderHookWithClient(() => - useResolveInvitation("CODE"), - ); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.resolved).toBeNull(); - }, - ); - - it("falls back to empty string when email is null", async () => { - sdk.resolveInvitation.mockResolvedValue( - mockSdkResponse({ - tenant_id: "t1", - user_id: "u1", - email: null, - status: "invited", - }), - ); - - const { result } = renderHookWithClient(() => useResolveInvitation("CODE")); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.resolved?.email).toBe(""); - }); - - it("does not fetch when invite is empty", () => { - const { result } = renderHookWithClient(() => useResolveInvitation("")); - - expect(result.current.resolved).toBeNull(); - expect(result.current.isLoading).toBe(false); - expect(sdk.resolveInvitation).not.toHaveBeenCalled(); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useLatestAnnouncement.test.ts b/ui/apps/console/src/hooks/__tests__/useLatestAnnouncement.test.ts deleted file mode 100644 index b1ba8935f5f..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useLatestAnnouncement.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { getConfig, defaultConfig } from "@/env"; -import { mockSdkResponse } from "@/tests/sdk"; -import { useLatestAnnouncement } from "../useLatestAnnouncement"; -import type { Announcement } from "@/client"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - listAnnouncements: vi.fn(), - getAnnouncement: vi.fn(), - }), -); - -const mockGetConfig = vi.mocked(getConfig); - -function makeAnnouncement(overrides: Partial = {}): Announcement { - return { - uuid: "ann-uuid-1", - title: "Test Announcement", - content: "## Hello\nSome content", - date: "2024-06-01T00:00:00Z", - ...overrides, - }; -} - -beforeEach(() => { - vi.clearAllMocks(); - mockGetConfig.mockReturnValue({ ...defaultConfig, announcements: true }); - sdk.listAnnouncements.mockReturnValue(new Promise(() => {})); - sdk.getAnnouncement.mockReturnValue(new Promise(() => {})); -}); - -describe("useLatestAnnouncement", () => { - describe("when announcements feature flag is disabled", () => { - beforeEach(() => { - mockGetConfig.mockReturnValue({ ...defaultConfig, announcements: false }); - }); - - it("returns null announcement immediately", () => { - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - expect(result.current.announcement).toBeNull(); - }); - - it("returns isLoading false", () => { - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - expect(result.current.isLoading).toBe(false); - }); - - it("does not call the SDK", () => { - renderHookWithClient(() => useLatestAnnouncement()); - - expect(sdk.listAnnouncements).not.toHaveBeenCalled(); - }); - }); - - describe("loading state", () => { - it("returns isLoading true while list query is pending", () => { - sdk.listAnnouncements.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - expect(result.current.isLoading).toBe(true); - }); - - it("returns null announcement while queries are loading", () => { - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - expect(result.current.announcement).toBeNull(); - }); - }); - - describe("when list resolves but is empty", () => { - it("returns null announcement", async () => { - sdk.listAnnouncements.mockResolvedValue(mockSdkResponse([])); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.announcement).toBeNull(); - }); - - it("does not call getAnnouncement", async () => { - sdk.listAnnouncements.mockResolvedValue(mockSdkResponse([])); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(sdk.getAnnouncement).not.toHaveBeenCalled(); - }); - }); - - describe("when both queries resolve successfully", () => { - it("returns the full announcement object", async () => { - const ann = makeAnnouncement({ uuid: "ann-abc", title: "Big Update" }); - - sdk.listAnnouncements.mockResolvedValue( - mockSdkResponse([{ uuid: "ann-abc" }]), - ); - sdk.getAnnouncement.mockResolvedValue(mockSdkResponse(ann)); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - await waitFor(() => expect(result.current.announcement).not.toBeNull()); - expect(result.current.announcement).toEqual(ann); - }); - - it("returns isLoading false after both queries settle", async () => { - const ann = makeAnnouncement(); - - sdk.listAnnouncements.mockResolvedValue( - mockSdkResponse([{ uuid: "ann-uuid-1" }]), - ); - sdk.getAnnouncement.mockResolvedValue(mockSdkResponse(ann)); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.announcement).toEqual(ann); - }); - }); - - describe("when the list query resolves but the detail query is still loading", () => { - it("returns isLoading true", async () => { - sdk.listAnnouncements.mockResolvedValue( - mockSdkResponse([{ uuid: "ann-uuid-1" }]), - ); - sdk.getAnnouncement.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => useLatestAnnouncement()); - - await waitFor(() => expect(sdk.getAnnouncement).toHaveBeenCalled()); - - expect(result.current.isLoading).toBe(true); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/usePublicKeys.test.ts b/ui/apps/console/src/hooks/__tests__/usePublicKeys.test.ts deleted file mode 100644 index 2812531bd59..00000000000 --- a/ui/apps/console/src/hooks/__tests__/usePublicKeys.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { usePublicKeys } from "../usePublicKeys"; -import type { PublicKeyResponse } from "@/client"; - -const sdk = vi.hoisted(() => - mockSdkGen({ - getPublicKeys: vi.fn(), - }), -); - -function makeKey( - overrides: Partial = {}, -): PublicKeyResponse { - return { - name: "test-key", - fingerprint: "aa:bb:cc", - created_at: "2024-01-01T00:00:00Z", - tenant_id: "tenant-1", - data: "c3NoLXJzYQ==", - filter: { hostname: ".*", tags: [] }, - username: ".*", - ...overrides, - }; -} - -function makeTag(name: string) { - return { - name, - tenant_id: "tenant-1", - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - }; -} - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("usePublicKeys", () => { - describe("returns", () => { - it("returns publicKeys from the paginated result", async () => { - const keys = [ - makeKey({ name: "key-1", filter: { hostname: ".*", tags: [] } }), - makeKey({ name: "key-2", filter: { tags: [makeTag("prod")] } }), - ]; - sdk.getPublicKeys.mockResolvedValue( - mockSdkResponse(keys, { "X-Total-Count": "2" }), - ); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.publicKeys).toHaveLength(2); - expect(result.current.publicKeys[0].name).toBe("key-1"); - expect(result.current.publicKeys[1].name).toBe("key-2"); - }); - - it("returns totalCount from the X-Total-Count header", async () => { - sdk.getPublicKeys.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "42" }), - ); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.totalCount).toBe(42); - }); - - it("defaults publicKeys to empty array while loading", () => { - sdk.getPublicKeys.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - expect(result.current.publicKeys).toEqual([]); - }); - - it("defaults totalCount to 0 while loading", () => { - sdk.getPublicKeys.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - expect(result.current.totalCount).toBe(0); - }); - - it("returns isLoading true initially", () => { - sdk.getPublicKeys.mockReturnValue(new Promise(() => {})); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - expect(result.current.isLoading).toBe(true); - }); - - it("exposes error when query fails", async () => { - const networkError = new Error("network failure"); - sdk.getPublicKeys.mockRejectedValue(networkError); - - const { result } = renderHookWithClient(() => usePublicKeys()); - - await waitFor(() => expect(result.current.error).toBeTruthy()); - expect(result.current.error).toBe(networkError); - }); - }); - - describe("pagination defaults", () => { - it("uses page 1 and perPage 10 as defaults", async () => { - sdk.getPublicKeys.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => usePublicKeys()); - - await waitFor(() => expect(sdk.getPublicKeys).toHaveBeenCalled()); - const [opts] = sdk.getPublicKeys.mock.calls[0]; - expect(opts.query.page).toBe(1); - expect(opts.query.per_page).toBe(10); - }); - - it("forwards custom page and perPage", async () => { - sdk.getPublicKeys.mockResolvedValue( - mockSdkResponse([], { "X-Total-Count": "0" }), - ); - - renderHookWithClient(() => usePublicKeys({ page: 3, perPage: 25 })); - - await waitFor(() => expect(sdk.getPublicKeys).toHaveBeenCalled()); - const [opts] = sdk.getPublicKeys.mock.calls[0]; - expect(opts.query.page).toBe(3); - expect(opts.query.per_page).toBe(25); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/__tests__/useUploadLicense.test.ts b/ui/apps/console/src/hooks/__tests__/useUploadLicense.test.ts deleted file mode 100644 index cc9c1e608b3..00000000000 --- a/ui/apps/console/src/hooks/__tests__/useUploadLicense.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { waitFor, act } from "@testing-library/react"; -import { renderHookWithClient } from "@/tests/wrapper"; -import { mockSdkResponse } from "@/tests/sdk"; -import { useUploadLicense } from "../useUploadLicense"; - -const mockInvalidate = vi.fn(); - -const sdk = vi.hoisted(() => - mockSdkGen({ - sendLicense: vi.fn(), - }), -); - -vi.mock("../useInvalidateQueries", () => ({ - useInvalidateByIds: vi.fn(() => mockInvalidate), -})); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("useUploadLicense", () => { - describe("mutation call", () => { - it("calls sendLicense with the provided body", async () => { - sdk.sendLicense.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useUploadLicense()); - - const file = new File(["license-data"], "license.lic"); - await act(() => result.current.mutateAsync({ body: { file } })); - - expect(sdk.sendLicense).toHaveBeenCalledWith( - expect.objectContaining({ body: { file }, throwOnError: true }), - ); - }); - }); - - describe("on success", () => { - it("invalidates getLicense queries after a successful mutation", async () => { - sdk.sendLicense.mockResolvedValue(mockSdkResponse(undefined)); - const { result } = renderHookWithClient(() => useUploadLicense()); - - await act(() => result.current.mutateAsync({})); - - await waitFor(() => expect(mockInvalidate).toHaveBeenCalledTimes(1)); - }); - }); - - describe("on failure", () => { - it("rejects and exposes the error when sendLicense fails", async () => { - const error = new Error("upload failed"); - sdk.sendLicense.mockRejectedValue(error); - const { result } = renderHookWithClient(() => useUploadLicense()); - - act(() => result.current.mutate({})); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(result.current.error).toBe(error); - }); - - it("does not call invalidate when sendLicense fails", async () => { - sdk.sendLicense.mockRejectedValue(new Error("upload failed")); - const { result } = renderHookWithClient(() => useUploadLicense()); - - act(() => result.current.mutate({})); - - await waitFor(() => expect(result.current.isError).toBe(true)); - expect(mockInvalidate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/apps/console/src/hooks/useAcceptDeviceByCode.ts b/ui/apps/console/src/hooks/useAcceptDeviceByCode.ts index bda22501eda..9ecdd325b9f 100644 --- a/ui/apps/console/src/hooks/useAcceptDeviceByCode.ts +++ b/ui/apps/console/src/hooks/useAcceptDeviceByCode.ts @@ -1,22 +1,20 @@ import { useState } from "react"; -import { resolveDeviceLoginCode, acceptDevicePairing } from "@/client"; +import { + resolveDeviceLoginCode, + acceptDevicePairing, + useAcceptDevice, +} from "@/client/api"; import { isSdkError } from "@/api/errors"; import { useAuthStore } from "@/stores/authStore"; -import { useAcceptDevice } from "@/hooks/useDeviceMutations"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useNamespace } from "@/hooks/useNamespaces"; import { isSubscriptionBlocked } from "@/utils/billing"; import { getAcceptErrorMessage } from "@/utils/acceptErrors"; -/** - * Resolves a pairing/login code and accepts the device in one shot, into the - * current namespace. Used by the onboarding wizard's code-entry step, which - * skips the preview/confirm screen: the user just installed the device, so they - * type the code and go. Returns the accepted device, or null with `error` set. - */ +/** Resolves a pairing/login code and accepts the device into the current namespace. */ export function useAcceptDeviceByCode() { - const authTenant = useAuthStore((s) => s.tenant); - const { namespace } = useNamespace(authTenant ?? ""); + const authTenant = useAuthStore((s) => s.tenant) ?? ""; + const { namespace } = useNamespace(authTenant); const hasSubscription = isSubscriptionBlocked(namespace?.billing); const canSubscribe = useHasPermission("billing:subscribe"); const acceptDevice = useAcceptDevice(); @@ -29,23 +27,18 @@ export function useAcceptDeviceByCode() { setError(""); setIsPending(true); try { - const { data } = await resolveDeviceLoginCode({ - path: { code }, - throwOnError: true, - }); + const resolved = await resolveDeviceLoginCode(code); - if (data.kind === "pairing") { - const { data: accepted } = await acceptDevicePairing({ - path: { code }, - body: { tenant_id: authTenant ?? "" }, - throwOnError: true, + if (resolved.kind === "pairing") { + const accepted = await acceptDevicePairing(code, { + tenant_id: authTenant, }); - return { uid: accepted.uid ?? "", name: data.name ?? "" }; + return { uid: accepted.uid ?? "", name: resolved.name ?? "" }; } - if (data.uid) { - await acceptDevice.mutateAsync({ path: { uid: data.uid } }); - return { uid: data.uid, name: data.name ?? "" }; + if (resolved.uid) { + await acceptDevice.mutateAsync({ uid: resolved.uid }); + return { uid: resolved.uid, name: resolved.name ?? "" }; } setError( diff --git a/ui/apps/console/src/hooks/useAccessPolicies.ts b/ui/apps/console/src/hooks/useAccessPolicies.ts deleted file mode 100644 index 14b281f47ae..00000000000 --- a/ui/apps/console/src/hooks/useAccessPolicies.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { listAccessPoliciesOptions, type AccessPolicy } from "../client"; - -/** - * The namespace's access policies. Returns an empty array rather than undefined while loading, - * so a caller can map over it without a guard. - */ -export function useAccessPolicies() { - const result = useQuery(listAccessPoliciesOptions()); - - const policies = useMemo( - () => result.data ?? [], - [result.data], - ); - - return { - policies, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useAccessPolicyMutations.ts b/ui/apps/console/src/hooks/useAccessPolicyMutations.ts deleted file mode 100644 index b3e578b58a0..00000000000 --- a/ui/apps/console/src/hooks/useAccessPolicyMutations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createAccessPolicyMutation, - updateAccessPolicyMutation, - deleteAccessPolicyMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates an access policy, refreshing the list on success. - */ -export function useCreateAccessPolicy() { - const invalidate = useInvalidateByIds("listAccessPolicies"); - return useMutation({ - ...createAccessPolicyMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates an access policy, refreshing the list on success. - */ -export function useUpdateAccessPolicy() { - const invalidate = useInvalidateByIds("listAccessPolicies"); - return useMutation({ - ...updateAccessPolicyMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes an access policy, refreshing the list on success. - */ -export function useDeleteAccessPolicy() { - const invalidate = useInvalidateByIds("listAccessPolicies"); - return useMutation({ - ...deleteAccessPolicyMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminAccountRequestMutations.ts b/ui/apps/console/src/hooks/useAdminAccountRequestMutations.ts deleted file mode 100644 index 4cffaf84b29..00000000000 --- a/ui/apps/console/src/hooks/useAdminAccountRequestMutations.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { approveUserMutation } from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Approves a pending account request. This clears awaiting_approval only — the account stays - * unconfirmed until the person activates it, which is what lets an activation link be minted - * for them afterwards from the members list. - */ -export function useApproveAccountRequest() { - const invalidate = useInvalidateByIds("getUsers", "getUser"); - return useMutation({ - ...approveUserMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminAccountRequests.ts b/ui/apps/console/src/hooks/useAdminAccountRequests.ts deleted file mode 100644 index 6eb2d12adac..00000000000 --- a/ui/apps/console/src/hooks/useAdminAccountRequests.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getUsers as getUsersSdk, - getUsersQueryKey, - type GetUsersData, - type UserAdminResponse, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; -import { toBase64Json } from "@/utils/encoding"; - -// Accounts a namespace admin provisioned that a system admin has not approved yet -// are just users flagged awaiting_approval; the "requests" queue is that filter. -const AWAITING_APPROVAL_FILTER = toBase64Json([ - { - type: "property", - params: { name: "awaiting_approval", operator: "bool", value: true }, - }, -]); - -interface UseAdminAccountRequestsParams { - page?: number; - perPage?: number; - enabled?: boolean; -} - -/** - * The accounts waiting for approval. Runs only for an admin, so a non-admin never issues the - * request that would be refused. - */ -export function useAdminAccountRequests({ - page = 1, - perPage = 10, - enabled = true, -}: UseAdminAccountRequestsParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const query: GetUsersData["query"] = { - page, - per_page: perPage, - filter: AWAITING_APPROVAL_FILTER, - }; - const options = { query }; - - const result = useQuery>({ - queryKey: getUsersQueryKey(options), - queryFn: paginatedQueryFn(getUsersSdk, options), - enabled: isAdmin && enabled, - staleTime: 60 * 1000, // 1 minute - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - requests: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useAdminAnnouncementMutations.ts b/ui/apps/console/src/hooks/useAdminAnnouncementMutations.ts deleted file mode 100644 index e3ebc868152..00000000000 --- a/ui/apps/console/src/hooks/useAdminAnnouncementMutations.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createAnnouncementMutation, - updateAnnouncementMutation, - deleteAnnouncementMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates an announcement, refreshing the admin list on success. - */ -export function useAdminCreateAnnouncement() { - const invalidate = useInvalidateByIds("listAnnouncementsAdmin"); - return useMutation({ - ...createAnnouncementMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates an announcement, refreshing both the list and the single-announcement query. - */ -export function useAdminUpdateAnnouncement() { - const invalidate = useInvalidateByIds( - "listAnnouncementsAdmin", - "getAnnouncementAdmin", - ); - return useMutation({ - ...updateAnnouncementMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes an announcement, refreshing both the list and the single-announcement query. - */ -export function useAdminDeleteAnnouncement() { - const invalidate = useInvalidateByIds( - "listAnnouncementsAdmin", - "getAnnouncementAdmin", - ); - return useMutation({ - ...deleteAnnouncementMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminAnnouncements.ts b/ui/apps/console/src/hooks/useAdminAnnouncements.ts deleted file mode 100644 index 69d0dc273ab..00000000000 --- a/ui/apps/console/src/hooks/useAdminAnnouncements.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - listAnnouncementsAdmin as listAnnouncementsAdminSdk, - listAnnouncementsAdminQueryKey, - getAnnouncementAdminOptions, - type ListAnnouncementsAdminData, - type AnnouncementShort, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; - -interface UseAdminAnnouncementsParams { - page?: number; - perPage?: number; -} - -/** - * A page of announcements for the admin list. Admin-only, so it does not run for anyone else. - */ -export function useAdminAnnouncements({ - page = 1, - perPage = 10, -}: UseAdminAnnouncementsParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const query: ListAnnouncementsAdminData["query"] = { - page, - per_page: perPage, - order_by: "desc", - }; - const options = { query }; - - const result = useQuery>({ - queryKey: listAnnouncementsAdminQueryKey(options), - queryFn: paginatedQueryFn(listAnnouncementsAdminSdk, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - announcements: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * One announcement by UUID. Cached for five minutes and not retried on a 4xx, since a missing or - * forbidden announcement will not appear on a second attempt. - */ -export function useAdminAnnouncement(uuid: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - return useQuery({ - ...getAnnouncementAdminOptions({ path: { uuid } }), - enabled: isAdmin && !!uuid, - staleTime: 5 * 60 * 1000, - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminDevices.ts b/ui/apps/console/src/hooks/useAdminDevices.ts deleted file mode 100644 index 3a5966c2624..00000000000 --- a/ui/apps/console/src/hooks/useAdminDevices.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getDevicesAdmin, - getDevicesAdminQueryKey, - getDeviceAdminOptions, - type GetDevicesAdminData, - type Device, - type DeviceStatus, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; -import { toBase64Json } from "@/utils/encoding"; -import { normalizeDeviceTags } from "@/utils/deviceTags"; - -export type { TaggedDevice as NormalizedDevice } from "@/utils/deviceTags"; - -function buildNameFilter(search: string): string { - const filter = [ - { - type: "property", - params: { name: "name", operator: "contains", value: search }, - }, - ]; - return toBase64Json(filter); -} - -interface UseAdminDevicesParams { - page?: number; - perPage?: number; - search?: string; - status?: DeviceStatus | ""; - sortBy?: string; - orderBy?: "asc" | "desc"; -} - -/** - * A page of devices across every namespace, for the admin device list. - */ -export function useAdminDevices({ - page = 1, - perPage = 10, - search = "", - status = "", - sortBy = "last_seen", - orderBy = "desc", -}: UseAdminDevicesParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const query: GetDevicesAdminData["query"] = { - page, - per_page: perPage, - sort_by: sortBy, - order_by: orderBy, - }; - if (search) query.filter = buildNameFilter(search); - if (status) query.status = status; - const options = { query }; - - const result = useQuery>({ - queryKey: getDevicesAdminQueryKey(options), - queryFn: paginatedQueryFn(getDevicesAdmin, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - const devices = useMemo( - () => result.data?.data.map(normalizeDeviceTags) ?? [], - [result.data], - ); - - return { - devices, - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * One device by UID, for the admin detail view. Cached for five minutes and not retried on a - * 4xx, since a device that is missing or out of reach will not appear on a retry. - */ -export function useAdminDevice(uid: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - return useQuery({ - ...getDeviceAdminOptions({ path: { uid } }), - enabled: isAdmin && !!uid, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - select: normalizeDeviceTags, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminFirewallRules.ts b/ui/apps/console/src/hooks/useAdminFirewallRules.ts deleted file mode 100644 index ccb3ed98b23..00000000000 --- a/ui/apps/console/src/hooks/useAdminFirewallRules.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getFirewallRulesAdmin, - getFirewallRulesAdminQueryKey, - getFirewallRuleAdminOptions, - type GetFirewallRulesAdminData, - type FirewallRulesResponse, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; - -interface UseAdminFirewallRulesParams { - page?: number; - perPage?: number; -} - -/** - * A page of firewall rules across every namespace, for the admin list. - */ -export function useAdminFirewallRules({ - page = 1, - perPage = 10, -}: UseAdminFirewallRulesParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const options = { - query: { - page, - per_page: perPage, - } satisfies GetFirewallRulesAdminData["query"], - }; - - const result = useQuery>({ - queryKey: getFirewallRulesAdminQueryKey(options), - queryFn: paginatedQueryFn(getFirewallRulesAdmin, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - const rules = useMemo(() => result.data?.data ?? [], [result.data]); - - return { - rules, - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} - -/** - * One firewall rule by id, for the admin detail view. Cached for five minutes and not retried on - * a 4xx. - */ -export function useAdminFirewallRule(id: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - return useQuery({ - ...getFirewallRuleAdminOptions({ path: { id } }), - enabled: isAdmin && !!id, - staleTime: 5 * 60 * 1000, - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminLicense.ts b/ui/apps/console/src/hooks/useAdminLicense.ts index c87b70b31b8..6f348da41cd 100644 --- a/ui/apps/console/src/hooks/useAdminLicense.ts +++ b/ui/apps/console/src/hooks/useAdminLicense.ts @@ -1,16 +1,13 @@ import { useQuery } from "@tanstack/react-query"; -import { - getLicense, - getLicenseQueryKey, - type GetLicenseResponse, -} from "../client"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; -import { isCloud } from "../env"; +import { getLicense, getGetLicenseQueryKey } from "@/client/api"; +import type { GetLicense200 } from "@/client/model"; +import { useAuthStore } from "@/stores/authStore"; +import { isSdkError } from "@/api/errors"; +import { isCloud } from "@/env"; -export { getLicenseQueryKey }; +export { getGetLicenseQueryKey }; -type LicenseData = GetLicenseResponse | null; +type LicenseData = GetLicense200 | null; /** * The installed licence, or null when there is none. Not run on cloud, where licensing is the @@ -21,18 +18,17 @@ export function useAdminLicense() { const enabled = isAdmin && !isCloud(); const query = useQuery({ - queryKey: getLicenseQueryKey(), - queryFn: async ({ signal }) => { + queryKey: getGetLicenseQueryKey(), + queryFn: async () => { try { - const { data } = await getLicense({ signal, throwOnError: true }); - return data; + return await getLicense(); } catch (err) { if (isSdkError(err) && err.status === 400) return null; throw err; } }, enabled, - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: 5 * 60 * 1000, retry: (count) => count < 1, refetchOnWindowFocus: false, }); diff --git a/ui/apps/console/src/hooks/useAdminNamespaceMutations.ts b/ui/apps/console/src/hooks/useAdminNamespaceMutations.ts deleted file mode 100644 index 05012267503..00000000000 --- a/ui/apps/console/src/hooks/useAdminNamespaceMutations.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - editNamespaceAdminMutation, - deleteNamespaceAdminMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Edits a namespace as an admin, refreshing both the list and the detail query. - */ -export function useAdminEditNamespace() { - const invalidate = useInvalidateByIds( - "getNamespacesAdmin", - "getNamespaceAdmin", - ); - return useMutation({ - ...editNamespaceAdminMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a namespace as an admin. Refreshes the list and the detail query; the namespace and - * everything in it are gone, so there is nothing to undo. - */ -export function useAdminDeleteNamespace() { - const invalidate = useInvalidateByIds( - "getNamespacesAdmin", - "getNamespaceAdmin", - ); - return useMutation({ - ...deleteNamespaceAdminMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminNamespaces.ts b/ui/apps/console/src/hooks/useAdminNamespaces.ts deleted file mode 100644 index 247d69cbb1f..00000000000 --- a/ui/apps/console/src/hooks/useAdminNamespaces.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getNamespacesAdmin as getNamespacesAdminSdk, - getNamespacesAdminQueryKey, - getNamespaceAdminOptions, - type GetNamespacesAdminData, - type Namespace, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; -import { toBase64Json } from "@/utils/encoding"; - -function buildNameFilter(search: string): string { - const filter = [ - { - type: "property", - params: { name: "name", operator: "contains", value: search }, - }, - ]; - return toBase64Json(filter); -} - -interface UseAdminNamespacesParams { - page?: number; - perPage?: number; - search?: string; -} - -/** - * A page of namespaces for the admin list. Admin-only, so it does not run for anyone else. - */ -export function useAdminNamespaces({ - page = 1, - perPage = 10, - search = "", -}: UseAdminNamespacesParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const query: GetNamespacesAdminData["query"] = { page, per_page: perPage }; - if (search) query.filter = buildNameFilter(search); - const options = { query }; - - const result = useQuery>({ - queryKey: getNamespacesAdminQueryKey(options), - queryFn: paginatedQueryFn(getNamespacesAdminSdk, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - namespaces: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * One namespace by tenant id, for the admin detail view. - */ -export function useAdminNamespace(tenantId: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - return useQuery({ - ...getNamespaceAdminOptions({ path: { tenant: tenantId } }), - enabled: isAdmin && !!tenantId, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminSessionDetail.ts b/ui/apps/console/src/hooks/useAdminSessionDetail.ts deleted file mode 100644 index 581dde6387b..00000000000 --- a/ui/apps/console/src/hooks/useAdminSessionDetail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getSessionAdminOptions } from "../client"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; - -/** - * One session by UID, for the admin detail view. Cached for a minute only: a live session's - * state changes while it is being looked at. - */ -export function useAdminSessionDetail(uid: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const result = useQuery({ - ...getSessionAdminOptions({ path: { uid } }), - enabled: isAdmin && !!uid, - staleTime: 60 * 1000, - retry: (count, err) => isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - session: result.data ?? null, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useAdminSessions.ts b/ui/apps/console/src/hooks/useAdminSessions.ts deleted file mode 100644 index 99ed87a5a64..00000000000 --- a/ui/apps/console/src/hooks/useAdminSessions.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getSessionsAdmin, - getSessionsAdminQueryKey, - type GetSessionsAdminData, - type Session, -} from "@/client"; -import { paginatedQueryFn, type PaginatedResult } from "@/api/pagination"; -import { useAuthStore } from "@/stores/authStore"; -import { isSdkError } from "@/api/errors"; - -/** - * A page of sessions across every namespace, for the admin list. - */ -export function useAdminSessions({ page = 1, perPage = 5 } = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - const options = { query: { page, per_page: perPage } } satisfies { - query: GetSessionsAdminData["query"]; - }; - - const result = useQuery>({ - queryKey: getSessionsAdminQueryKey(options), - queryFn: paginatedQueryFn(getSessionsAdmin, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - sessions: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useAdminStats.ts b/ui/apps/console/src/hooks/useAdminStats.ts deleted file mode 100644 index 8215e66adfc..00000000000 --- a/ui/apps/console/src/hooks/useAdminStats.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getStatsOptions, getStatsQueryKey } from "../client"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; - -export { getStatsQueryKey }; - -/** - * Instance-wide counts for the admin dashboard. - */ -export function useAdminStats() { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const result = useQuery({ - ...getStatsOptions(), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - stats: result.data, - isLoading: result.isLoading, - isError: result.isError, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useAdminUserMutations.ts b/ui/apps/console/src/hooks/useAdminUserMutations.ts deleted file mode 100644 index 8f644d4a943..00000000000 --- a/ui/apps/console/src/hooks/useAdminUserMutations.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createUserAdminMutation, - adminUpdateUserMutation, - adminDeleteUserMutation, - adminResetUserPasswordMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a user as an admin, refreshing the user list. - */ -export function useCreateUser() { - const invalidate = useInvalidateByIds("getUsers"); - return useMutation({ - ...createUserAdminMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates a user as an admin, refreshing the list and the user's own query. - */ -export function useUpdateUser() { - const invalidate = useInvalidateByIds("getUsers", "getUser"); - return useMutation({ - ...adminUpdateUserMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a user as an admin, refreshing the list and the user's own query. - */ -export function useDeleteUser() { - const invalidate = useInvalidateByIds("getUsers", "getUser"); - return useMutation({ - ...adminDeleteUserMutation(), - onSuccess: invalidate, - }); -} - -/** - * Resets a user's password as an admin. The user is not notified by this call, so whoever ran it - * has to tell them. - */ -export function useResetUserPassword() { - const invalidate = useInvalidateByIds("getUsers", "getUser"); - return useMutation({ - ...adminResetUserPasswordMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useAdminUsers.ts b/ui/apps/console/src/hooks/useAdminUsers.ts deleted file mode 100644 index 25aec859aab..00000000000 --- a/ui/apps/console/src/hooks/useAdminUsers.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getUsers as getUsersSdk, - getUsersQueryKey, - getUserOptions, - type GetUsersData, - type UserAdminResponse, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { useAuthStore } from "../stores/authStore"; -import { isSdkError } from "../api/errors"; -import { toBase64Json } from "@/utils/encoding"; - -function buildUsernameFilter(search: string): string { - const filter = [ - { - type: "property", - params: { name: "username", operator: "contains", value: search }, - }, - ]; - return toBase64Json(filter); -} - -interface UseAdminUsersParams { - page?: number; - perPage?: number; - search?: string; -} - -/** - * A page of users for the admin list. - */ -export function useAdminUsers({ - page = 1, - perPage = 10, - search = "", -}: UseAdminUsersParams = {}) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - const query: GetUsersData["query"] = { page, per_page: perPage }; - if (search) query.filter = buildUsernameFilter(search); - const options = { query }; - - const result = useQuery>({ - queryKey: getUsersQueryKey(options), - queryFn: paginatedQueryFn(getUsersSdk, options), - enabled: isAdmin, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); - - return { - users: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * One user by id, for the admin detail view. - */ -export function useAdminUser(id: string) { - const isAdmin = useAuthStore((s) => s.isAdmin); - - return useQuery({ - ...getUserOptions({ path: { id } }), - enabled: isAdmin && !!id, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: (count, err) => - isSdkError(err) && err.status === 401 ? false : count < 1, - refetchOnWindowFocus: false, - }); -} diff --git a/ui/apps/console/src/hooks/useApiKeyMutations.ts b/ui/apps/console/src/hooks/useApiKeyMutations.ts deleted file mode 100644 index d6a67621fb2..00000000000 --- a/ui/apps/console/src/hooks/useApiKeyMutations.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - apiKeyCreateMutation, - apiKeyUpdateMutation, - apiKeyDeleteMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates an API key, refreshing the list. The response carries the only copy of the secret the - * caller will ever see, so it has to be shown before the mutation's data is discarded. - */ -export function useCreateApiKey() { - const invalidate = useInvalidateByIds("apiKeyList"); - return useMutation({ - ...apiKeyCreateMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates an API key's name or role, refreshing the list. The secret is not re-issued. - */ -export function useUpdateApiKey() { - const invalidate = useInvalidateByIds("apiKeyList"); - return useMutation({ - ...apiKeyUpdateMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes an API key, refreshing the list. Anything still authenticating with it starts failing - * at once. - */ -export function useDeleteApiKey() { - const invalidate = useInvalidateByIds("apiKeyList"); - return useMutation({ - ...apiKeyDeleteMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useApiKeys.ts b/ui/apps/console/src/hooks/useApiKeys.ts deleted file mode 100644 index a4d1a6a1802..00000000000 --- a/ui/apps/console/src/hooks/useApiKeys.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - apiKeyList, - apiKeyListQueryKey, - type ApiKeyListData, - type ApiKey, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; - -interface UseApiKeysParams { - page?: number; - perPage?: number; - sortBy?: string; - orderBy?: "asc" | "desc"; -} - -/** - * A page of the namespace's API keys, newest first. - */ -export function useApiKeys({ - page = 1, - perPage = 10, - sortBy = "created_at", - orderBy = "desc", -}: UseApiKeysParams = {}) { - const options = { query: { page, per_page: perPage, sort_by: sortBy, order_by: orderBy } } satisfies { query: ApiKeyListData["query"] }; - - const result = useQuery>({ - queryKey: apiKeyListQueryKey(options), - queryFn: paginatedQueryFn(apiKeyList, options), - }); - - return { - apiKeys: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useBilling.ts b/ui/apps/console/src/hooks/useBilling.ts deleted file mode 100644 index e01abd503f8..00000000000 --- a/ui/apps/console/src/hooks/useBilling.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { - getCustomerOptions, - getSubscriptionOptions, - createCustomerMutation, - createSubscriptionMutation, - attachPaymentMethodMutation, - detachPaymentMethodMutation, - setDefaultPaymentMethodMutation, - createBillingPortalSession, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -function useInvalidateBilling() { - return useInvalidateByIds("getCustomer", "getSubscription", "getNamespace"); -} - -/** - * The namespace's billing customer, if it has one. enabled is a parameter because a namespace - * without billing has no customer to fetch and the call would 404. - */ -export function useCustomer(enabled = true) { - const result = useQuery({ - ...getCustomerOptions(), - enabled, - }); - return { - customer: result.data, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * The namespace's subscription, if it has one. - */ -export function useSubscription(enabled = true) { - const result = useQuery({ - ...getSubscriptionOptions(), - enabled, - }); - return { - subscription: result.data, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * Creates the billing customer, refreshing every billing query on success. - */ -export function useCreateCustomer() { - const invalidate = useInvalidateBilling(); - return useMutation({ - ...createCustomerMutation(), - onSuccess: invalidate, - }); -} - -/** - * Starts a subscription, refreshing every billing query on success. - */ -export function useCreateSubscription() { - const invalidate = useInvalidateBilling(); - return useMutation({ - ...createSubscriptionMutation(), - onSuccess: invalidate, - }); -} - -/** - * Attaches a payment method, refreshing every billing query on success. - */ -export function useAttachPaymentMethod() { - const invalidate = useInvalidateBilling(); - return useMutation({ - ...attachPaymentMethodMutation(), - onSuccess: invalidate, - }); -} - -/** - * Detaches a payment method, refreshing every billing query on success. - */ -export function useDetachPaymentMethod() { - const invalidate = useInvalidateBilling(); - return useMutation({ - ...detachPaymentMethodMutation(), - onSuccess: invalidate, - }); -} - -/** - * Makes a payment method the default one, refreshing every billing query on success. - */ -export function useSetDefaultPaymentMethod() { - const invalidate = useInvalidateBilling(); - return useMutation({ - ...setDefaultPaymentMethodMutation(), - onSuccess: invalidate, - }); -} - -/** - * Opens the provider's billing portal. The URL is single-use and short-lived, so it is minted on - * demand rather than fetched with the page and held. - */ -export function useOpenBillingPortal() { - return useMutation({ - mutationFn: async () => { - const { data } = await createBillingPortalSession({ throwOnError: true }); - if (!data.url) throw new Error("Missing billing portal URL"); - window.open(data.url, "_blank", "noopener,noreferrer"); - return data.url; - }, - }); -} diff --git a/ui/apps/console/src/hooks/useChatwoot.ts b/ui/apps/console/src/hooks/useChatwoot.ts index 63401b9e19a..47b3a3fb5e1 100644 --- a/ui/apps/console/src/hooks/useChatwoot.ts +++ b/ui/apps/console/src/hooks/useChatwoot.ts @@ -9,7 +9,7 @@ import { import { getConfig, isCloud } from "@/env"; import { useAuthStore } from "@/stores/authStore"; import { useNamespace } from "@/hooks/useNamespaces"; -import { useSupportIdentifier } from "@/hooks/useSupportIdentifier"; +import { useGetNamespaceSupport } from "@/client/api"; import { hasActiveSubscription } from "@/utils/billing"; import { falseSnapshot, @@ -75,10 +75,15 @@ export function useChatwoot(): ChatwootHandle { const hasCloudConfig = !!config.chatwootWebsiteToken && !!config.chatwootBaseUrl; - const { identifier, isError: identifierError } = useSupportIdentifier( - tenant, - isCloudEdition && hasCloudConfig && hasActiveBilling, - ); + const { data: supportData, isError: identifierError } = + useGetNamespaceSupport(tenant ?? "", { + query: { + enabled: + isCloudEdition && hasCloudConfig && hasActiveBilling && !!tenant, + retry: 1, + }, + }); + const identifier = supportData?.identifier ?? null; const widgetReady = useSyncExternalStore( subscribeChatwootState, diff --git a/ui/apps/console/src/hooks/useContainer.ts b/ui/apps/console/src/hooks/useContainer.ts deleted file mode 100644 index 998f3abcf0e..00000000000 --- a/ui/apps/console/src/hooks/useContainer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getContainerOptions } from "../client"; - -/** - * One container by UID. Idle until a UID is given, so a route that has not resolved its - * parameter yet does not issue a request for an empty path. - */ -export function useContainer(uid: string) { - const result = useQuery({ - ...getContainerOptions({ path: { uid } }), - enabled: !!uid, - }); - - return { - container: (result.data ?? null), - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useContainerActionRunner.ts b/ui/apps/console/src/hooks/useContainerActionRunner.ts index 4635f2f9a49..b7ae91257b1 100644 --- a/ui/apps/console/src/hooks/useContainerActionRunner.ts +++ b/ui/apps/console/src/hooks/useContainerActionRunner.ts @@ -1,26 +1,20 @@ import { useCallback } from "react"; -import { - useUpdateContainerStatus, - useRemoveContainer, -} from "@/hooks/useContainerMutations"; +import { useUpdateContainerStatus, useDeleteContainer } from "@/client/api"; import type { EntityBase, EntityOperation } from "@/hooks/useActionDialog"; -/** - * Runs the confirmation dialog's chosen operation against a container, so the dialog does not - * have to know which mutation each operation maps to. - */ +/** Maps action-dialog operations to container mutations. */ export function useContainerActionRunner() { const status = useUpdateContainerStatus(); - const remove = useRemoveContainer(); + const remove = useDeleteContainer(); return useCallback( async (entity: EntityBase, operation: EntityOperation) => { if (operation === "remove") { - await remove.mutateAsync({ path: { uid: entity.uid } }); + await remove.mutateAsync({ uid: entity.uid }); return; } - await status.mutateAsync({ path: { uid: entity.uid, status: operation } }); + await status.mutateAsync({ uid: entity.uid, status: operation }); }, [status, remove], ); diff --git a/ui/apps/console/src/hooks/useContainerMutations.ts b/ui/apps/console/src/hooks/useContainerMutations.ts index 1808238d82f..9581ba7777b 100644 --- a/ui/apps/console/src/hooks/useContainerMutations.ts +++ b/ui/apps/console/src/hooks/useContainerMutations.ts @@ -1,88 +1,19 @@ import { useMutation } from "@tanstack/react-query"; import { isSdkError } from "../api/errors"; -import { - deleteContainerMutation, - updateContainerMutation, - updateContainerStatusMutation, - createTag, - pushTagToContainer, - pullTagFromContainer, -} from "../client"; +import { createTag, pushTagToContainer } from "@/client/api"; import { useInvalidateByIds } from "./useInvalidateQueries"; -/** - * Accepts or rejects a pending container, refreshing the list and the container itself. - */ -export function useUpdateContainerStatus() { - const invalidate = useInvalidateByIds("getContainers", "getContainer"); - return useMutation({ - ...updateContainerStatusMutation(), - onSuccess: invalidate, - }); -} - -/** - * Removes a container from the namespace. - */ -export function useRemoveContainer() { - const invalidate = useInvalidateByIds("getContainers", "getContainer"); - return useMutation({ - ...deleteContainerMutation(), - onSuccess: invalidate, - }); -} - -/** - * Renames a container. - */ -export function useRenameContainer() { - const invalidate = useInvalidateByIds("getContainers", "getContainer"); - return useMutation({ - ...updateContainerMutation(), - onSuccess: invalidate, - }); -} - -/** - * Tags a container. The tag list is refreshed too, because a tag may not have existed before. - */ +/** Creates the tag if it doesn't exist (swallows 409), then pushes it to the container. */ export function useAddContainerTag() { - const invalidate = useInvalidateByIds( - "getContainers", - "getContainer", - "getTags", - ); + const invalidate = useInvalidateByIds("/api/containers", "/api/tags"); return useMutation({ - mutationFn: async (options: { path: { uid: string; name: string } }) => { + mutationFn: async ({ uid, name }: { uid: string; name: string }) => { try { - await createTag({ - body: { name: options.path.name }, - throwOnError: true, - }); + await createTag({ name }); } catch (e) { if (!isSdkError(e) || e.status !== 409) throw e; } - return pushTagToContainer({ - path: { uid: options.path.uid, name: options.path.name }, - throwOnError: true, - }); - }, - onSuccess: invalidate, - }); -} - -/** - * Removes a tag from a container. The tag itself survives on anything else carrying it, so the - * tag list is not refreshed. - */ -export function useRemoveContainerTag() { - const invalidate = useInvalidateByIds("getContainers", "getContainer"); - return useMutation({ - mutationFn: async (options: { path: { uid: string; name: string } }) => { - return pullTagFromContainer({ - path: { uid: options.path.uid, name: options.path.name }, - throwOnError: true, - }); + return pushTagToContainer(uid, name); }, onSuccess: invalidate, }); diff --git a/ui/apps/console/src/hooks/useContainers.ts b/ui/apps/console/src/hooks/useContainers.ts deleted file mode 100644 index 31d80e88b8d..00000000000 --- a/ui/apps/console/src/hooks/useContainers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getContainers as getContainersSdk, - getContainersQueryKey, - type GetContainersData, - type Device, - type DeviceStatus, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { toBase64Json } from "@/utils/encoding"; -import { normalizeDeviceTags } from "@/utils/deviceTags"; - -export type { TaggedDevice as NormalizedContainer } from "@/utils/deviceTags"; - -function buildFilter(search: string, tags: string[]): string { - const filters: Record[] = []; - if (search) { - filters.push({ - type: "property", - params: { name: "name", operator: "contains", value: search }, - }); - } - if (tags.length > 0) { - filters.push({ - type: "property", - params: { name: "tags.name", operator: "contains", value: tags }, - }); - } - return toBase64Json(filters); -} - -interface UseContainersParams { - page?: number; - perPage?: number; - status?: DeviceStatus | ""; - search?: string; - filterTags?: string[]; - sortBy?: string; - orderBy?: "asc" | "desc"; -} - -/** - * A page of the namespace's containers, filtered by status, search and tags. - */ -export function useContainers({ - page = 1, - perPage = 10, - status = "", - search = "", - filterTags = [], - sortBy = "last_seen", - orderBy = "desc", -}: UseContainersParams = {}) { - const query: GetContainersData["query"] = { page, per_page: perPage }; - if (status) query.status = status; - if (search || filterTags.length > 0) - query.filter = buildFilter(search, filterTags); - query.sort_by = sortBy; - query.order_by = orderBy; - - const options = { query }; - - const result = useQuery>({ - queryKey: getContainersQueryKey(options), - queryFn: paginatedQueryFn(getContainersSdk, options), - }); - - const containers = useMemo( - () => result.data?.data.map(normalizeDeviceTags) ?? [], - [result.data], - ); - - return { - containers, - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useDevice.ts b/ui/apps/console/src/hooks/useDevice.ts deleted file mode 100644 index 5addaa04882..00000000000 --- a/ui/apps/console/src/hooks/useDevice.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getDeviceOptions } from "../client"; - -/** - * One device by UID. Idle until a UID is given. - */ -export function useDevice(uid: string) { - const result = useQuery({ - ...getDeviceOptions({ path: { uid } }), - enabled: !!uid, - }); - - return { - device: result.data ?? null, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useDeviceActionRunner.ts b/ui/apps/console/src/hooks/useDeviceActionRunner.ts index d5a71fb9026..7061f10c84f 100644 --- a/ui/apps/console/src/hooks/useDeviceActionRunner.ts +++ b/ui/apps/console/src/hooks/useDeviceActionRunner.ts @@ -1,29 +1,26 @@ import { useCallback } from "react"; import { useAcceptDevice, - useRejectDevice, - useRemoveDevice, -} from "@/hooks/useDeviceMutations"; + useUpdateDeviceStatus, + useDeleteDevice, +} from "@/client/api"; import type { EntityBase, EntityOperation } from "@/hooks/useActionDialog"; -/** - * Runs the confirmation dialog's chosen operation against a device, so the dialog does not have - * to know which mutation each operation maps to. - */ +/** Maps action-dialog operations to device mutations. */ export function useDeviceActionRunner() { const accept = useAcceptDevice(); - const reject = useRejectDevice(); - const remove = useRemoveDevice(); + const reject = useUpdateDeviceStatus(); + const remove = useDeleteDevice(); return useCallback( async (entity: EntityBase, operation: EntityOperation) => { if (operation === "reject") { - await reject.mutateAsync({ path: { uid: entity.uid, status: "reject" } }); + await reject.mutateAsync({ uid: entity.uid, status: "reject" }); return; } const action = operation === "accept" ? accept : remove; - await action.mutateAsync({ path: { uid: entity.uid } }); + await action.mutateAsync({ uid: entity.uid }); }, [accept, reject, remove], ); diff --git a/ui/apps/console/src/hooks/useDeviceChooser.ts b/ui/apps/console/src/hooks/useDeviceChooser.ts deleted file mode 100644 index 4b80dc2fc75..00000000000 --- a/ui/apps/console/src/hooks/useDeviceChooser.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { - choiceDevicesMutation, - getDevicesMostUsedOptions, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; -import { normalizeDeviceTags, type TaggedDevice } from "@/utils/deviceTags"; - -/** - * The devices used most, offered first when a namespace over its limit has to choose which to - * keep. Tags are normalized, so callers get plain strings. - */ -export function useSuggestedDevices(enabled = true) { - const result = useQuery({ - ...getDevicesMostUsedOptions(), - enabled, - }); - const devices: TaggedDevice[] = (result.data ?? []).map(normalizeDeviceTags); - return { - devices, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * Commits the chosen devices. Everything not chosen loses its place, so this refreshes the - * device queries and the counts together. - */ -export function useChoiceDevices() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - ); - return useMutation({ - ...choiceDevicesMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useDeviceCode.ts b/ui/apps/console/src/hooks/useDeviceCode.ts deleted file mode 100644 index 1c9d5f660fc..00000000000 --- a/ui/apps/console/src/hooks/useDeviceCode.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery, useMutation } from "@tanstack/react-query"; -import { - resolveDeviceLoginCodeOptions, - acceptDevicePairingMutation, -} from "@/client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Resolves an enrolment code to the device waiting behind it. Never retried and never stale: a - * code is single-use, so a second attempt would fail and a cached answer would be wrong. - */ -export function useResolveDeviceCode(code: string) { - const { data, isLoading, isError, error } = useQuery({ - ...resolveDeviceLoginCodeOptions({ path: { code } }), - enabled: !!code, - retry: false, - staleTime: Infinity, - }); - - return { device: data ?? null, isLoading, isError, error }; -} - -/** - * Completes an enrolment from a code, adding the device to the namespace and refreshing the - * device queries and counts. - */ -export function useAcceptDevicePairing() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - "getStats", - "installKeyList", - ); - return useMutation({ - ...acceptDevicePairingMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useDeviceMutations.ts b/ui/apps/console/src/hooks/useDeviceMutations.ts index 04bd0ab871b..6f18e37bff3 100644 --- a/ui/apps/console/src/hooks/useDeviceMutations.ts +++ b/ui/apps/console/src/hooks/useDeviceMutations.ts @@ -1,144 +1,24 @@ import { useMutation } from "@tanstack/react-query"; import { isSdkError } from "../api/errors"; -import { - acceptDeviceMutation, - updateDeviceStatusMutation, - deleteDeviceMutation, - updateDeviceMutation, - pullTagFromDeviceMutation, - setDeviceCustomFieldMutation, - deleteDeviceCustomFieldMutation, - createTag, - pushTagToDevice, -} from "../client"; +import { createTag, pushTagToDevice } from "@/client/api"; import { useInvalidateByIds } from "./useInvalidateQueries"; -/** - * Accepts a pending device. The counts change with it, so the stats query is refreshed as well - * as the lists — including the install keys list, which carries each key's pending count. - */ -export function useAcceptDevice() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - "getStats", - "installKeyList", - ); - return useMutation({ - ...acceptDeviceMutation(), - onSuccess: invalidate, - }); -} - -/** - * Rejects a pending device. The device leaves the pending list but the accepted count is - * unchanged, so stats are not refreshed. - */ -export function useRejectDevice() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - "installKeyList", - ); - return useMutation({ - ...updateDeviceStatusMutation(), - onSuccess: invalidate, - }); -} - -/** - * Removes a device from the namespace, refreshing the lists and the counts. - */ -export function useRemoveDevice() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - "getStats", - "installKeyList", - ); - return useMutation({ - ...deleteDeviceMutation(), - onSuccess: invalidate, - }); -} - -/** - * Renames a device. - */ -export function useRenameDevice() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - ); - return useMutation({ - ...updateDeviceMutation(), - onSuccess: invalidate, - }); -} - -/** - * Sets a custom field on a device. - */ -export function useSetDeviceCustomField() { - const invalidate = useInvalidateByIds("getDevices", "getDevice"); - return useMutation({ - ...setDeviceCustomFieldMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a custom field from a device. - */ -export function useDeleteDeviceCustomField() { - const invalidate = useInvalidateByIds("getDevices", "getDevice"); - return useMutation({ - ...deleteDeviceCustomFieldMutation(), - onSuccess: invalidate, - }); -} - -/** - * Tags a device. The tag list is refreshed too, because the tag may be new. - */ +/** Creates the tag if it doesn't exist (swallows 409), then pushes it to the device. */ export function useAddDeviceTag() { const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - "getTags", + "/api/devices", + "/api/stats", + "/api/tags", ); return useMutation({ - mutationFn: async (options: { path: { uid: string; name: string } }) => { + mutationFn: async ({ uid, name }: { uid: string; name: string }) => { try { - await createTag({ - body: { name: options.path.name }, - throwOnError: true, - }); + await createTag({ name }); } catch (e) { if (!isSdkError(e) || e.status !== 409) throw e; } - return pushTagToDevice({ ...options, throwOnError: true }); + return pushTagToDevice(uid, name); }, onSuccess: invalidate, }); } - -/** - * Removes a tag from a device. The tag survives on anything else carrying it. - */ -export function useRemoveDeviceTag() { - const invalidate = useInvalidateByIds( - "getDevices", - "getDevice", - "getStatusDevices", - ); - return useMutation({ - ...pullTagFromDeviceMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useDevices.ts b/ui/apps/console/src/hooks/useDevices.ts index c8874648892..efb289c05dc 100644 --- a/ui/apps/console/src/hooks/useDevices.ts +++ b/ui/apps/console/src/hooks/useDevices.ts @@ -1,13 +1,7 @@ import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getDevices as getDevicesSdk, - getDevicesQueryKey, - type GetDevicesData, - type DeviceStatus, - type Device as GeneratedDevice, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; +import { useGetDevices } from "@/client/api"; +import type { DeviceStatus, GetDevicesParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; import { toBase64Json } from "@/utils/encoding"; import { normalizeDeviceTags } from "@/utils/deviceTags"; @@ -66,29 +60,26 @@ export function useDevices({ sortBy = "last_seen", orderBy = "desc", }: UseDevicesParams = {}) { - const query: GetDevicesData["query"] = { page, per_page: perPage }; - if (status) query.status = status; + const params: GetDevicesParams = { + page, + per_page: perPage, + sort_by: sortBy, + order_by: orderBy, + }; + if (status) params.status = status; if (search || filterTags.length > 0) - query.filter = buildFilter(search, filterTags); - query.sort_by = sortBy; - query.order_by = orderBy; - - const options = { query }; + params.filter = buildFilter(search, filterTags); - const result = useQuery>({ - queryKey: getDevicesQueryKey(options), - queryFn: paginatedQueryFn(getDevicesSdk, options), - enabled, - }); + const result = useGetDevices(params, { query: { enabled } }); const devices = useMemo( - () => result.data?.data.map(normalizeDeviceTags) ?? [], + () => (result.data ?? []).map(normalizeDeviceTags), [result.data], ); return { devices, - totalCount: result.data?.totalCount ?? 0, + totalCount: totalCount(result.data), isLoading: result.isLoading, error: result.error, refetch: result.refetch, diff --git a/ui/apps/console/src/hooks/useFirewallRuleMutations.ts b/ui/apps/console/src/hooks/useFirewallRuleMutations.ts deleted file mode 100644 index a8f143023d3..00000000000 --- a/ui/apps/console/src/hooks/useFirewallRuleMutations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createFirewallRuleMutation, - updateFirewallRuleMutation, - deleteFirewallRuleMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a firewall rule, refreshing the list. - */ -export function useCreateFirewallRule() { - const invalidate = useInvalidateByIds("getFirewallRules"); - return useMutation({ - ...createFirewallRuleMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates a firewall rule, refreshing the list. - */ -export function useUpdateFirewallRule() { - const invalidate = useInvalidateByIds("getFirewallRules"); - return useMutation({ - ...updateFirewallRuleMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a firewall rule, refreshing the list. - */ -export function useDeleteFirewallRule() { - const invalidate = useInvalidateByIds("getFirewallRules"); - return useMutation({ - ...deleteFirewallRuleMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useFirewallRules.ts b/ui/apps/console/src/hooks/useFirewallRules.ts deleted file mode 100644 index d49ad048242..00000000000 --- a/ui/apps/console/src/hooks/useFirewallRules.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getFirewallRules as getFirewallRulesSdk, - getFirewallRulesQueryKey, - type GetFirewallRulesData, - type FirewallRulesResponse, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; - -interface UseFirewallRulesParams { - page?: number; - perPage?: number; -} - -/** - * A page of the namespace's firewall rules, in priority order. - */ -export function useFirewallRules({ - page = 1, - perPage = 10, -}: UseFirewallRulesParams = {}) { - const options = { - query: { page, per_page: perPage } satisfies GetFirewallRulesData["query"], - }; - - const result = useQuery>({ - queryKey: getFirewallRulesQueryKey(options), - queryFn: paginatedQueryFn(getFirewallRulesSdk, options), - }); - - const rules = useMemo(() => result.data?.data ?? [], [result.data]); - - return { - rules, - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useInstallKeyEvents.ts b/ui/apps/console/src/hooks/useInstallKeyEvents.ts deleted file mode 100644 index 9bd41b2da01..00000000000 --- a/ui/apps/console/src/hooks/useInstallKeyEvents.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - installKeyHistory, - installKeyHistoryQueryKey, - type InstallKeyHistoryData, - type InstallKeyEvent, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; - -interface UseInstallKeyEventsParams { - id: string | null; - page?: number; - perPage?: number; -} - -/** - * A page of the enrolments made with an install key — what it was used for, and when. - */ -export function useInstallKeyEvents({ - id, - page = 1, - perPage = 15, -}: UseInstallKeyEventsParams) { - const options = { - path: { id: id ?? "" }, - query: { page, per_page: perPage, sort_by: "created_at", order_by: "desc" }, - } satisfies { - path: InstallKeyHistoryData["path"]; - query: InstallKeyHistoryData["query"]; - }; - - const result = useQuery>({ - queryKey: installKeyHistoryQueryKey(options), - queryFn: paginatedQueryFn(installKeyHistory, options), - enabled: !!id, - }); - - return { - events: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useInstallKeyMutations.ts b/ui/apps/console/src/hooks/useInstallKeyMutations.ts deleted file mode 100644 index 58712977f94..00000000000 --- a/ui/apps/console/src/hooks/useInstallKeyMutations.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - installKeyCreateMutation, - installKeyUpdateMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates an install key. The response carries the only copy of the key, so it has to be shown - * before the mutation's data is discarded. - */ -export function useCreateInstallKey() { - const invalidate = useInvalidateByIds("installKeyList"); - return useMutation({ - ...installKeyCreateMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates an install key's name or limits. The key itself is not re-issued. - */ -export function useUpdateInstallKey() { - const invalidate = useInvalidateByIds("installKeyList"); - return useMutation({ - ...installKeyUpdateMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useInstallKeys.ts b/ui/apps/console/src/hooks/useInstallKeys.ts deleted file mode 100644 index 2d35ab13fad..00000000000 --- a/ui/apps/console/src/hooks/useInstallKeys.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - installKeyList, - installKeyListQueryKey, - type InstallKeyListData, - type InstallKey, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; - -interface UseInstallKeysParams { - page?: number; - perPage?: number; - sortBy?: string; - orderBy?: "asc" | "desc"; -} - -/** - * A page of the namespace's install keys, newest first. - */ -export function useInstallKeys({ - page = 1, - perPage = 10, - sortBy = "created_at", - orderBy = "desc", -}: UseInstallKeysParams = {}) { - const options = { - query: { page, per_page: perPage, sort_by: sortBy, order_by: orderBy }, - } satisfies { query: InstallKeyListData["query"] }; - - const result = useQuery>({ - queryKey: installKeyListQueryKey(options), - queryFn: paginatedQueryFn(installKeyList, options), - }); - - return { - installKeys: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useInstanceApiKeyMutations.ts b/ui/apps/console/src/hooks/useInstanceApiKeyMutations.ts deleted file mode 100644 index 790e70b8128..00000000000 --- a/ui/apps/console/src/hooks/useInstanceApiKeyMutations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createInstanceApiKeyMutation, - deleteInstanceApiKeyMutation, -} from "@/client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates an instance API key, refreshing the list. The response carries the only copy of the - * secret the caller will ever see, so it has to be shown before the mutation's data is discarded. - */ -export function useCreateInstanceApiKey() { - const invalidate = useInvalidateByIds("listInstanceApiKeys"); - - return useMutation({ - ...createInstanceApiKeyMutation(), - onSuccess: invalidate, - }); -} - -/** - * Revokes an instance API key, refreshing the list. Anything still authenticating with it starts - * failing at once. - */ -export function useDeleteInstanceApiKey() { - const invalidate = useInvalidateByIds("listInstanceApiKeys"); - - return useMutation({ - ...deleteInstanceApiKeyMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useInstanceApiKeys.ts b/ui/apps/console/src/hooks/useInstanceApiKeys.ts deleted file mode 100644 index 81ee099d4c6..00000000000 --- a/ui/apps/console/src/hooks/useInstanceApiKeys.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - listInstanceApiKeys, - listInstanceApiKeysQueryKey, - type ListInstanceApiKeysData, - type InstanceApiKey, -} from "@/client"; -import { paginatedQueryFn, type PaginatedResult } from "@/api/pagination"; - -interface UseInstanceApiKeysParams { - page?: number; - perPage?: number; - orderBy?: "asc" | "desc"; -} - -/** - * A page of the instance's admin API keys, newest first. - */ -export function useInstanceApiKeys({ - page = 1, - perPage = 10, - orderBy = "desc", -}: UseInstanceApiKeysParams = {}) { - const options = { - query: { page, per_page: perPage, order_by: orderBy }, - } satisfies { query: ListInstanceApiKeysData["query"] }; - - const result = useQuery>({ - queryKey: listInstanceApiKeysQueryKey(options), - queryFn: paginatedQueryFn(listInstanceApiKeys, options), - }); - - return { - apiKeys: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useInvalidateQueries.ts b/ui/apps/console/src/hooks/useInvalidateQueries.ts index 212532fd96d..d403cf08baa 100644 --- a/ui/apps/console/src/hooks/useInvalidateQueries.ts +++ b/ui/apps/console/src/hooks/useInvalidateQueries.ts @@ -1,23 +1,20 @@ import { useQueryClient } from "@tanstack/react-query"; /** - * Builds an invalidator for whole query families, matched on the first element of the key rather - * than the whole key. A mutation cannot know which page or filter is cached, so it invalidates - * by operation id and lets React Query refetch whichever are mounted. + * Builds an invalidator that matches orval-generated query keys by URL path prefix. Orval keys + * have the endpoint URL as the first array element, so `/api/devices` matches both the list + * (`/api/devices`) and a detail (`/api/devices/uid-123`). */ -export function useInvalidateByIds(...ids: string[]) { +export function useInvalidateByIds(...pathPrefixes: string[]) { const queryClient = useQueryClient(); - const idSet = new Set(ids); - return () => queryClient.invalidateQueries({ - predicate: (query) => { - const key = query.queryKey[0]; - return ( - typeof key === "object" - && key !== null - && "_id" in key - && typeof key._id === "string" - && idSet.has(key._id) - ); - }, - }); + return () => + queryClient.invalidateQueries({ + predicate: (query) => { + const head = query.queryKey[0]; + return ( + typeof head === "string" && + pathPrefixes.some((p) => head.startsWith(p)) + ); + }, + }); } diff --git a/ui/apps/console/src/hooks/useInvitationMutations.ts b/ui/apps/console/src/hooks/useInvitationMutations.ts deleted file mode 100644 index 8e202ae4086..00000000000 --- a/ui/apps/console/src/hooks/useInvitationMutations.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - acceptInviteMutation, - generateInvitationLinkMutation, - cancelMembershipInvitationMutation, -} from "@/client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Accepts an invitation. The user gains a namespace, so the namespace queries are refreshed - * along with the invitation list. - */ -export function useAcceptInvite() { - const invalidate = useInvalidateByIds( - "getMembershipInvitationList", - "getNamespace", - "getNamespaces", - ); - return useMutation({ - ...acceptInviteMutation(), - onSuccess: invalidate, - }); -} - -/** - * Mints an invitation link for a namespace. Refreshes the member list too, since a pending - * invitation is shown there alongside the members. - */ -export function useGenerateInvitationLink() { - const invalidate = useInvalidateByIds( - "getNamespaceMembershipInvitationList", - "listNamespaceMembers", - "getNamespace", - "getNamespaces", - ); - return useMutation({ - ...generateInvitationLinkMutation(), - onSuccess: invalidate, - }); -} - -/** - * Cancels a pending invitation, which makes its link stop working. - */ -export function useCancelMembershipInvitation() { - const invalidate = useInvalidateByIds("getNamespaceMembershipInvitationList"); - return useMutation({ - ...cancelMembershipInvitationMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useInvitations.ts b/ui/apps/console/src/hooks/useInvitations.ts deleted file mode 100644 index 5503755bd06..00000000000 --- a/ui/apps/console/src/hooks/useInvitations.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getNamespaceMembershipInvitationList, - getNamespaceMembershipInvitationListQueryKey, - resolveInvitationOptions, - type GetNamespaceMembershipInvitationListData, - type MembershipInvitation, -} from "@/client"; -import { paginatedQueryFn, type PaginatedResult } from "@/api/pagination"; -import { - invitationStatusFilter, - type InvitationStatus, -} from "@/utils/invitations"; - -interface UseNamespaceInvitationsParams { - tenantId: string; - status?: InvitationStatus; - page?: number; - perPage?: number; - enabled?: boolean; -} - -/** - * Resolves an invitation token to what it offers. Never retried and never stale: the token is - * single-use, so a retry would fail and a cached answer would be wrong. - */ -export function useResolveInvitation(invite: string) { - const { data, isLoading, isError } = useQuery({ - ...resolveInvitationOptions({ query: { invite } }), - enabled: !!invite, - retry: false, - staleTime: Infinity, - }); - - const resolved = - data?.tenant_id && data.user_id && data.status - ? { - tenantId: data.tenant_id, - userId: data.user_id, - email: data.email ?? "", - status: data.status, - } - : null; - - return { resolved, isLoading, isError }; -} - -/** - * A page of a namespace's invitations, filtered by status. - */ -export function useNamespaceInvitations({ - tenantId, - status = "pending", - page = 1, - perPage = 10, - enabled = true, -}: UseNamespaceInvitationsParams) { - const options = { - path: { tenant: tenantId }, - query: { - filter: invitationStatusFilter(status), - page, - per_page: perPage, - }, - } satisfies { - path: GetNamespaceMembershipInvitationListData["path"]; - query: GetNamespaceMembershipInvitationListData["query"]; - }; - - const result = useQuery>({ - queryKey: getNamespaceMembershipInvitationListQueryKey(options), - queryFn: paginatedQueryFn(getNamespaceMembershipInvitationList, options), - enabled: enabled && !!tenantId, - }); - - return { - invitations: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useLatestAnnouncement.ts b/ui/apps/console/src/hooks/useLatestAnnouncement.ts deleted file mode 100644 index b9a60cbca67..00000000000 --- a/ui/apps/console/src/hooks/useLatestAnnouncement.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getConfig } from "@/env"; -import { - listAnnouncementsOptions, - getAnnouncementOptions, -} from "@/client"; - -/** - * The most recent announcement, or nothing when announcements are switched off for the instance. - */ -export function useLatestAnnouncement() { - const enabled = getConfig().announcements; - - const listResult = useQuery({ - ...listAnnouncementsOptions({ - query: { page: 1, per_page: 1, order_by: "desc" }, - }), - enabled, - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - retry: false, - }); - - const latestUuid = listResult.data?.[0]?.uuid; - - const detailResult = useQuery({ - ...getAnnouncementOptions({ path: { uuid: latestUuid ?? "" } }), - enabled: enabled && !!latestUuid, - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - retry: false, - }); - - return { - announcement: detailResult.data ?? null, - isLoading: - listResult.isLoading || (!!latestUuid && detailResult.isLoading), - }; -} diff --git a/ui/apps/console/src/hooks/useLoginAsUser.ts b/ui/apps/console/src/hooks/useLoginAsUser.ts index d46f1d5396f..51a0bdc82f6 100644 --- a/ui/apps/console/src/hooks/useLoginAsUser.ts +++ b/ui/apps/console/src/hooks/useLoginAsUser.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useRef } from "react"; -import { getUserTokenAdmin } from "../client"; +import { getUserTokenAdmin } from "@/client/api"; /** * Signs an admin in as another user. Tracks the id in flight so a row can show its own spinner, @@ -16,11 +16,8 @@ export function useLoginAsUser() { setLoadingId(userId); setErrorId(null); try { - const { data } = await getUserTokenAdmin({ - path: { id: userId }, - throwOnError: true, - }); - if (data?.token) { + const data = await getUserTokenAdmin(userId); + if (data.token) { window.open( `/login?token=${encodeURIComponent(data.token)}`, "_blank", diff --git a/ui/apps/console/src/hooks/useMemberMutations.ts b/ui/apps/console/src/hooks/useMemberMutations.ts deleted file mode 100644 index 5b7a2fc29c1..00000000000 --- a/ui/apps/console/src/hooks/useMemberMutations.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - addNamespaceMemberMutation, - approveUserMutation, - removeNamespaceMemberMutation, - updateNamespaceMemberMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Adds a member to the namespace, refreshing the member list and the namespace itself. - */ -export function useAddMember() { - const invalidate = useInvalidateByIds( - "getNamespaces", - "getNamespace", - "listNamespaceMembers", - ); - return useMutation({ - ...addNamespaceMemberMutation(), - onSuccess: invalidate, - }); -} - -/** - * Changes a member's role. The namespace queries are refreshed because the caller's own - * permissions may be what changed. - */ -export function useUpdateMemberRole() { - const invalidate = useInvalidateByIds( - "getNamespaces", - "getNamespace", - "listNamespaceMembers", - ); - return useMutation({ - ...updateNamespaceMemberMutation(), - onSuccess: invalidate, - }); -} - -/** - * Removes a member from the namespace. - */ -export function useRemoveMember() { - const invalidate = useInvalidateByIds( - "getNamespaces", - "getNamespace", - "listNamespaceMembers", - ); - return useMutation({ - ...removeNamespaceMemberMutation(), - onSuccess: invalidate, - }); -} - -/** - * Approves an account that a non-superadmin provisioned, clearing awaiting_approval. - * Instance-admin only — the API gates on it. The account still has to be activated before it - * can sign in; this only removes the block, which is why the member list is refreshed rather - * than the account being treated as live. - */ -export function useApproveMember() { - const invalidate = useInvalidateByIds( - "getNamespaces", - "getNamespace", - "listNamespaceMembers", - ); - return useMutation({ - ...approveUserMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useNamespaceMutations.ts b/ui/apps/console/src/hooks/useNamespaceMutations.ts index 950647a2f04..ea7ac4aafff 100644 --- a/ui/apps/console/src/hooks/useNamespaceMutations.ts +++ b/ui/apps/console/src/hooks/useNamespaceMutations.ts @@ -1,43 +1,12 @@ import { useMutation } from "@tanstack/react-query"; import { - editNamespaceMutation, - setSshAccessModeMutation, getNamespaceToken, createNamespace as createNamespaceSdk, - deleteNamespace as deleteNamespaceSdk, - leaveNamespace as leaveNamespaceSdk, -} from "../client"; +} from "@/client/api"; import { useAuthStore } from "../stores/authStore"; import { consumePendingDeviceCode } from "@/utils/navigation"; -import { useInvalidateByIds } from "./useInvalidateQueries"; -/** - * Edits the namespace's settings. - */ -export function useEditNamespace() { - const invalidate = useInvalidateByIds("getNamespaces", "getNamespace"); - return useMutation({ - ...editNamespaceMutation(), - onSuccess: invalidate, - }); -} - -/** - * Sets how SSH access is granted in the namespace. It changes who can reach every device at - * once, so the namespace queries are refreshed with it. - */ -export function useSetSshAccessMode() { - const invalidate = useInvalidateByIds("getNamespaces", "getNamespace"); - return useMutation({ - ...setSshAccessModeMutation(), - onSuccess: invalidate, - }); -} - -/** - * Switches the active namespace, which re-issues the token and lands on redirectTo. Everything - * cached belongs to the previous namespace, so this is a navigation rather than a refetch. - */ +/** Exchanges a tenant token and redirects, updating the auth store. */ export function useSwitchNamespace() { return useMutation({ mutationFn: async ({ @@ -47,76 +16,32 @@ export function useSwitchNamespace() { tenantId: string; redirectTo?: string; }) => { - const { data } = await getNamespaceToken({ - path: { tenant: tenantId }, - throwOnError: true, - }); + const auth = await getNamespaceToken(tenantId); window.location.href = redirectTo ?? "/dashboard"; useAuthStore.getState().setSession({ - token: data.token, + token: auth.token, tenant: tenantId, - role: data.role, + role: auth.role, }); }, }); } -/** - * Creates a namespace and switches into it, so the user ends up inside what they just made. - */ +/** Creates a namespace, exchanges its token, and redirects to the dashboard. */ export function useCreateNamespace() { return useMutation({ mutationFn: async (name: string) => { - const { data: ns } = await createNamespaceSdk({ - body: { name }, - throwOnError: true, - }); - const { data } = await getNamespaceToken({ - path: { tenant: ns.tenant_id }, - throwOnError: true, - }); + const { tenant_id: tenant } = await createNamespaceSdk({ name }); + const auth = await getNamespaceToken(tenant); const pendingCode = consumePendingDeviceCode(); window.location.href = pendingCode ? `/accept-device?code=${encodeURIComponent(pendingCode)}` : "/dashboard"; useAuthStore.getState().setSession({ - token: data.token, - tenant: ns.tenant_id, - role: data.role, - }); - }, - }); -} - -/** - * Deletes a namespace along with everything in it. Irreversible. - */ -export function useDeleteNamespace() { - return useMutation({ - mutationFn: async (tenantId: string) => { - await deleteNamespaceSdk({ - path: { tenant: tenantId }, - throwOnError: true, - }); - useAuthStore.getState().logout(); - window.location.replace("/login"); - }, - }); -} - -/** - * Leaves a namespace. Unlike deleting, the namespace survives — this only removes the caller, - * and an owner cannot be the one to go. - */ -export function useLeaveNamespace() { - return useMutation({ - mutationFn: async (tenantId: string) => { - await leaveNamespaceSdk({ - path: { tenant: tenantId }, - throwOnError: true, + token: auth.token, + tenant, + role: auth.role, }); - useAuthStore.getState().logout(); - window.location.replace("/login"); }, }); } diff --git a/ui/apps/console/src/hooks/useNamespaces.ts b/ui/apps/console/src/hooks/useNamespaces.ts index 906951bb38e..97e553ce509 100644 --- a/ui/apps/console/src/hooks/useNamespaces.ts +++ b/ui/apps/console/src/hooks/useNamespaces.ts @@ -1,14 +1,8 @@ -import { useEffect } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - type Namespace as GeneratedNamespace, - type NamespaceMemberRole, - getNamespacesOptions, - getNamespaceOptions, - getNamespaceTokenOptions, - listNamespaceMembersOptions, -} from "../client"; -import { useAuthStore } from "../stores/authStore"; +import { useGetNamespaces, useGetNamespace } from "@/client/api"; +import type { + Namespace as GeneratedNamespace, + NamespaceMemberRole, +} from "@/client/model"; /** * A namespace as the console uses it: the generated model plus the type the cloud API adds and @@ -35,78 +29,26 @@ export interface NamespaceMember { * them all, and nobody is in more. */ export function useNamespaces() { - const result = useQuery({ - ...getNamespacesOptions({ query: { page: 1, per_page: 100 } }), - }); - - return { - namespaces: (result.data ?? []) as Namespace[], - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * Fetches a fresh namespace token on every cold start so that - * authStore.role is always populated from the server — covering first - * login (where role is null) and stale-localStorage scenarios. - * - * Should be called once at the app's authenticated boundary - * (NamespaceGuard). - */ -export function useInitRole() { - const tenant = useAuthStore((s) => s.tenant); - - const { data } = useQuery({ - ...getNamespaceTokenOptions({ path: { tenant: tenant ?? "" } }), - enabled: !!tenant, - }); - - useEffect(() => { - if (!data || !tenant) return; - useAuthStore - .getState() - .setSession({ token: data.token, tenant, role: data.role }); - }, [data, tenant]); + const { + data: namespaces = [], + isLoading, + error, + refetch, + } = useGetNamespaces({ page: 1, per_page: 100 }); + + return { namespaces, isLoading, error, refetch }; } /** * One namespace by tenant id. Idle until an id is given. */ export function useNamespace(tenantId: string) { - const result = useQuery({ - ...getNamespaceOptions({ path: { tenant: tenantId } }), - enabled: !!tenantId, - }); - - return { - namespace: result.data ?? null, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} - -/** - * Lists a namespace's members with their full identity (name, username, email) - * and a flattened account status. Backs the members table; the (cloud/enterprise) - * pending invitations are fetched separately and merged in the component. - * Member lists are small, so a single large page is fetched (no pagination UI). - */ -export function useNamespaceMembers(tenantId: string) { - const result = useQuery({ - ...listNamespaceMembersOptions({ - path: { tenant: tenantId }, - query: { page: 1, per_page: 100 }, - }), - enabled: !!tenantId, - }); - - return { - members: result.data ?? [], - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; + const { + data: namespace = null, + isLoading, + error, + refetch, + } = useGetNamespace(tenantId, { query: { enabled: !!tenantId } }); + + return { namespace, isLoading, error, refetch }; } diff --git a/ui/apps/console/src/hooks/usePublicKeyMutations.ts b/ui/apps/console/src/hooks/usePublicKeyMutations.ts deleted file mode 100644 index 48beb959b31..00000000000 --- a/ui/apps/console/src/hooks/usePublicKeyMutations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createPublicKeyMutation, - updatePublicKeyMutation, - deletePublicKeyMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a public key, refreshing the list. - */ -export function useCreatePublicKey() { - const invalidate = useInvalidateByIds("getPublicKeys"); - return useMutation({ - ...createPublicKeyMutation(), - onSuccess: invalidate, - }); -} - -/** - * Updates a public key, refreshing the list. - */ -export function useUpdatePublicKey() { - const invalidate = useInvalidateByIds("getPublicKeys"); - return useMutation({ - ...updatePublicKeyMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a public key, refreshing the list. Anything authenticating with it stops working. - */ -export function useDeletePublicKey() { - const invalidate = useInvalidateByIds("getPublicKeys"); - return useMutation({ - ...deletePublicKeyMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/usePublicKeys.ts b/ui/apps/console/src/hooks/usePublicKeys.ts deleted file mode 100644 index 73971ba2cfb..00000000000 --- a/ui/apps/console/src/hooks/usePublicKeys.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { - getPublicKeys as getPublicKeysSdk, - getPublicKeysQueryKey, - type GetPublicKeysData, - type PublicKeyResponse, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { toBase64Json } from "@/utils/encoding"; - -/** - * Builds the public-key list filter: a search matched against the name or the fingerprint, so - * either half of what a user remembers finds the key. - */ -export function buildPublicKeyFilter(search: string): string { - const filters = [ - { type: "operator", params: { name: "or" } }, - { - type: "property", - params: { name: "name", operator: "contains", value: search }, - }, - { type: "operator", params: { name: "or" } }, - { - type: "property", - params: { name: "fingerprint", operator: "contains", value: search }, - }, - ]; - return toBase64Json(filters); -} - -interface UsePublicKeysParams { - page?: number; - perPage?: number; - search?: string; -} - -/** - * A page of the namespace's public keys. - */ -export function usePublicKeys({ - page = 1, - perPage = 10, - search = "", -}: UsePublicKeysParams = {}) { - const query: GetPublicKeysData["query"] = { page, per_page: perPage }; - if (search) query.filter = buildPublicKeyFilter(search); - - const options = { query }; - - const result = useQuery>({ - queryKey: getPublicKeysQueryKey(options), - queryFn: paginatedQueryFn(getPublicKeysSdk, options), - }); - - const publicKeys = useMemo(() => result.data?.data ?? [], [result.data]); - - return { - publicKeys, - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useRevealInstallKey.ts b/ui/apps/console/src/hooks/useRevealInstallKey.ts deleted file mode 100644 index 726d940430a..00000000000 --- a/ui/apps/console/src/hooks/useRevealInstallKey.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { installKeyRevealOptions } from "../client"; - -/** - * Reveal a install key's plaintext on demand. The secret is never preloaded for - * the list rows, and not even fetched when the dialog opens: the query only - * fires once a key is targeted (`name` set) AND the user opts in (`enabled`), so - * the plaintext is decrypted only on an explicit click. The result is dropped - * from cache as soon as the dialog closes so the decrypted value doesn't linger. - */ -export function useRevealInstallKey(name: string | null, enabled = true) { - const result = useQuery({ - ...installKeyRevealOptions({ path: { key: name ?? "" } }), - enabled: !!name && enabled, - gcTime: 0, - }); - - return { - key: result.data?.key ?? "", - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useSSHApproval.ts b/ui/apps/console/src/hooks/useSSHApproval.ts index 33a86d6f44c..3911147a09f 100644 --- a/ui/apps/console/src/hooks/useSSHApproval.ts +++ b/ui/apps/console/src/hooks/useSSHApproval.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { getSshApproval } from "@/client"; -import { isSdkError } from "@/api/errors"; +import { useEffect, useRef, useState } from "react"; import { - useConfirmSSHApproval, - useRejectSSHApproval, -} from "./useSSHIdentityMutations"; + getSshApproval, + useConfirmSshApproval, + useRejectSshApproval, +} from "@/client/api"; +import { isSdkError } from "@/api/errors"; /** * Drives the SSH login approval modal: fetch the request the gateway is holding @@ -57,8 +57,8 @@ export function useSSHApproval(code: string) { const [deciding, setDeciding] = useState(false); const [actionError, setActionError] = useState(""); const expiresAtRef = useRef(0); - const confirmMutation = useConfirmSSHApproval(); - const rejectMutation = useRejectSSHApproval(); + const confirmMutation = useConfirmSshApproval(); + const rejectMutation = useRejectSshApproval(); useEffect(() => { let cancelled = false; @@ -71,10 +71,7 @@ export function useSSHApproval(code: string) { setPhase("loading"); try { - const { data } = await getSshApproval({ - path: { code }, - throwOnError: true, - }); + const data = await getSshApproval(code); if (cancelled) return; setDetails({ @@ -132,35 +129,32 @@ export function useSSHApproval(code: string) { return () => window.clearInterval(id); }, [phase]); - const decide = useCallback( - async (decision: "confirm" | "reject") => { - if (!code || deciding) return false; - setDeciding(true); - setActionError(""); - try { - const mutation = - decision === "confirm" ? confirmMutation : rejectMutation; - await mutation.mutateAsync({ path: { code } }); - setPhase(decision === "confirm" ? "confirmed" : "rejected"); - - return true; - } catch (err) { - if (isSdkError(err) && err.status === 404) { - setPhase("expired"); - return false; - } - setActionError("Something went wrong. Please try again."); - + const decide = async (decision: "confirm" | "reject") => { + if (!code || deciding) return false; + setDeciding(true); + setActionError(""); + try { + const mutation = + decision === "confirm" ? confirmMutation : rejectMutation; + await mutation.mutateAsync({ code }); + setPhase(decision === "confirm" ? "confirmed" : "rejected"); + + return true; + } catch (err) { + if (isSdkError(err) && err.status === 404) { + setPhase("expired"); return false; - } finally { - setDeciding(false); } - }, - [code, deciding, confirmMutation, rejectMutation], - ); + setActionError("Something went wrong. Please try again."); + + return false; + } finally { + setDeciding(false); + } + }; - const confirm = useCallback(() => decide("confirm"), [decide]); - const reject = useCallback(() => decide("reject"), [decide]); + const confirm = () => decide("confirm"); + const reject = () => decide("reject"); return { phase, @@ -169,7 +163,7 @@ export function useSSHApproval(code: string) { totalSeconds, confirm, reject, - markConfirmed: useCallback(() => setPhase("confirmed"), []), + markConfirmed: () => setPhase("confirmed"), deciding, actionError, }; diff --git a/ui/apps/console/src/hooks/useSSHIdentities.ts b/ui/apps/console/src/hooks/useSSHIdentities.ts deleted file mode 100644 index 1b0aed7f22c..00000000000 --- a/ui/apps/console/src/hooks/useSSHIdentities.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { listSshIdentitiesOptions, type SshIdentity } from "../client"; - -/** - * The SSH identities. By default the caller's own; all includes every identity in the namespace, - * which needs the permission to see them. - */ -export function useSSHIdentities(all = false) { - const options = all ? { query: { all: true } } : {}; - const result = useQuery(listSshIdentitiesOptions(options)); - - const identities = useMemo( - () => result.data ?? [], - [result.data], - ); - - return { - identities, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useSSHIdentityMutations.ts b/ui/apps/console/src/hooks/useSSHIdentityMutations.ts deleted file mode 100644 index ec191771ce7..00000000000 --- a/ui/apps/console/src/hooks/useSSHIdentityMutations.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - confirmSshApprovalMutation, - rejectSshApprovalMutation, - createSshIdentityMutation, - renameSshIdentityMutation, - deleteSshIdentityMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Approves a pending SSH login. The identity list is refreshed, since approving may enrol one. - */ -export function useConfirmSSHApproval() { - const invalidate = useInvalidateByIds("listSshIdentities"); - return useMutation({ - ...confirmSshApprovalMutation(), - onSuccess: invalidate, - }); -} - -/** - * Rejects a pending SSH login. Nothing is enrolled, so nothing is invalidated. - */ -export function useRejectSSHApproval() { - return useMutation({ - ...rejectSshApprovalMutation(), - }); -} - -/** - * Enrols an SSH identity, refreshing the list. - */ -export function useCreateSSHIdentity() { - const invalidate = useInvalidateByIds("listSshIdentities"); - return useMutation({ - ...createSshIdentityMutation(), - onSuccess: invalidate, - }); -} - -/** - * Renames an SSH identity. The key is untouched — only its label changes. - */ -export function useRenameSSHIdentity() { - const invalidate = useInvalidateByIds("listSshIdentities"); - return useMutation({ - ...renameSshIdentityMutation(), - onSuccess: invalidate, - }); -} - -/** - * Revokes an SSH identity. Anything signing with it stops being able to connect. - */ -export function useDeleteSSHIdentity() { - const invalidate = useInvalidateByIds("listSshIdentities"); - return useMutation({ - ...deleteSshIdentityMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useServiceAccountMutations.ts b/ui/apps/console/src/hooks/useServiceAccountMutations.ts deleted file mode 100644 index c0a9566e1ed..00000000000 --- a/ui/apps/console/src/hooks/useServiceAccountMutations.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createServiceAccountMutation, - deleteServiceAccountMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a service account. It brings an SSH identity with it, so both lists are refreshed. - */ -export function useCreateServiceAccount() { - const invalidate = useInvalidateByIds( - "listServiceAccounts", - "listSshIdentities", - ); - return useMutation({ - ...createServiceAccountMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a service account and the identity that belongs to it. - */ -export function useDeleteServiceAccount() { - const invalidate = useInvalidateByIds( - "listServiceAccounts", - "listSshIdentities", - ); - return useMutation({ - ...deleteServiceAccountMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useServiceAccounts.ts b/ui/apps/console/src/hooks/useServiceAccounts.ts deleted file mode 100644 index 6c4c384789f..00000000000 --- a/ui/apps/console/src/hooks/useServiceAccounts.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { listServiceAccountsOptions, type ServiceAccount } from "../client"; - -/** - * The namespace's service accounts. Returns an empty array while loading, so a caller can map - * over it without a guard. - */ -export function useServiceAccounts() { - const result = useQuery(listServiceAccountsOptions()); - - const serviceAccounts = useMemo( - () => result.data ?? [], - [result.data], - ); - - return { - serviceAccounts, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/hooks/useSession.ts b/ui/apps/console/src/hooks/useSession.ts deleted file mode 100644 index 376454ad7e3..00000000000 --- a/ui/apps/console/src/hooks/useSession.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getSessionOptions } from "../client"; - -/** - * One session by UID. Idle until a UID is given. - */ -export function useSession(uid: string) { - const result = useQuery({ - ...getSessionOptions({ path: { uid } }), - enabled: !!uid, - }); - - return { - session: result.data ?? null, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useSessionMutations.ts b/ui/apps/console/src/hooks/useSessionMutations.ts deleted file mode 100644 index fcd91c0a874..00000000000 --- a/ui/apps/console/src/hooks/useSessionMutations.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { clsoeSessionMutation, deleteSessionRecord } from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Closes an open session. The device's status changes with it, so the device counts are - * refreshed alongside the session queries. - */ -export function useCloseSession() { - const invalidate = useInvalidateByIds("getSessions", "getSession", "getStatusDevices"); - return useMutation({ - ...clsoeSessionMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a session's recording, leaving the session itself. Seat zero is the only one the UI - * records, so that is the one removed. - */ -export function useDeleteSessionRecording() { - const invalidate = useInvalidateByIds("getSessions", "getSession"); - return useMutation({ - mutationFn: async (uid: string) => { - await deleteSessionRecord({ path: { uid, seat: 0 }, throwOnError: true }); - }, - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useSessionRecording.ts b/ui/apps/console/src/hooks/useSessionRecording.ts index 767bb9827f9..c3507e9ad3b 100644 --- a/ui/apps/console/src/hooks/useSessionRecording.ts +++ b/ui/apps/console/src/hooks/useSessionRecording.ts @@ -1,5 +1,5 @@ import { useState } from "react"; -import { getSessionRecord } from "@/client"; +import { getSessionRecord } from "@/client/api"; /** * Fetches a session recording on demand. Not a query: a recording is large and only wanted when @@ -14,14 +14,7 @@ export function useSessionRecording() { setIsLoading(true); setError(null); try { - const { data } = await getSessionRecord({ - path: { uid, seat: 0 }, - parseAs: "text", - throwOnError: true, - }); - const recording: unknown = data; - if (typeof recording !== "string") throw new Error("recording is not text"); - + const recording = await getSessionRecord(uid, 0); setLogs(recording); return true; } catch { diff --git a/ui/apps/console/src/hooks/useSessions.ts b/ui/apps/console/src/hooks/useSessions.ts deleted file mode 100644 index 4279fff4ebe..00000000000 --- a/ui/apps/console/src/hooks/useSessions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getSessions as getSessionsSdk, - getSessionsQueryKey, - type GetSessionsData, - type Session, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; - -interface UseSessionsParams { - page?: number; - perPage?: number; -} - -/** - * A page of the namespace's sessions, newest first. - */ -export function useSessions({ page = 1, perPage = 10 }: UseSessionsParams = {}) { - const options = { query: { page, per_page: perPage } } satisfies { query: GetSessionsData["query"] }; - - const result = useQuery>({ - queryKey: getSessionsQueryKey(options), - queryFn: paginatedQueryFn(getSessionsSdk, options), - }); - - return { - sessions: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useStats.ts b/ui/apps/console/src/hooks/useStats.ts deleted file mode 100644 index 1a6781226d1..00000000000 --- a/ui/apps/console/src/hooks/useStats.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getStatusDevicesOptions } from "../client"; - -/** - * The namespace's device counts, by status. Returns null while loading rather than zeroes, so a - * dashboard does not flash "0 devices" at someone who has some. - */ -export function useStats() { - const result = useQuery(getStatusDevicesOptions()); - - return { - stats: result.data ?? null, - isLoading: result.isLoading, - error: result.error, - refetch: result.refetch, - }; -} diff --git a/ui/apps/console/src/hooks/useSupportIdentifier.ts b/ui/apps/console/src/hooks/useSupportIdentifier.ts deleted file mode 100644 index 0762ec74466..00000000000 --- a/ui/apps/console/src/hooks/useSupportIdentifier.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getNamespaceSupportOptions } from "@/client"; - -/** - * The namespace's support identifier, used to attach a chat conversation to the account. Idle - * without a tenant, since the identifier is per namespace. - */ -export function useSupportIdentifier( - tenantId: string | null | undefined, - enabled: boolean, -) { - const result = useQuery({ - ...getNamespaceSupportOptions({ path: { tenant: tenantId ?? "" } }), - enabled: enabled && !!tenantId, - retry: 1, - }); - - return { - identifier: result.data?.identifier ?? null, - isLoading: result.isLoading, - isError: result.isError, - }; -} diff --git a/ui/apps/console/src/hooks/useTagMutations.ts b/ui/apps/console/src/hooks/useTagMutations.ts deleted file mode 100644 index f62111ddca3..00000000000 --- a/ui/apps/console/src/hooks/useTagMutations.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createTagMutation, - deleteTagMutation, - updateTagMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a tag. The device queries are refreshed too, because a device list may be filtered on - * tags that now include it. - */ -export function useCreateTag() { - const invalidate = useInvalidateByIds("getTags", "getDevices", "getDevice"); - return useMutation({ - ...createTagMutation(), - onSuccess: invalidate, - }); -} - -/** - * Renames a tag. Every device carrying it shows the new name, so the device queries go with it. - */ -export function useUpdateTag() { - const invalidate = useInvalidateByIds("getTags", "getDevices", "getDevice"); - return useMutation({ - ...updateTagMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a tag, removing it from every device that carried it. - */ -export function useDeleteTag() { - const invalidate = useInvalidateByIds("getTags", "getDevices", "getDevice"); - return useMutation({ - ...deleteTagMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useTags.ts b/ui/apps/console/src/hooks/useTags.ts index 19c27899289..97238b706e1 100644 --- a/ui/apps/console/src/hooks/useTags.ts +++ b/ui/apps/console/src/hooks/useTags.ts @@ -1,33 +1,7 @@ -import { useQuery } from "@tanstack/react-query"; -import { - getTags as getTagsSdk, - getTagsQueryKey, - type GetTagsData, - type Tag, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; +import { useGetTags } from "@/client/api"; -interface UseTagsParams { - page?: number; - perPage?: number; -} - -/** - * The namespace's tags. Fetched a hundred at a time, since they are used to populate filters - * rather than to be paged through. - */ -export function useTags({ page = 1, perPage = 100 }: UseTagsParams = {}) { - const options = { query: { page, per_page: perPage } } satisfies { query: GetTagsData["query"] }; - - const result = useQuery>({ - queryKey: getTagsQueryKey(options), - queryFn: paginatedQueryFn(getTagsSdk, options), - }); - - return { - tags: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; +/** All tag names in the namespace, with loading state for callers that need it. */ +export function useTagNames() { + const { data = [], isLoading } = useGetTags({ page: 1, per_page: 100 }); + return { names: data.map((t) => t.name), isLoading }; } diff --git a/ui/apps/console/src/hooks/useUploadLicense.ts b/ui/apps/console/src/hooks/useUploadLicense.ts deleted file mode 100644 index 2b8e373c540..00000000000 --- a/ui/apps/console/src/hooks/useUploadLicense.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { sendLicenseMutation } from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Uploads a licence file and refreshes the licence query, so the new terms take effect in the UI - * as soon as the server accepts them. - */ -export function useUploadLicense() { - const invalidate = useInvalidateByIds("getLicense"); - return useMutation({ - ...sendLicenseMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useWebEndpointMutations.ts b/ui/apps/console/src/hooks/useWebEndpointMutations.ts deleted file mode 100644 index 522c0063e7f..00000000000 --- a/ui/apps/console/src/hooks/useWebEndpointMutations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { - createWebEndpointMutation, - deleteWebEndpointMutation, -} from "../client"; -import { useInvalidateByIds } from "./useInvalidateQueries"; - -/** - * Creates a web endpoint, refreshing the list. - */ -export function useCreateWebEndpoint() { - const invalidate = useInvalidateByIds("listWebEndpoints"); - return useMutation({ - ...createWebEndpointMutation(), - onSuccess: invalidate, - }); -} - -/** - * Deletes a web endpoint, refreshing the list. The address stops resolving at once. - */ -export function useDeleteWebEndpoint() { - const invalidate = useInvalidateByIds("listWebEndpoints"); - return useMutation({ - ...deleteWebEndpointMutation(), - onSuccess: invalidate, - }); -} diff --git a/ui/apps/console/src/hooks/useWebEndpoints.ts b/ui/apps/console/src/hooks/useWebEndpoints.ts deleted file mode 100644 index 5a161930643..00000000000 --- a/ui/apps/console/src/hooks/useWebEndpoints.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { - listWebEndpoints as listWebEndpointsSdk, - listWebEndpointsQueryKey, - type ListWebEndpointsData, - type Webendpoint, -} from "../client"; -import { paginatedQueryFn, type PaginatedResult } from "../api/pagination"; -import { toBase64Json } from "@/utils/encoding"; - -interface UseWebEndpointsParams { - page?: number; - perPage?: number; - addressFilter?: string; -} - -function encodeAddressFilter(value: string): string { - const clauses = [ - { - type: "property", - params: { name: "address", operator: "contains", value }, - }, - ]; - return toBase64Json(clauses); -} - -/** - * A page of the namespace's web endpoints, optionally filtered by address. A blank filter is - * dropped rather than sent, so an empty search box does not narrow the list to nothing. - */ -export function useWebEndpoints({ - page = 1, - perPage = 10, - addressFilter, -}: UseWebEndpointsParams = {}) { - const trimmedFilter = addressFilter?.trim(); - const filter = trimmedFilter ? encodeAddressFilter(trimmedFilter) : undefined; - - const options = { - query: { - page, - per_page: perPage, - ...(filter ? { filter } : {}), - }, - } satisfies { query: ListWebEndpointsData["query"] }; - - const result = useQuery>({ - queryKey: listWebEndpointsQueryKey(options), - queryFn: paginatedQueryFn(listWebEndpointsSdk, options), - }); - - return { - webEndpoints: result.data?.data ?? [], - totalCount: result.data?.totalCount ?? 0, - isLoading: result.isLoading, - error: result.error, - }; -} diff --git a/ui/apps/console/src/stores/__tests__/authStore.test.ts b/ui/apps/console/src/stores/__tests__/authStore.test.ts index f58dc37f70e..5eda9976cef 100644 --- a/ui/apps/console/src/stores/__tests__/authStore.test.ts +++ b/ui/apps/console/src/stores/__tests__/authStore.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { useAuthStore } from "../authStore"; -import type { UserAuth } from "@/client"; +import type { UserAuth } from "@/client/model"; import { mockSdkResponse, type SdkResponse } from "@/tests/sdk"; import { mockUserAuth } from "@/tests/factories"; diff --git a/ui/apps/console/src/stores/authStore.ts b/ui/apps/console/src/stores/authStore.ts index 9facd207c9e..a9f74db2d25 100644 --- a/ui/apps/console/src/stores/authStore.ts +++ b/ui/apps/console/src/stores/authStore.ts @@ -2,14 +2,15 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import { type Role } from "../utils/permission"; import { - login as loginSdk, + getLoginUrl, getUserInfo, updateUser as updateUserSdk, deleteUser as deleteUserSdk, - authMfa, - mfaRecover, - type UserOrigin, -} from "../client"; + authMFA, + getMfaRecoverUrl, +} from "@/client/api"; +import type { UserAuth, UserOrigin } from "@/client/model"; +import { fetchWithHeaders } from "@/api/customInstance"; import { queryClient } from "../api/queryClient"; import { tearDownChatwoot } from "../hooks/chatwootRuntime"; import { useVaultStore } from "./vaultStore"; @@ -87,12 +88,12 @@ export const useAuthStore = create()( login: async (username: string, password: string) => { set({ loading: true, mfaToken: null }); try { - const { data, response } = await loginSdk({ - body: { username, password }, - throwOnError: true, - }); + const { data, headers } = await fetchWithHeaders( + getLoginUrl(), + { method: "POST", body: JSON.stringify({ username, password }) }, + ); - const mfaToken = response.headers.get("x-mfa-token"); + const mfaToken = headers.get("x-mfa-token"); if (mfaToken) { set({ @@ -130,7 +131,7 @@ export const useAuthStore = create()( loginWithToken: async (token: string) => { set({ loading: true, token }); try { - const { data } = await getUserInfo({ throwOnError: true }); + const data = await getUserInfo(); set({ user: data.user, userId: data.id, @@ -161,8 +162,7 @@ export const useAuthStore = create()( fetchUser: async () => { try { - const { data } = await getUserInfo({ throwOnError: true }); - const user = data; + const user = await getUserInfo(); set({ user: user.user, username: user.user, @@ -185,19 +185,19 @@ export const useAuthStore = create()( }, updateProfile: async (data) => { - await updateUserSdk({ body: data, throwOnError: true }); + await updateUserSdk(data); await get().fetchUser(); }, updatePassword: async (currentPassword, newPassword) => { await updateUserSdk({ - body: { current_password: currentPassword, password: newPassword }, - throwOnError: true, + current_password: currentPassword, + password: newPassword, }); }, deleteUser: async () => { - await deleteUserSdk({ throwOnError: true }); + await deleteUserSdk(); get().logout(); window.location.replace("/login"); }, @@ -210,10 +210,7 @@ export const useAuthStore = create()( set({ loading: true, error: null }); try { - const { data } = await authMfa({ - body: { token: mfaToken, code }, - throwOnError: true, - }); + const data = await authMFA({ token: mfaToken, code }); set({ token: data.token, user: data.user, @@ -241,13 +238,18 @@ export const useAuthStore = create()( set({ loading: true, error: null }); try { - const { data, response } = await mfaRecover({ - body: { identifier: username, recovery_code: code }, - throwOnError: true, - }); + const { data: userData, headers } = await fetchWithHeaders( + getMfaRecoverUrl(), + { + method: "POST", + body: JSON.stringify({ + identifier: username, + recovery_code: code, + }), + }, + ); - const userData = data; - const expiresAt = response.headers.get("x-expires-at") || ""; + const expiresAt = headers.get("x-expires-at") || ""; let expiryValue: number | null = null; if (expiresAt) { diff --git a/ui/apps/console/src/stores/connectivityStore.ts b/ui/apps/console/src/stores/connectivityStore.ts index b54af248ac1..09bbbda9072 100644 --- a/ui/apps/console/src/stores/connectivityStore.ts +++ b/ui/apps/console/src/stores/connectivityStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { getInfo } from "../client"; +import { getInfo } from "@/client/api"; interface ConnectivityState { apiReachable: boolean; @@ -18,7 +18,7 @@ function startPolling() { const poll = async () => { try { - await getInfo({ throwOnError: true }); + await getInfo(); useConnectivityStore.getState().markUp(); polling = false; } catch { @@ -40,7 +40,7 @@ export const useConnectivityStore = create()((set) => ({ checkInitial: async () => { try { - await getInfo({ throwOnError: true }); + await getInfo(); set({ apiReachable: true, initialCheckDone: true, diff --git a/ui/apps/console/src/stores/mfaResetStore.ts b/ui/apps/console/src/stores/mfaResetStore.ts index 053a89f93f8..3ef6fdb8388 100644 --- a/ui/apps/console/src/stores/mfaResetStore.ts +++ b/ui/apps/console/src/stores/mfaResetStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { requestResetMfa, resetMfa } from "../client"; +import { requestResetMFA, resetMFA } from "@/client/api"; import { useAuthStore } from "./authStore"; interface MfaResetState { @@ -32,10 +32,7 @@ export const useMfaResetStore = create()((set, get) => ({ requestMfaReset: async (identifier: string) => { set({ loading: true, error: null }); try { - const { data } = await requestResetMfa({ - body: { identifier }, - throwOnError: true, - }); + const data = await requestResetMFA({ identifier }); set({ mfaResetToken: data.token, mfaResetIdentifier: identifier, @@ -62,13 +59,9 @@ export const useMfaResetStore = create()((set, get) => ({ set({ loading: true, error: null }); try { - const { data } = await resetMfa({ - path: { "user-id": mfaResetToken }, - body: { - main_email_code: mainEmailCode, - recovery_email_code: recoveryEmailCode, - }, - throwOnError: true, + const data = await resetMFA(mfaResetToken, { + main_email_code: mainEmailCode, + recovery_email_code: recoveryEmailCode, }); useAuthStore.setState({ diff --git a/ui/apps/console/src/stores/signUpStore.ts b/ui/apps/console/src/stores/signUpStore.ts index 2b0cbf66242..818a423ccc6 100644 --- a/ui/apps/console/src/stores/signUpStore.ts +++ b/ui/apps/console/src/stores/signUpStore.ts @@ -4,7 +4,7 @@ import { registerUser, resendEmail as resendEmailSdk, getValidateAccount, -} from "../client"; +} from "@/client/api"; /** * Where an email verification stands. failed-token is separate from failed because an expired @@ -62,17 +62,13 @@ export const useSignUpStore = create()((set) => ({ signUp: async (payload) => { set({ signUpLoading: true, signUpError: null, signUpServerFields: [], signUpToken: null, signUpTenant: null }); try { - const { data } = await registerUser({ - body: payload, - throwOnError: true, - }); - const response = (data ?? {}) as { token?: string; tenant?: string }; + const response = await registerUser(payload); set({ signUpLoading: false, - signUpToken: response.token ?? null, - signUpTenant: response.tenant ?? null, + signUpToken: response?.token ?? null, + signUpTenant: response?.tenant ?? null, }); - return response.token ?? null; + return response?.token ?? null; } catch (error: unknown) { const fields = Object.keys(apiErrorFields(error)); if (fields.length > 0) { @@ -95,7 +91,7 @@ export const useSignUpStore = create()((set) => ({ resendEmail: async (username) => { set({ resendLoading: true, resendError: null }); try { - await resendEmailSdk({ body: { username }, throwOnError: true }); + await resendEmailSdk({ username }); set({ resendLoading: false }); return true; } catch { @@ -107,7 +103,7 @@ export const useSignUpStore = create()((set) => ({ validateAccount: async (email, token, signal) => { set({ validationStatus: "processing" }); try { - await getValidateAccount({ query: { email, token }, signal, throwOnError: true }); + await getValidateAccount({ email, token }, { signal }); set({ validationStatus: "success" }); } catch (error: unknown) { if (signal?.aborted) return; diff --git a/ui/apps/console/src/tests/factories.ts b/ui/apps/console/src/tests/factories.ts index 99cbb1e6eb8..f08cc04ca7e 100644 --- a/ui/apps/console/src/tests/factories.ts +++ b/ui/apps/console/src/tests/factories.ts @@ -5,8 +5,8 @@ import type { Customer, Device, FirewallRulesResponse, - GetLicenseResponse, - GetStatusDevicesResponse, + GetLicense200 as GetLicenseResponse, + GetStatusDevices200 as GetStatusDevicesResponse, InstallKey, MembershipInvitation, Namespace, @@ -18,7 +18,7 @@ import type { Tag, UserAuth, Webendpoint, -} from "@/client"; +} from "@/client/model"; /** * Builds a signed-in user for a test. Every field has a value, so a case names only what it is about diff --git a/ui/apps/console/src/tests/mockNamespaces.ts b/ui/apps/console/src/tests/mockNamespaces.ts index 3c1ba2461b1..ea1e242ee82 100644 --- a/ui/apps/console/src/tests/mockNamespaces.ts +++ b/ui/apps/console/src/tests/mockNamespaces.ts @@ -1,5 +1,5 @@ import { vi } from "vitest"; -import type { Namespace } from "@/client"; +import type { Namespace } from "@/client/model"; import { getNamespaces, getNamespace } from "@/client/sdk.gen"; import { paginatedResponse, mockSdkResponse } from "./sdk"; import { mockNamespace } from "./factories"; diff --git a/ui/apps/console/src/utils/__tests__/deviceTags.test.ts b/ui/apps/console/src/utils/__tests__/deviceTags.test.ts index 2b26da266be..576ad37d398 100644 --- a/ui/apps/console/src/utils/__tests__/deviceTags.test.ts +++ b/ui/apps/console/src/utils/__tests__/deviceTags.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import type { Device } from "@/client"; +import type { Device } from "@/client/model"; import { normalizeDeviceTags } from "@/utils/deviceTags"; function makeDevice(tags: unknown): Device { diff --git a/ui/apps/console/src/utils/__tests__/stats.test.ts b/ui/apps/console/src/utils/__tests__/stats.test.ts index 3f1c0a0b2b7..59043a986f0 100644 --- a/ui/apps/console/src/utils/__tests__/stats.test.ts +++ b/ui/apps/console/src/utils/__tests__/stats.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { hasAnyDevices } from "../stats"; -import type { GetStatusDevicesResponse } from "@/client"; +import type { GetStatusDevices200 as GetStatusDevicesResponse } from "@/client/model"; const emptyStats: GetStatusDevicesResponse = { registered_devices: 0, diff --git a/ui/apps/console/src/utils/billing.ts b/ui/apps/console/src/utils/billing.ts index 385ba00de28..86a178dafff 100644 --- a/ui/apps/console/src/utils/billing.ts +++ b/ui/apps/console/src/utils/billing.ts @@ -1,4 +1,4 @@ -import type { BillingStatus, NamespaceBilling } from "@/client"; +import type { BillingStatus, NamespaceBilling } from "@/client/model"; const ACTIVE_STATUSES = new Set([ "active", diff --git a/ui/apps/console/src/utils/deviceTags.ts b/ui/apps/console/src/utils/deviceTags.ts index 1674b12be27..5c8332368b2 100644 --- a/ui/apps/console/src/utils/deviceTags.ts +++ b/ui/apps/console/src/utils/deviceTags.ts @@ -1,4 +1,4 @@ -import type { Device } from "@/client"; +import type { Device } from "@/client/model"; /** A device whose tags are flattened to plain names, the shape the console renders. */ export type TaggedDevice = Omit & { tags: string[] }; diff --git a/ui/apps/console/src/utils/invitations.ts b/ui/apps/console/src/utils/invitations.ts index 810f6e2c81d..534089c2494 100644 --- a/ui/apps/console/src/utils/invitations.ts +++ b/ui/apps/console/src/utils/invitations.ts @@ -1,4 +1,4 @@ -import type { MembershipInvitation } from "@/client"; +import type { MembershipInvitation } from "@/client/model"; import { toBase64Json } from "@/utils/encoding"; /** diff --git a/ui/apps/console/src/utils/license.ts b/ui/apps/console/src/utils/license.ts index c15adda9c77..bdf3ab0f83f 100644 --- a/ui/apps/console/src/utils/license.ts +++ b/ui/apps/console/src/utils/license.ts @@ -1,5 +1,5 @@ import { formatExpiry } from "./date"; -import type { GetLicenseResponse } from "../client"; +import type { GetLicense200 as GetLicenseResponse } from "@/client/model"; /** * Formats a license timestamp. -1 is the sentinel for a boundary already passed, and renders as diff --git a/ui/apps/console/src/utils/session.ts b/ui/apps/console/src/utils/session.ts index eee032a7dd3..fbdc65c741c 100644 --- a/ui/apps/console/src/utils/session.ts +++ b/ui/apps/console/src/utils/session.ts @@ -1,4 +1,4 @@ -import type { Session } from "../client"; +import type { Session } from "@/client/model"; /** * The badge for a session, derived from the event types it recorded: an SFTP transfer, a single diff --git a/ui/apps/console/src/utils/sshIdentity.ts b/ui/apps/console/src/utils/sshIdentity.ts index 80a30f57126..c381486a3d0 100644 --- a/ui/apps/console/src/utils/sshIdentity.ts +++ b/ui/apps/console/src/utils/sshIdentity.ts @@ -1,7 +1,7 @@ import { differenceInDays } from "date-fns"; import { formatDateFull, formatDateShort } from "@/utils/date"; import { isSdkError } from "@/api/errors"; -import type { SshIdentity } from "@/client"; +import type { SshIdentity } from "@/client/model"; /** * Whether an enroll failure means the key is already an identity. The endpoint diff --git a/ui/apps/console/src/utils/stats.ts b/ui/apps/console/src/utils/stats.ts index 3d502b4fd7d..a21fb745da9 100644 --- a/ui/apps/console/src/utils/stats.ts +++ b/ui/apps/console/src/utils/stats.ts @@ -1,4 +1,4 @@ -import type { GetStatusDevicesResponse } from "@/client"; +import type { GetStatusDevices200 as GetStatusDevicesResponse } from "@/client/model"; /** True if the namespace has at least one device in any status. */ export function hasAnyDevices(stats: GetStatusDevicesResponse | null): boolean { diff --git a/ui/apps/console/src/utils/vault-backend-server.ts b/ui/apps/console/src/utils/vault-backend-server.ts index 35e0fc926f9..8303d90d921 100644 --- a/ui/apps/console/src/utils/vault-backend-server.ts +++ b/ui/apps/console/src/utils/vault-backend-server.ts @@ -4,8 +4,9 @@ import { saveVaultData, saveVaultSettings, deleteVault, -} from "@/client"; -import type { VaultResponse } from "@/client"; +} from "@/client/api"; +import type { VaultResponse } from "@/client/model"; +import { isSdkError } from "@/api/errors"; import type { VaultMeta, VaultData, @@ -60,12 +61,14 @@ export class ServerVaultBackend implements IVaultBackend { } private async fetch(): Promise { - const { data, error, response } = await getVault(); - if (response?.status === 404) return null; - if (error || !data) + try { + const data = await getVault(); + this.track(data); + return data; + } catch (err) { + if (isSdkError(err) && err.status === 404) return null; throw new Error("Failed to load the vault from the server."); - this.track(data); - return data; + } } /** @@ -81,12 +84,12 @@ export class ServerVaultBackend implements IVaultBackend { * means the vault they just created was not saved. */ async saveMeta(meta: VaultMeta): Promise { - const { data, error } = await saveVaultMeta({ - body: { meta: JSON.stringify(meta) }, - }); - if (error || !data) + try { + const data = await saveVaultMeta({ meta: JSON.stringify(meta) }); + this.track(data); + } catch { throw new Error("Failed to save the vault to the server."); - this.track(data); + } } /** @@ -103,18 +106,21 @@ export class ServerVaultBackend implements IVaultBackend { * overwrite, because a blind write would silently drop the other session's keys. */ async saveData(data: VaultData): Promise { - const res = await saveVaultData({ - body: { data: JSON.stringify(data), version: this.version }, - }); - if (res.response?.status === 409) { - await this.fetch().catch(() => null); - throw new Error( - "The vault was changed in another session. Reload the vault and try again.", - ); - } - if (res.error || !res.data) + try { + const res = await saveVaultData({ + data: JSON.stringify(data), + version: this.version, + }); + this.track(res); + } catch (err) { + if (isSdkError(err) && err.status === 409) { + await this.fetch().catch(() => null); + throw new Error( + "The vault was changed in another session. Reload the vault and try again.", + ); + } throw new Error("Failed to save the vault to the server."); - this.track(res.data); + } } /** @@ -122,9 +128,12 @@ export class ServerVaultBackend implements IVaultBackend { * the version counter is dropped so a later write does not carry a stale one. */ async clear(): Promise { - const { error, response } = await deleteVault(); - if (error && response?.status !== 404) - throw new Error("Failed to reset the vault on the server."); + try { + await deleteVault(); + } catch (err) { + if (!isSdkError(err) || err.status !== 404) + throw new Error("Failed to reset the vault on the server."); + } versionRegistry.delete(this.key); } @@ -141,12 +150,14 @@ export class ServerVaultBackend implements IVaultBackend { * Stores the vault settings. */ async saveSettings(settings: VaultSettings): Promise { - const { data, error } = await saveVaultSettings({ - body: { settings: JSON.stringify(settings) }, - }); - if (error || !data) + try { + const data = await saveVaultSettings({ + settings: JSON.stringify(settings), + }); + this.track(data); + } catch { throw new Error("Failed to save the vault settings to the server."); - this.track(data); + } } /** From 3ef48911f92c41026da30df910a75c883732dd32 Mon Sep 17 00:00:00 2001 From: luizhf42 Date: Thu, 3 Sep 2026 15:06:19 -0300 Subject: [PATCH 3/5] refactor(ui): migrate pages to orval generated client --- ui/apps/console/src/pages/AcceptInvite.tsx | 29 ++++--- ui/apps/console/src/pages/AddDevice.tsx | 19 +++-- ui/apps/console/src/pages/BannerEdit.tsx | 7 +- .../console/src/pages/ContainerDetails.tsx | 22 +++-- ui/apps/console/src/pages/Dashboard.tsx | 8 +- ui/apps/console/src/pages/DeviceDetails.tsx | 28 ++++--- ui/apps/console/src/pages/ForgotPassword.tsx | 9 +- ui/apps/console/src/pages/Login.tsx | 6 +- ui/apps/console/src/pages/MfaRecover.tsx | 4 +- .../console/src/pages/MfaResetComplete.tsx | 12 +-- ui/apps/console/src/pages/SSHApproval.tsx | 18 ++-- ui/apps/console/src/pages/SessionDetails.tsx | 22 ++--- ui/apps/console/src/pages/Settings.tsx | 36 ++++---- ui/apps/console/src/pages/Setup.tsx | 17 ++-- ui/apps/console/src/pages/UpdatePassword.tsx | 8 +- ui/apps/console/src/pages/WebEndpoints.tsx | 42 +++++++--- .../src/pages/__tests__/Login.test.tsx | 2 +- .../access-policies/AccessPolicyDrawer.tsx | 17 ++-- .../access-policies/__tests__/index.test.tsx | 2 +- .../src/pages/access-policies/index.tsx | 16 ++-- ui/apps/console/src/pages/admin/Dashboard.tsx | 8 +- ui/apps/console/src/pages/admin/License.tsx | 8 +- .../src/pages/admin/SessionDetails.tsx | 10 ++- ui/apps/console/src/pages/admin/Sessions.tsx | 18 ++-- .../announcements/AnnouncementDetails.tsx | 6 +- .../DeleteAnnouncementDialog.tsx | 6 +- .../admin/announcements/EditAnnouncement.tsx | 13 ++- .../admin/announcements/NewAnnouncement.tsx | 6 +- .../__tests__/AdminAnnouncements.test.tsx | 2 +- .../src/pages/admin/announcements/index.tsx | 25 +++--- .../admin/devices/AdminDeviceDetails.tsx | 13 ++- .../pages/admin/devices/DeviceStatusChip.tsx | 2 +- .../__tests__/AdminDeviceDetails.test.tsx | 2 +- .../console/src/pages/admin/devices/index.tsx | 56 +++++++++---- .../AdminFirewallRuleDetails.tsx | 10 ++- .../AdminFirewallRuleDetails.test.tsx | 2 +- .../src/pages/admin/firewall-rules/index.tsx | 41 ++++++---- .../GenerateInstanceKeyDrawer.tsx | 6 +- .../instance-api-keys/InstanceApiKeys.tsx | 33 ++++---- .../__tests__/InstanceApiKeys.test.tsx | 26 +++--- .../namespaces/DeleteNamespaceDialog.tsx | 6 +- .../admin/namespaces/EditNamespaceDrawer.tsx | 10 +-- .../admin/namespaces/NamespaceDetails.tsx | 19 +++-- .../__tests__/EditNamespaceDrawer.test.tsx | 2 +- .../admin/namespaces/editNamespaceSchema.ts | 2 +- .../src/pages/admin/namespaces/index.tsx | 32 ++++++-- .../pages/admin/settings/Authentication.tsx | 26 ++---- .../pages/admin/settings/SamlConfigDrawer.tsx | 7 +- .../src/pages/admin/settings/samlSchema.ts | 2 +- .../pages/admin/users/CreateUserDrawer.tsx | 15 ++-- .../pages/admin/users/DeleteUserDialog.tsx | 6 +- .../src/pages/admin/users/EditUserDrawer.tsx | 10 +-- .../pages/admin/users/ResetPasswordDialog.tsx | 6 +- .../src/pages/admin/users/UserDetails.tsx | 11 ++- .../admin/users/__tests__/AdminUsers.test.tsx | 2 +- .../users/__tests__/EditUserDrawer.test.tsx | 2 +- .../console/src/pages/admin/users/index.tsx | 38 ++++++--- .../src/pages/admin/users/userSchema.ts | 2 +- .../__tests__/ContainerDetails.test.tsx | 2 +- .../console/src/pages/containers/index.tsx | 82 ++++++++++--------- .../src/pages/devices/CustomFieldsSection.tsx | 11 +-- .../devices/__tests__/DeviceDetails.test.tsx | 2 +- ui/apps/console/src/pages/devices/index.tsx | 8 +- .../src/pages/firewall-rules/RuleDrawer.tsx | 11 +-- .../__tests__/RuleDrawer.test.tsx | 2 +- .../__tests__/ruleSchema.test.ts | 2 +- .../src/pages/firewall-rules/index.tsx | 18 ++-- .../src/pages/firewall-rules/ruleSchema.ts | 2 +- .../install-keys/CreateInstallKeyDrawer.tsx | 6 +- .../install-keys/EditInstallKeyDrawer.tsx | 8 +- .../src/pages/install-keys/EventPublicKey.tsx | 2 +- .../src/pages/install-keys/ExpiryLabel.tsx | 2 +- .../pages/install-keys/InstallKeyActions.tsx | 2 +- .../install-keys/InstallKeyActionsMenu.tsx | 2 +- .../install-keys/InstallKeyEventReview.tsx | 2 +- .../install-keys/InstallKeyEventsTable.tsx | 21 +++-- .../install-keys/InstallKeyHistoryPage.tsx | 6 +- .../pages/install-keys/InstallKeysTable.tsx | 2 +- .../install-keys/RevealInstallKeyDialog.tsx | 16 ++-- .../install-keys/RevokeInstallKeyDialog.tsx | 10 +-- .../src/pages/install-keys/UsageMeter.tsx | 2 +- .../install-keys/__tests__/helpers.test.ts | 2 +- .../console/src/pages/install-keys/helpers.ts | 2 +- .../console/src/pages/install-keys/index.tsx | 12 +-- .../install-keys/installKeyEventColumns.tsx | 2 +- .../pages/install-keys/useToggleInstallKey.ts | 14 ++-- .../src/pages/public-keys/KeyDrawer.tsx | 13 ++- .../public-keys/__tests__/KeyDrawer.test.tsx | 2 +- .../public-keys/__tests__/keySchema.test.ts | 2 +- .../console/src/pages/public-keys/index.tsx | 30 ++++--- .../src/pages/public-keys/keySchema.ts | 2 +- ui/apps/console/src/pages/sessions/index.tsx | 22 +++-- .../pages/ssh-identities/IdentityDrawer.tsx | 22 ++--- .../ssh-identities/__tests__/index.test.tsx | 2 +- .../src/pages/ssh-identities/index.tsx | 18 ++-- .../src/pages/team/AddMemberDrawer.tsx | 20 +++-- ui/apps/console/src/pages/team/ApiKeysTab.tsx | 29 ++++--- .../console/src/pages/team/EditKeyDrawer.tsx | 10 +-- .../src/pages/team/EditMemberDrawer.tsx | 9 +- .../src/pages/team/GenerateKeyDrawer.tsx | 6 +- ui/apps/console/src/pages/team/MembersTab.tsx | 49 +++++------ .../src/pages/team/ServiceAccountDrawer.tsx | 4 +- .../src/pages/team/ServiceAccountsTab.tsx | 9 +- .../pages/team/__tests__/ApiKeysTab.test.tsx | 2 +- ui/apps/console/src/pages/team/schemas.ts | 2 +- 105 files changed, 738 insertions(+), 583 deletions(-) diff --git a/ui/apps/console/src/pages/AcceptInvite.tsx b/ui/apps/console/src/pages/AcceptInvite.tsx index e8f9fa99cba..6c76f46f5e7 100644 --- a/ui/apps/console/src/pages/AcceptInvite.tsx +++ b/ui/apps/console/src/pages/AcceptInvite.tsx @@ -12,8 +12,7 @@ import { } from "@heroicons/react/24/outline"; import { useAuthStore } from "@/stores/authStore"; import { useSignUpStore } from "@/stores/signUpStore"; -import { useAcceptInvite } from "@/hooks/useInvitationMutations"; -import { useResolveInvitation } from "@/hooks/useInvitations"; +import { useAcceptInvite, useResolveInvitation } from "@/client/api"; import { useSwitchNamespace } from "@/hooks/useNamespaceMutations"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import { @@ -60,10 +59,17 @@ export default function AcceptInvite() { const signUpLoading = useSignUpStore((s) => s.signUpLoading); const signUpError = useSignUpStore((s) => s.signUpError); - const { resolved, isLoading, isError } = useResolveInvitation(invite); + const { + data: resolvedInvite, + isLoading, + isError, + } = useResolveInvitation( + { invite }, + { query: { enabled: !!invite, retry: false, staleTime: Infinity } }, + ); - const tenant = resolved?.tenantId ?? ""; - const inviteEmail = resolved?.email ?? ""; + const tenant = resolvedInvite?.tenant_id ?? ""; + const inviteEmail = resolvedInvite?.email ?? ""; const [postAction, setPostAction] = useState(null); const [showConfirm, setShowConfirm] = useState(false); @@ -83,8 +89,9 @@ export default function AcceptInvite() { const needsLogin = !authToken && !postAction && - !!resolved && - (resolved.status === "not-confirmed" || resolved.status === "confirmed"); + !!resolvedInvite && + (resolvedInvite.status === "not-confirmed" || + resolvedInvite.status === "confirmed"); useEffect(() => { if (!needsLogin) return; @@ -96,13 +103,13 @@ export default function AcceptInvite() { if (postAction) return postAction.kind; if (!invite) return "missing-params"; if (isLoading || needsLogin) return "loading"; - if (isError || !resolved) return "error"; + if (isError || !resolvedInvite) return "error"; if (authToken) { - return authUserId === resolved.userId ? "accept" : "wrong-user"; + return authUserId === resolvedInvite.user_id ? "accept" : "wrong-user"; } - if (resolved.status === "invited") return "sign-up"; + if (resolvedInvite.status === "invited") return "sign-up"; return "error"; })(); @@ -140,7 +147,7 @@ export default function AcceptInvite() { if (!tenant || !authToken) return; setError(""); try { - await acceptInvite.mutateAsync({ path: { tenant } }); + await acceptInvite.mutateAsync({ tenant }); setShowConfirm(false); setPostAction({ kind: "joined" }); } catch { diff --git a/ui/apps/console/src/pages/AddDevice.tsx b/ui/apps/console/src/pages/AddDevice.tsx index b438ac3b6bc..8c0c955ce4c 100644 --- a/ui/apps/console/src/pages/AddDevice.tsx +++ b/ui/apps/console/src/pages/AddDevice.tsx @@ -21,8 +21,7 @@ import CreateInstallKeyDrawer from "@/pages/install-keys/CreateInstallKeyDrawer" import { isSystemKey } from "@/pages/install-keys/helpers"; import { modeInfo } from "@/pages/install-keys/constants"; import { METHODS, type Method } from "@/pages/install/methods"; -import { useInstallKeys } from "@/hooks/useInstallKeys"; -import { useRevealInstallKey } from "@/hooks/useRevealInstallKey"; +import { useInstallKeyList, useInstallKeyReveal } from "@/client/api"; import InputField from "@/components/common/fields/InputField"; import NumericInput from "@/components/common/fields/NumericInput"; import RadioCard from "@/components/common/fields/RadioCard"; @@ -112,16 +111,22 @@ export default function AddDevice() { const origin = window.location.origin; - const { installKeys } = useInstallKeys({ perPage: 50 }); + const { data: installKeys = [] } = useInstallKeyList({ + page: 1, + per_page: 50, + sort_by: "created_at", + order_by: "desc", + }); const usableKeys = installKeys.filter( (k) => !isSystemKey(k) && !k.revoked && !k.disabled, ); const selectedKey = usableKeys.find((k) => k.name === selectedKeyName) ?? usableKeys[0]; - const { key: revealedKey } = useRevealInstallKey( - aud === "fleet" ? (selectedKey?.name ?? null) : null, - aud === "fleet", - ); + const revealKeyName = aud === "fleet" ? selectedKey?.name : undefined; + const { data: revealData } = useInstallKeyReveal(revealKeyName ?? "", { + query: { enabled: !!revealKeyName, gcTime: 0 }, + }); + const revealedKey = revealData?.key ?? ""; const codeless = aud === "machine" && CODELESS_METHODS.includes(method); diff --git a/ui/apps/console/src/pages/BannerEdit.tsx b/ui/apps/console/src/pages/BannerEdit.tsx index 0478e102bff..eb770fb9e61 100644 --- a/ui/apps/console/src/pages/BannerEdit.tsx +++ b/ui/apps/console/src/pages/BannerEdit.tsx @@ -4,7 +4,7 @@ import { CheckIcon } from "@heroicons/react/24/outline"; import Breadcrumb from "@/components/common/Breadcrumb"; import { useNamespace } from "../hooks/useNamespaces"; import type { Namespace } from "../hooks/useNamespaces"; -import { useEditNamespace } from "../hooks/useNamespaceMutations"; +import { useEditNamespace } from "@/client/api"; import { useAuthStore } from "../stores/authStore"; import { useHasPermission } from "../hooks/useHasPermission"; import { Button } from "@shellhub/design-system/primitives"; @@ -31,12 +31,13 @@ function BannerEditor({ ns, canEdit }: { ns: Namespace; canEdit: boolean }) { setError(""); try { await editNs.mutateAsync({ - path: { tenant: ns.tenant_id }, - body: { + tenant: ns.tenant_id, + data: { settings: { connection_announcement: text, session_record: ns.settings?.session_record ?? false, ssh_access_mode: ns.settings?.ssh_access_mode ?? "legacy", + ssh_legacy_allowed: ns.settings?.ssh_legacy_allowed ?? false, }, }, }); diff --git a/ui/apps/console/src/pages/ContainerDetails.tsx b/ui/apps/console/src/pages/ContainerDetails.tsx index 45cbe5ee344..5f516c127c1 100644 --- a/ui/apps/console/src/pages/ContainerDetails.tsx +++ b/ui/apps/console/src/pages/ContainerDetails.tsx @@ -8,12 +8,12 @@ import { CubeIcon, ChevronDoubleRightIcon, } from "@heroicons/react/24/outline"; -import { useContainer } from "../hooks/useContainer"; import { - useRenameContainer, - useAddContainerTag, - useRemoveContainerTag, -} from "../hooks/useContainerMutations"; + useGetContainer, + useUpdateContainer, + usePullTagFromContainer, +} from "@/client/api"; +import { useAddContainerTag } from "../hooks/useContainerMutations"; import { normalizeDeviceTags } from "@/utils/deviceTags"; import { useNamespace } from "../hooks/useNamespaces"; import { useAuthStore } from "../stores/authStore"; @@ -43,7 +43,13 @@ export default function ContainerDetails() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const { container, isLoading, error } = useContainer(uid ?? ""); + const { + data: container, + isLoading, + error, + } = useGetContainer(uid ?? "", { + query: { enabled: !!uid }, + }); const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); @@ -53,9 +59,9 @@ export default function ContainerDetails() { const restoreTerminal = useTerminalStore((s) => s.restore); const [connectOpen, setConnectOpen] = useState(false); - const renameMutation = useRenameContainer(); + const renameMutation = useUpdateContainer(); const addTagMutation = useAddContainerTag(); - const removeTagMutation = useRemoveContainerTag(); + const removeTagMutation = usePullTagFromContainer(); const containerActions = useActionDialog({ onSuccess: (operation) => { if (operation === "remove") void navigate("/containers"); diff --git a/ui/apps/console/src/pages/Dashboard.tsx b/ui/apps/console/src/pages/Dashboard.tsx index 42fb4c73e95..6800158f520 100644 --- a/ui/apps/console/src/pages/Dashboard.tsx +++ b/ui/apps/console/src/pages/Dashboard.tsx @@ -6,7 +6,7 @@ import { } from "@heroicons/react/24/outline"; import { useNamespace } from "@/hooks/useNamespaces"; import { useAuthStore } from "@/stores/authStore"; -import { useStats } from "@/hooks/useStats"; +import { useGetStatusDevices } from "@/client/api"; import { hasAnyDevices } from "@/utils/stats"; import PageHeader from "@/components/common/PageHeader"; import StatCard from "@/components/common/StatCard"; @@ -22,7 +22,11 @@ import { Card } from "@shellhub/design-system/primitives"; export default function Dashboard() { const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); - const { stats, isLoading: statsLoading, error: statsError } = useStats(); + const { + data: stats, + isLoading: statsLoading, + error: statsError, + } = useGetStatusDevices(); if (statsLoading) return null; diff --git a/ui/apps/console/src/pages/DeviceDetails.tsx b/ui/apps/console/src/pages/DeviceDetails.tsx index 3fa31ca3aa8..9eed63a93f6 100644 --- a/ui/apps/console/src/pages/DeviceDetails.tsx +++ b/ui/apps/console/src/pages/DeviceDetails.tsx @@ -13,15 +13,15 @@ import { CpuChipIcon, ChevronDoubleRightIcon, } from "@heroicons/react/24/outline"; -import { useDevice } from "../hooks/useDevice"; -import { useActionDialog } from "../hooks/useActionDialog"; import { - useRenameDevice, - useAddDeviceTag, - useRemoveDeviceTag, -} from "../hooks/useDeviceMutations"; + useGetDevice, + useUpdateDevice, + usePullTagFromDevice, +} from "@/client/api"; +import { useActionDialog } from "../hooks/useActionDialog"; +import { useAddDeviceTag } from "../hooks/useDeviceMutations"; import { useNamespace } from "../hooks/useNamespaces"; -import { useInstallKeys } from "../hooks/useInstallKeys"; +import { useInstallKeyList } from "@/client/api"; import { enrollmentSourceName, resolveEnrollmentSource, @@ -54,19 +54,25 @@ export default function DeviceDetails() { const { uid } = useParams<{ uid: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const { device, isLoading, error } = useDevice(uid ?? ""); + const { + data: device, + isLoading, + error, + } = useGetDevice(uid ?? "", { + query: { enabled: !!uid }, + }); const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); - const { installKeys } = useInstallKeys({ perPage: 100 }); + const { data: installKeys = [] } = useInstallKeyList({ per_page: 100 }); const existingSession = useTerminalStore((s) => s.sessions.find((sess) => sess.deviceUid === uid), ); const restoreTerminal = useTerminalStore((s) => s.restore); const [connectOpen, setConnectOpen] = useState(false); - const renameMutation = useRenameDevice(); + const renameMutation = useUpdateDevice(); const canRename = useHasPermission("device:rename"); const addTagMutation = useAddDeviceTag(); - const removeTagMutation = useRemoveDeviceTag(); + const removeTagMutation = usePullTagFromDevice(); const actionsController = useActionDialog({ onSuccess: (operation) => { if (operation === "remove") void navigate("/devices"); diff --git a/ui/apps/console/src/pages/ForgotPassword.tsx b/ui/apps/console/src/pages/ForgotPassword.tsx index e57ad288d1e..6aaec8319c3 100644 --- a/ui/apps/console/src/pages/ForgotPassword.tsx +++ b/ui/apps/console/src/pages/ForgotPassword.tsx @@ -7,7 +7,7 @@ import { LockClosedIcon, } from "@heroicons/react/24/outline"; import { Button } from "@shellhub/design-system/primitives"; -import { recoverPassword } from "../client"; +import { recoverPassword } from "@/client/api"; import FormInputField from "@/components/common/fields/rhf/FormInputField"; import { forgotPasswordResolver, @@ -36,10 +36,9 @@ export default function ForgotPassword() { const onSubmit = async (values: ForgotPasswordFormValues) => { setLoading(true); - await recoverPassword({ - body: { username: values.account }, - throwOnError: true, - }).catch(silenceToPreventAccountEnumeration); + await recoverPassword({ username: values.account }).catch( + silenceToPreventAccountEnumeration, + ); setLoading(false); setSent(true); diff --git a/ui/apps/console/src/pages/Login.tsx b/ui/apps/console/src/pages/Login.tsx index af74c481c3d..860078b9a32 100644 --- a/ui/apps/console/src/pages/Login.tsx +++ b/ui/apps/console/src/pages/Login.tsx @@ -19,7 +19,7 @@ import { getSafeRedirect, resolvePostLoginRedirect } from "@/utils/navigation"; import PendingDeviceCallout from "@/components/auth/PendingDeviceCallout"; import AuthFooterLinks from "../components/common/AuthFooterLinks"; import LoginLayoutCard from "@/components/layout/LoginLayoutCard"; -import { getInfo, getSamlAuthUrl } from "../client"; +import { getInfo, getSamlAuthUrl } from "@/client/api"; import { FormInputField, FormPasswordField, @@ -103,7 +103,7 @@ export default function Login() { useEffect(() => { void getInfo() - .then(({ data }) => setAuthentication(data?.authentication ?? null)) + .then((data) => setAuthentication(data?.authentication ?? null)) .catch(() => setAuthentication(null)); }, []); @@ -138,7 +138,7 @@ export default function Login() { const handleSsoLogin = async () => { setSsoLoading(true); try { - const { data } = await getSamlAuthUrl({ throwOnError: true }); + const data = await getSamlAuthUrl(); window.location.replace(data.url); } catch { setError("Failed to retrieve SSO login URL. Please try again."); diff --git a/ui/apps/console/src/pages/MfaRecover.tsx b/ui/apps/console/src/pages/MfaRecover.tsx index 2c9c39ff516..76faeac1cda 100644 --- a/ui/apps/console/src/pages/MfaRecover.tsx +++ b/ui/apps/console/src/pages/MfaRecover.tsx @@ -4,7 +4,7 @@ import { KeyIcon } from "@heroicons/react/24/outline"; import { useForm } from "react-hook-form"; import { Button, Callout } from "@shellhub/design-system/primitives"; import { useAuthStore } from "../stores/authStore"; -import { recoveryDisableMfa } from "../client"; +import { recoveryDisableMFA } from "@/client/api"; import MfaRecoveryTimeoutModal from "../components/mfa/MfaRecoveryTimeoutModal"; import AuthFooterLinks from "../components/common/AuthFooterLinks"; import LoginLayoutCard from "@/components/layout/LoginLayoutCard"; @@ -67,7 +67,7 @@ export default function MfaRecover() { }; const handleDisableMfa = async () => { - await recoveryDisableMfa({ throwOnError: true }); + await recoveryDisableMFA(); updateMfaStatus(false); setShowTimeoutModal(false); void navigate("/dashboard"); diff --git a/ui/apps/console/src/pages/MfaResetComplete.tsx b/ui/apps/console/src/pages/MfaResetComplete.tsx index 3be99684a5b..fe5d7e8fea5 100644 --- a/ui/apps/console/src/pages/MfaResetComplete.tsx +++ b/ui/apps/console/src/pages/MfaResetComplete.tsx @@ -5,7 +5,7 @@ import { ExclamationCircleIcon, } from "@heroicons/react/24/outline"; import { Button, Callout } from "@shellhub/design-system/primitives"; -import { resetMfa } from "../client"; +import { resetMFA } from "@/client/api"; import { useAuthStore } from "../stores/authStore"; import { useOtpInput } from "../hooks/useOtpInput"; import AuthFooterLinks from "../components/common/AuthFooterLinks"; @@ -34,13 +34,9 @@ export default function MfaResetComplete() { setError(null); try { - const { data } = await resetMfa({ - path: { "user-id": userId }, - body: { - main_email_code: otpMain.getValue(), - recovery_email_code: otpRecovery.getValue(), - }, - throwOnError: true, + const data = await resetMFA(userId, { + main_email_code: otpMain.getValue(), + recovery_email_code: otpRecovery.getValue(), }); useAuthStore.setState({ diff --git a/ui/apps/console/src/pages/SSHApproval.tsx b/ui/apps/console/src/pages/SSHApproval.tsx index 11fe12f2e20..0cd4ffd4ab2 100644 --- a/ui/apps/console/src/pages/SSHApproval.tsx +++ b/ui/apps/console/src/pages/SSHApproval.tsx @@ -20,7 +20,7 @@ import { Button, Spinner } from "@shellhub/design-system/primitives"; import BaseDialog from "@/components/common/BaseDialog"; import InputField from "@/components/common/fields/InputField"; import { isSdkError } from "@/api/errors"; -import { webTerminalReauth, getSamlReauthUrl } from "@/client"; +import { webTerminalReauth, getSamlReauthUrl } from "@/client/api"; import { useOtpInput } from "@/hooks/useOtpInput"; import { useSSHApproval, ApprovalDetails } from "@/hooks/useSSHApproval"; import { useAuthStore } from "@/stores/authStore"; @@ -439,11 +439,8 @@ function ReauthFactor({ if (submitting) return; setSubmitting(true); setError(null); - getSamlReauthUrl({ - query: { fingerprint, approval_code: approvalCode }, - throwOnError: true, - }) - .then(({ data: { url } }) => { + getSamlReauthUrl({ fingerprint, approval_code: approvalCode }) + .then(({ url }) => { if (!window.open(url, "sso-reauth", "width=520,height=680")) { setError( "Pop-up blocked. Allow pop-ups for this site and try again.", @@ -463,12 +460,9 @@ function ReauthFactor({ setError(null); try { await webTerminalReauth({ - body: { - ...(mfaEnabled ? { code: otp.getValue() } : { password }), - fingerprint, - approval_code: approvalCode, - }, - throwOnError: true, + ...(mfaEnabled ? { code: otp.getValue() } : { password }), + fingerprint, + approval_code: approvalCode, }); onDone(); } catch (err) { diff --git a/ui/apps/console/src/pages/SessionDetails.tsx b/ui/apps/console/src/pages/SessionDetails.tsx index 6e28d09e5b6..93026e07fde 100644 --- a/ui/apps/console/src/pages/SessionDetails.tsx +++ b/ui/apps/console/src/pages/SessionDetails.tsx @@ -21,18 +21,18 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/solid"; -import { useSession } from "../hooks/useSession"; import { - useCloseSession, - useDeleteSessionRecording, -} from "../hooks/useSessionMutations"; + useGetSession, + useClsoeSession, + useDeleteSessionRecord, +} from "@/client/api"; import { useSessionRecording } from "../hooks/useSessionRecording"; import SessionPlayerDialog from "./sessions/SessionPlayerDialog"; import CopyButton from "../components/common/CopyButton"; import DeviceChip from "../components/common/DeviceChip"; import DistroIcon from "../components/common/DistroIcon"; import { formatDateFull, formatRelative, formatDuration } from "../utils/date"; -import type { Session } from "../client"; +import type { Session } from "@/client/model"; import RestrictedAction from "../components/common/RestrictedAction"; import PageLoader from "@/components/common/PageLoader"; import ConfirmDialog from "../components/common/ConfirmDialog"; @@ -249,9 +249,9 @@ function DurationStat({ */ export default function SessionDetails() { const { uid } = useParams<{ uid: string }>(); - const { session, isLoading, error } = useSession(uid!); - const closeSession = useCloseSession(); - const deleteRecording = useDeleteSessionRecording(); + const { data: session, isLoading, error } = useGetSession(uid ?? ""); + const closeSession = useClsoeSession(); + const deleteRecording = useDeleteSessionRecord(); const { logs: sessionLogs, isLoading: logsLoading, @@ -275,7 +275,7 @@ export default function SessionDetails() { const handleDeleteLogs = async () => { setDeleteLogsError(null); try { - await deleteRecording.mutateAsync(uid!); + await deleteRecording.mutateAsync({ uid: uid!, seat: 0 }); setShowDeleteLogs(false); } catch { setDeleteLogsError("Failed to delete recording. Check your permissions."); @@ -287,8 +287,8 @@ export default function SessionDetails() { setCloseError(null); try { await closeSession.mutateAsync({ - path: { uid }, - body: { device: session.device_uid ?? session.device?.uid ?? "" }, + uid, + data: { device: session.device_uid ?? session.device?.uid ?? "" }, }); setShowClose(false); } catch { diff --git a/ui/apps/console/src/pages/Settings.tsx b/ui/apps/console/src/pages/Settings.tsx index abdcfdbe981..aa48c3d137d 100644 --- a/ui/apps/console/src/pages/Settings.tsx +++ b/ui/apps/console/src/pages/Settings.tsx @@ -17,13 +17,13 @@ import { } from "@heroicons/react/24/outline"; import { isSdkError } from "../api/errors"; import { useNamespace } from "../hooks/useNamespaces"; -import { useAccessPolicies } from "../hooks/useAccessPolicies"; import { + useListAccessPolicies, useEditNamespace, - useDeleteNamespace, - useLeaveNamespace, useSetSshAccessMode, -} from "../hooks/useNamespaceMutations"; + deleteNamespace, + leaveNamespace, +} from "@/client/api"; import { useAuthStore } from "../stores/authStore"; import { useHasPermission } from "../hooks/useHasPermission"; import PageHeader from "../components/common/PageHeader"; @@ -50,6 +50,11 @@ import PageLoader from "@/components/common/PageLoader"; import SettingsCard from "@/components/common/SettingsCard"; import SettingsRow from "@/components/common/SettingsRow"; +function logoutAndRedirect() { + useAuthStore.getState().logout(); + window.location.replace("/login"); +} + function EditNameDrawer({ open, onClose, @@ -74,8 +79,8 @@ function EditNameDrawer({ clearErrors("root"); try { await editNs.mutateAsync({ - path: { tenant: tenantId }, - body: { name: values.name }, + tenant: tenantId, + data: { name: values.name }, }); onClose(); } catch { @@ -125,7 +130,6 @@ function DeleteDialog({ tenantId: string; onClose: () => void; }) { - const deleteNs = useDeleteNamespace(); const [confirm, setConfirm] = useState(""); const [error, setError] = useState(""); @@ -138,7 +142,8 @@ function DeleteDialog({ onConfirm={async () => { setError(""); try { - await deleteNs.mutateAsync(tenantId); + await deleteNamespace(tenantId); + logoutAndRedirect(); } catch (err) { setError( isSdkError(err) && err.status === 409 @@ -180,7 +185,6 @@ function LeaveDialog({ tenantId: string; onClose: () => void; }) { - const leaveNs = useLeaveNamespace(); const [error, setError] = useState(""); return ( @@ -190,7 +194,8 @@ function LeaveDialog({ onConfirm={async () => { setError(""); try { - await leaveNs.mutateAsync(tenantId); + await leaveNamespace(tenantId); + logoutAndRedirect(); } catch { setError("Failed to leave namespace."); throw new Error(); @@ -322,7 +327,7 @@ function BannerPreview({ export default function Settings() { const { tenant: tenantId } = useAuthStore(); const { namespace: ns } = useNamespace(tenantId ?? ""); - const { policies } = useAccessPolicies(); + const { data: policies = [] } = useListAccessPolicies(); const editNs = useEditNamespace(); const setSshAccessMode = useSetSshAccessMode(); const [editNameOpen, setEditNameOpen] = useState(false); @@ -352,12 +357,13 @@ export default function Settings() { setTogglingRecord(true); editNs.mutate( { - path: { tenant: tenantId }, - body: { + tenant: tenantId, + data: { settings: { session_record: !sessionRecord, connection_announcement: banner, ssh_access_mode: sshAccessMode, + ssh_legacy_allowed: sshLegacyAllowed, }, }, }, @@ -370,8 +376,8 @@ export default function Settings() { setSwitchingAccessMode(true); setSshAccessMode.mutate( { - path: { tenant: tenantId }, - body: { ssh_access_mode: mode }, + tenant: tenantId, + data: { ssh_access_mode: mode }, }, { onSettled: () => setSwitchingAccessMode(false) }, ); diff --git a/ui/apps/console/src/pages/Setup.tsx b/ui/apps/console/src/pages/Setup.tsx index 4c7ed71bbc5..9f1ee2fdee0 100644 --- a/ui/apps/console/src/pages/Setup.tsx +++ b/ui/apps/console/src/pages/Setup.tsx @@ -7,7 +7,7 @@ import { ExclamationCircleIcon, PencilSquareIcon, } from "@heroicons/react/24/outline"; -import { setup } from "../client"; +import { setup } from "@/client/api"; import { getConfig, isCommunity } from "../env"; import { useAuthStore } from "@/stores/authStore"; import { setupResolver, type SetupFormValues } from "./setup/setupResolver"; @@ -119,15 +119,12 @@ export default function Setup() { let token: string | undefined; try { - const { data } = await setup({ - body: { - name: values.name, - username: values.username, - namespace: values.namespace, - email: values.email, - password: values.password, - }, - throwOnError: true, + const data = await setup({ + name: values.name, + username: values.username, + namespace: values.namespace, + email: values.email, + password: values.password, }); token = data.token; } catch (err: unknown) { diff --git a/ui/apps/console/src/pages/UpdatePassword.tsx b/ui/apps/console/src/pages/UpdatePassword.tsx index 7e0a451c701..a9f50fed9a2 100644 --- a/ui/apps/console/src/pages/UpdatePassword.tsx +++ b/ui/apps/console/src/pages/UpdatePassword.tsx @@ -6,7 +6,7 @@ import { } from "@heroicons/react/24/outline"; import { useForm } from "react-hook-form"; import { Button } from "@shellhub/design-system/primitives"; -import { updateRecoverPassword } from "@/client"; +import { updateRecoverPassword } from "@/client/api"; import { updatePasswordResolver } from "./setup/updatePasswordResolver"; import type { UpdatePasswordFormValues } from "./setup/updatePasswordResolver"; import { FormPasswordField } from "@/components/common/fields/rhf"; @@ -36,11 +36,7 @@ export default function UpdatePassword() { setError(""); setLoading(true); try { - await updateRecoverPassword({ - path: { uid }, - body: { token, password: values.password }, - throwOnError: true, - }); + await updateRecoverPassword(uid, { token, password: values.password }); void navigate("/login", { state: { notice: "Password updated successfully. Please sign in." }, }); diff --git a/ui/apps/console/src/pages/WebEndpoints.tsx b/ui/apps/console/src/pages/WebEndpoints.tsx index 702d9bcfa0e..c0437723236 100644 --- a/ui/apps/console/src/pages/WebEndpoints.tsx +++ b/ui/apps/console/src/pages/WebEndpoints.tsx @@ -3,12 +3,15 @@ import { isSdkError } from "@/api/errors"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import { useWebEndpoints } from "@/hooks/useWebEndpoints"; import { + useListWebEndpoints, useCreateWebEndpoint, useDeleteWebEndpoint, -} from "@/hooks/useWebEndpointMutations"; -import type { Webendpoint } from "@/client"; +} from "@/client/api"; +import type { ListWebEndpointsParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; +import { toBase64Json } from "@/utils/encoding"; +import type { Webendpoint } from "@/client/model"; import { useDevices, type NormalizedDevice } from "@/hooks/useDevices"; import PageHeader from "@/components/common/PageHeader"; import EmptyState from "@/components/common/EmptyState"; @@ -383,7 +386,7 @@ function EndpointDrawer({ const domain = tlsDomain.trim(); const hasTlsConfig = tlsEnabled || domain !== ""; await createEndpoint.mutateAsync({ - body: { + data: { uid: device.uid, host: host.trim(), port: portNum, @@ -839,10 +842,25 @@ function WebEndpointsContent() { SEARCH_DEBOUNCE_MS, ); - const { webEndpoints, totalCount, isLoading } = useWebEndpoints({ + const requestParams: ListWebEndpointsParams = { page: params.page, - addressFilter: debouncedSearch, - }); + per_page: 10, + }; + if (debouncedSearch) { + requestParams.filter = toBase64Json([ + { + type: "property", + params: { + name: "address", + operator: "contains", + value: debouncedSearch, + }, + }, + ]); + } + const { data: webEndpoints = [], isLoading } = + useListWebEndpoints(requestParams); + const total = totalCount(webEndpoints); const deleteEndpoint = useDeleteWebEndpoint(); const [drawerOpen, setDrawerOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState<{ @@ -861,7 +879,7 @@ function WebEndpointsContent() { setDeleteError(null); try { await deleteEndpoint.mutateAsync({ - path: { address: deleteTarget.address }, + address: deleteTarget.address, }); if (webEndpoints.length === 1 && params.page > 1) setPage(params.page - 1); @@ -881,10 +899,10 @@ function WebEndpointsContent() { setDrawerOpen(false); }; - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const isSearching = debouncedSearch.length > 0; - const isTrulyEmpty = !isLoading && !isSearching && totalCount === 0; - const isNoResults = !isLoading && isSearching && totalCount === 0; + const isTrulyEmpty = !isLoading && !isSearching && total === 0; + const isNoResults = !isLoading && isSearching && total === 0; return ( <> @@ -1009,7 +1027,7 @@ function WebEndpointsContent() { diff --git a/ui/apps/console/src/pages/__tests__/Login.test.tsx b/ui/apps/console/src/pages/__tests__/Login.test.tsx index 49741bd27db..a32bf732d37 100644 --- a/ui/apps/console/src/pages/__tests__/Login.test.tsx +++ b/ui/apps/console/src/pages/__tests__/Login.test.tsx @@ -8,7 +8,7 @@ import { hasPendingDeviceCode, setPendingDeviceCode, } from "@/utils/navigation"; -import type { Info, UserAuth } from "@/client"; +import type { Info, UserAuth } from "@/client/model"; import { mockUserAuth } from "@/tests/factories"; import { simulateBrowserTranslation } from "@/tests/simulateBrowserTranslation"; import Login from "../Login"; diff --git a/ui/apps/console/src/pages/access-policies/AccessPolicyDrawer.tsx b/ui/apps/console/src/pages/access-policies/AccessPolicyDrawer.tsx index d2b9e6eee37..d9b55508372 100644 --- a/ui/apps/console/src/pages/access-policies/AccessPolicyDrawer.tsx +++ b/ui/apps/console/src/pages/access-policies/AccessPolicyDrawer.tsx @@ -21,13 +21,13 @@ import { cn } from "@shellhub/design-system/cn"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; import { useAuthStore } from "@/stores/authStore"; import { useNamespace, type NamespaceMember } from "@/hooks/useNamespaces"; -import { useServiceAccounts } from "@/hooks/useServiceAccounts"; -import { useTags } from "@/hooks/useTags"; import { + useListServiceAccounts, useCreateAccessPolicy, useUpdateAccessPolicy, -} from "@/hooks/useAccessPolicyMutations"; -import type { AccessPolicy, AccessPolicyRequest } from "@/client"; +} from "@/client/api"; +import { useTagNames } from "@/hooks/useTags"; +import type { AccessPolicy, AccessPolicyRequest } from "@/client/model"; import { ROLES } from "@/pages/team/helpers"; import SourceIpInput from "@/components/common/fields/SourceIpInput"; import InputField from "@/components/common/fields/InputField"; @@ -354,8 +354,7 @@ function AccessPolicyDrawer({ }) { const { tenant: tenantId } = useAuthStore(); const { namespace } = useNamespace(tenantId ?? ""); - const { tags: allTagObjects } = useTags(); - const allTags = allTagObjects.map((t) => t.name); + const { names: allTags } = useTagNames(); const createPolicy = useCreateAccessPolicy(); const updatePolicy = useUpdateAccessPolicy(); const isEdit = !!editPolicy; @@ -364,7 +363,7 @@ function AccessPolicyDrawer({ (m): m is NamespaceMember => !!m.id && !!m.role && !!m.email && String(m.role) !== "service", ); - const { serviceAccounts } = useServiceAccounts(); + const { data: serviceAccounts = [] } = useListServiceAccounts(); const roleMemberCount = (role: string) => members.filter((m) => String(m.role) === role).length; @@ -501,9 +500,9 @@ function AccessPolicyDrawer({ }; try { if (isEdit && editPolicy) { - await updatePolicy.mutateAsync({ path: { id: editPolicy.id }, body }); + await updatePolicy.mutateAsync({ id: editPolicy.id, data: body }); } else { - await createPolicy.mutateAsync({ body }); + await createPolicy.mutateAsync({ data: body }); } onClose(); } catch (err: unknown) { diff --git a/ui/apps/console/src/pages/access-policies/__tests__/index.test.tsx b/ui/apps/console/src/pages/access-policies/__tests__/index.test.tsx index 4729aba1715..aaa448b47c9 100644 --- a/ui/apps/console/src/pages/access-policies/__tests__/index.test.tsx +++ b/ui/apps/console/src/pages/access-policies/__tests__/index.test.tsx @@ -5,7 +5,7 @@ import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse } from "@/tests/sdk"; import { mockAccessPolicy, mockNamespace } from "@/tests/factories"; import { seedAuthStore } from "@/tests/seedAuthStore"; -import type { AccessPolicy } from "@/client"; +import type { AccessPolicy } from "@/client/model"; import AccessPolicies from "../index"; const sdk = vi.hoisted(() => diff --git a/ui/apps/console/src/pages/access-policies/index.tsx b/ui/apps/console/src/pages/access-policies/index.tsx index a05772d83b3..e11f79662f6 100644 --- a/ui/apps/console/src/pages/access-policies/index.tsx +++ b/ui/apps/console/src/pages/access-policies/index.tsx @@ -22,12 +22,14 @@ import { IconButton, } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; -import { useAccessPolicies } from "@/hooks/useAccessPolicies"; -import { useDeleteAccessPolicy } from "@/hooks/useAccessPolicyMutations"; +import { + useListAccessPolicies, + useListServiceAccounts, + useDeleteAccessPolicy, +} from "@/client/api"; import { useNamespace } from "@/hooks/useNamespaces"; -import { useServiceAccounts } from "@/hooks/useServiceAccounts"; import { useAuthStore } from "@/stores/authStore"; -import type { AccessPolicy } from "@/client"; +import type { AccessPolicy } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import EmptyState from "@/components/common/EmptyState"; import ConfirmDialog from "@/components/common/ConfirmDialog"; @@ -205,10 +207,10 @@ function ActionCell({ policy }: { policy: AccessPolicy }) { * firewall pages in namespaces using identity access mode. */ export default function AccessPolicies() { - const { policies, isLoading } = useAccessPolicies(); + const { data: policies = [], isLoading } = useListAccessPolicies(); const { tenant: tenantId } = useAuthStore(); const { namespace: ns } = useNamespace(tenantId ?? ""); - const { serviceAccounts } = useServiceAccounts(); + const { data: serviceAccounts = [] } = useListServiceAccounts(); const isIdentityMode = ns?.settings?.ssh_access_mode === "identity"; const members = ns?.members ?? []; @@ -237,7 +239,7 @@ export default function AccessPolicies() { if (!deleteTarget) return; setDeleteError(null); try { - await deletePolicy.mutateAsync({ path: { id: deleteTarget.id } }); + await deletePolicy.mutateAsync({ id: deleteTarget.id }); closeDelete(); } catch (err) { setDeleteError( diff --git a/ui/apps/console/src/pages/admin/Dashboard.tsx b/ui/apps/console/src/pages/admin/Dashboard.tsx index 9ee346592fe..810fbb4e3dd 100644 --- a/ui/apps/console/src/pages/admin/Dashboard.tsx +++ b/ui/apps/console/src/pages/admin/Dashboard.tsx @@ -12,7 +12,7 @@ import { import PageHeader from "@/components/common/PageHeader"; import StatCard from "@/components/common/StatCard"; import RecentSessionsTable from "@/components/sessions/RecentSessionsTable"; -import { useAdminStats } from "@/hooks/useAdminStats"; +import { useGetStats } from "@/client/api"; import PageLoader from "@/components/common/PageLoader"; /** @@ -20,10 +20,12 @@ import PageLoader from "@/components/common/PageLoader"; */ export default function AdminDashboard() { const { - stats: statsData, + data: statsData, isLoading: statsLoading, isError: statsError, - } = useAdminStats(); + } = useGetStats({ + query: { staleTime: 5 * 60_000, refetchOnWindowFocus: false, retry: 1 }, + }); if (statsLoading) { return ; diff --git a/ui/apps/console/src/pages/admin/License.tsx b/ui/apps/console/src/pages/admin/License.tsx index 3dcf3a929f5..446d04848f4 100644 --- a/ui/apps/console/src/pages/admin/License.tsx +++ b/ui/apps/console/src/pages/admin/License.tsx @@ -16,7 +16,7 @@ import { cn } from "@shellhub/design-system/cn"; import PageHeader from "@/components/common/PageHeader"; import CopyButton from "@/components/common/CopyButton"; import { useAdminLicense } from "@/hooks/useAdminLicense"; -import { useUploadLicense } from "@/hooks/useUploadLicense"; +import { useSendLicense } from "@/client/api"; import { formatLicenseTimestamp, formatDeviceCount, @@ -25,7 +25,7 @@ import { validateLicenseFile, getLicenseAlertConfig, } from "@/utils/license"; -import type { GetLicenseResponse } from "@/client"; +import type { GetLicense200 as GetLicenseResponse } from "@/client/model"; import PageLoader from "@/components/common/PageLoader"; import { Button, Card, IconButton } from "@shellhub/design-system/primitives"; @@ -202,7 +202,7 @@ function LicenseFeatures({ } function LicenseUpload() { - const upload = useUploadLicense(); + const upload = useSendLicense(); const [file, setFile] = useState(null); const [validationError, setValidationError] = useState(null); const [feedback, setFeedback] = useState<{ @@ -242,7 +242,7 @@ function LicenseUpload() { if (!file || validationError) return; setFeedback(null); try { - await upload.mutateAsync({ body: { file } }); + await upload.mutateAsync({ data: { file } }); setFeedback({ type: "success", message: "License uploaded successfully.", diff --git a/ui/apps/console/src/pages/admin/SessionDetails.tsx b/ui/apps/console/src/pages/admin/SessionDetails.tsx index f99a8d4614a..03f69e81b09 100644 --- a/ui/apps/console/src/pages/admin/SessionDetails.tsx +++ b/ui/apps/console/src/pages/admin/SessionDetails.tsx @@ -5,7 +5,7 @@ import { MinusCircleIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminSessionDetail } from "@/hooks/useAdminSessionDetail"; +import { useGetSessionAdmin } from "@/client/api"; import Breadcrumb from "@/components/common/Breadcrumb"; import InfoItem from "@/components/common/InfoItem"; import { formatDateFull } from "@/utils/date"; @@ -44,7 +44,13 @@ function BoolField({ */ export default function AdminSessionDetails() { const { uid = "" } = useParams<{ uid: string }>(); - const { session, isLoading, error } = useAdminSessionDetail(uid); + const { + data: session, + isLoading, + error, + } = useGetSessionAdmin(uid, { + query: { staleTime: 60_000, refetchOnWindowFocus: false, retry: 1 }, + }); if (isLoading) { return ; diff --git a/ui/apps/console/src/pages/admin/Sessions.tsx b/ui/apps/console/src/pages/admin/Sessions.tsx index e86497f89ac..facf1772182 100644 --- a/ui/apps/console/src/pages/admin/Sessions.tsx +++ b/ui/apps/console/src/pages/admin/Sessions.tsx @@ -7,8 +7,9 @@ import { } from "@heroicons/react/24/outline"; import { Callout } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminSessions } from "@/hooks/useAdminSessions"; -import type { Session } from "@/client"; +import { useGetSessionsAdmin } from "@/client/api"; +import { totalCount } from "@/api/pagination"; +import type { Session } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import DataTable, { type Column } from "@/components/common/DataTable"; import DeviceChip from "@/components/common/DeviceChip"; @@ -30,13 +31,18 @@ export default function AdminSessions() { const { params, setPage } = usePaginatedListState({ defaults: DEFAULTS, }); - const { sessions, totalCount, isLoading, error } = useAdminSessions({ + const { + data: sessions = [], + isLoading, + error, + } = useGetSessionsAdmin({ page: params.page, - perPage: PER_PAGE, + per_page: PER_PAGE, }); + const total = totalCount(sessions); const navigate = useNavigate(); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -180,7 +186,7 @@ export default function AdminSessions() { loadingMessage="Loading sessions..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="session" onPageChange={setPage} onRowClick={(s) => void navigate(`/admin/sessions/${s.uid}`)} diff --git a/ui/apps/console/src/pages/admin/announcements/AnnouncementDetails.tsx b/ui/apps/console/src/pages/admin/announcements/AnnouncementDetails.tsx index 29cfa737bde..16d6e16a016 100644 --- a/ui/apps/console/src/pages/admin/announcements/AnnouncementDetails.tsx +++ b/ui/apps/console/src/pages/admin/announcements/AnnouncementDetails.tsx @@ -6,7 +6,7 @@ import { PencilSquareIcon, TrashIcon, } from "@heroicons/react/24/outline"; -import { useAdminAnnouncement } from "@/hooks/useAdminAnnouncements"; +import { useGetAnnouncementAdmin } from "@/client/api"; import Breadcrumb from "@/components/common/Breadcrumb"; import CopyButton from "@/components/common/CopyButton"; import DeleteAnnouncementDialog from "./DeleteAnnouncementDialog"; @@ -23,13 +23,13 @@ const LABEL = * One announcement as published, with the ways to edit or delete it. */ export default function AnnouncementDetails() { - const { uuid } = useParams<{ uuid: string }>(); + const { uuid = "" } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const { data: announcement, isLoading, error, - } = useAdminAnnouncement(uuid ?? ""); + } = useGetAnnouncementAdmin(uuid, { query: { enabled: !!uuid } }); const [deleteOpen, setDeleteOpen] = useState(false); if (isLoading) { diff --git a/ui/apps/console/src/pages/admin/announcements/DeleteAnnouncementDialog.tsx b/ui/apps/console/src/pages/admin/announcements/DeleteAnnouncementDialog.tsx index 474278d6df3..760a53b289a 100644 --- a/ui/apps/console/src/pages/admin/announcements/DeleteAnnouncementDialog.tsx +++ b/ui/apps/console/src/pages/admin/announcements/DeleteAnnouncementDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useAdminDeleteAnnouncement } from "@/hooks/useAdminAnnouncementMutations"; +import { useDeleteAnnouncement } from "@/client/api"; import ConfirmDialog from "@/components/common/ConfirmDialog"; interface DeleteAnnouncementDialogProps { @@ -19,7 +19,7 @@ export default function DeleteAnnouncementDialog({ announcement, onDeleted, }: DeleteAnnouncementDialogProps) { - const deleteAnnouncement = useAdminDeleteAnnouncement(); + const deleteAnnouncement = useDeleteAnnouncement(); const [error, setError] = useState(""); return ( @@ -34,7 +34,7 @@ export default function DeleteAnnouncementDialog({ setError(""); try { await deleteAnnouncement.mutateAsync({ - path: { uuid: announcement.uuid }, + uuid: announcement.uuid, }); onClose(); onDeleted?.(); diff --git a/ui/apps/console/src/pages/admin/announcements/EditAnnouncement.tsx b/ui/apps/console/src/pages/admin/announcements/EditAnnouncement.tsx index 209026ce06f..75e2ad6cdad 100644 --- a/ui/apps/console/src/pages/admin/announcements/EditAnnouncement.tsx +++ b/ui/apps/console/src/pages/admin/announcements/EditAnnouncement.tsx @@ -3,8 +3,7 @@ import { Link, useNavigate, useParams } from "react-router-dom"; import { useForm, useController, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { MegaphoneIcon } from "@heroicons/react/24/outline"; -import { useAdminAnnouncement } from "@/hooks/useAdminAnnouncements"; -import { useAdminUpdateAnnouncement } from "@/hooks/useAdminAnnouncementMutations"; +import { useGetAnnouncementAdmin, useUpdateAnnouncement } from "@/client/api"; import AnnouncementEditor from "./AnnouncementEditor"; import Breadcrumb from "@/components/common/Breadcrumb"; import { FormInputField } from "@/components/common/fields/rhf"; @@ -22,14 +21,14 @@ import { * Edits an existing announcement. Changes are live as soon as they are saved. */ export default function EditAnnouncement() { - const { uuid } = useParams<{ uuid: string }>(); + const { uuid = "" } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const { data: announcement, isLoading: isFetching, error: fetchError, - } = useAdminAnnouncement(uuid ?? ""); - const updateAnnouncement = useAdminUpdateAnnouncement(); + } = useGetAnnouncementAdmin(uuid, { query: { enabled: !!uuid } }); + const updateAnnouncement = useUpdateAnnouncement(); const values = useMemo( () => ({ @@ -62,8 +61,8 @@ export default function EditAnnouncement() { clearErrors("root"); try { await updateAnnouncement.mutateAsync({ - path: { uuid }, - body: buildAnnouncementBody(formValues), + uuid, + data: buildAnnouncementBody(formValues), }); void navigate(`/admin/announcements/${uuid}`); } catch { diff --git a/ui/apps/console/src/pages/admin/announcements/NewAnnouncement.tsx b/ui/apps/console/src/pages/admin/announcements/NewAnnouncement.tsx index b2ae95203be..636ba894876 100644 --- a/ui/apps/console/src/pages/admin/announcements/NewAnnouncement.tsx +++ b/ui/apps/console/src/pages/admin/announcements/NewAnnouncement.tsx @@ -1,7 +1,7 @@ import { Link, useNavigate } from "react-router-dom"; import { useForm, useController, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useAdminCreateAnnouncement } from "@/hooks/useAdminAnnouncementMutations"; +import { useCreateAnnouncement } from "@/client/api"; import AnnouncementEditor from "./AnnouncementEditor"; import Breadcrumb from "@/components/common/Breadcrumb"; import { FormInputField } from "@/components/common/fields/rhf"; @@ -19,7 +19,7 @@ import { */ export default function NewAnnouncement() { const navigate = useNavigate(); - const createAnnouncement = useAdminCreateAnnouncement(); + const createAnnouncement = useCreateAnnouncement(); const form = useForm({ mode: "onChange", @@ -42,7 +42,7 @@ export default function NewAnnouncement() { clearErrors("root"); try { await createAnnouncement.mutateAsync({ - body: buildAnnouncementBody(values), + data: buildAnnouncementBody(values), }); void navigate("/admin/announcements"); } catch { diff --git a/ui/apps/console/src/pages/admin/announcements/__tests__/AdminAnnouncements.test.tsx b/ui/apps/console/src/pages/admin/announcements/__tests__/AdminAnnouncements.test.tsx index 975e62deb93..6fa350497d9 100644 --- a/ui/apps/console/src/pages/admin/announcements/__tests__/AdminAnnouncements.test.tsx +++ b/ui/apps/console/src/pages/admin/announcements/__tests__/AdminAnnouncements.test.tsx @@ -3,7 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import AdminAnnouncements from "../index"; -import type { AnnouncementShort } from "@/client"; +import type { AnnouncementShort } from "@/client/model"; import { makeSdkError, paginatedResponse } from "@/tests/sdk"; import { createTestWrapper } from "@/tests/wrapper"; import { mockAnnouncement } from "@/tests/factories"; diff --git a/ui/apps/console/src/pages/admin/announcements/index.tsx b/ui/apps/console/src/pages/admin/announcements/index.tsx index 716b6372577..888b793ca21 100644 --- a/ui/apps/console/src/pages/admin/announcements/index.tsx +++ b/ui/apps/console/src/pages/admin/announcements/index.tsx @@ -6,12 +6,13 @@ import { PencilSquareIcon, PlusIcon, } from "@heroicons/react/24/outline"; -import { useAdminAnnouncements } from "@/hooks/useAdminAnnouncements"; +import { useListAnnouncementsAdmin } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import PageHeader from "@/components/common/PageHeader"; import DataTable, { type Column } from "@/components/common/DataTable"; import DeleteAnnouncementDialog from "./DeleteAnnouncementDialog"; import { formatDateShort } from "@/utils/date"; -import type { AnnouncementShort } from "@/client"; +import type { AnnouncementShort } from "@/client/model"; import { Badge, Button, @@ -40,14 +41,18 @@ export default function AdminAnnouncements() { null, ); - const { announcements, totalCount, isLoading, error } = useAdminAnnouncements( - { - page: params.page, - perPage: PER_PAGE, - }, - ); + const { + data: announcements = [], + isLoading, + error, + } = useListAnnouncementsAdmin({ + page: params.page, + per_page: PER_PAGE, + order_by: "desc", + }); + const total = totalCount(announcements); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -142,7 +147,7 @@ export default function AdminAnnouncements() { loadingMessage="Loading announcements..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="announcement" onPageChange={setPage} onRowClick={(a) => void navigate(`/admin/announcements/${a.uuid}`)} diff --git a/ui/apps/console/src/pages/admin/devices/AdminDeviceDetails.tsx b/ui/apps/console/src/pages/admin/devices/AdminDeviceDetails.tsx index baa9d418130..9d54c712475 100644 --- a/ui/apps/console/src/pages/admin/devices/AdminDeviceDetails.tsx +++ b/ui/apps/console/src/pages/admin/devices/AdminDeviceDetails.tsx @@ -7,7 +7,8 @@ import { KeyIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminDevice } from "@/hooks/useAdminDevices"; +import { useGetDeviceAdmin } from "@/client/api"; +import { normalizeDeviceTags } from "@/utils/deviceTags"; import Breadcrumb from "@/components/common/Breadcrumb"; import DistroIcon from "@/components/common/DistroIcon"; import PlatformBadge from "@/components/common/PlatformBadge"; @@ -23,8 +24,14 @@ import { Card } from "@shellhub/design-system/primitives"; * One device, seen from the admin area, including which namespace it belongs to. */ export default function AdminDeviceDetails() { - const { uid } = useParams<{ uid: string }>(); - const { data: device, isLoading, error } = useAdminDevice(uid ?? ""); + const { uid = "" } = useParams<{ uid: string }>(); + const { + data: device, + isLoading, + error, + } = useGetDeviceAdmin(uid, { + query: { enabled: !!uid, select: normalizeDeviceTags }, + }); if (isLoading) { return ; diff --git a/ui/apps/console/src/pages/admin/devices/DeviceStatusChip.tsx b/ui/apps/console/src/pages/admin/devices/DeviceStatusChip.tsx index 9a87b9ee823..1cce27eab26 100644 --- a/ui/apps/console/src/pages/admin/devices/DeviceStatusChip.tsx +++ b/ui/apps/console/src/pages/admin/devices/DeviceStatusChip.tsx @@ -5,7 +5,7 @@ import { MinusCircleIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import type { DeviceStatus } from "@/client"; +import type { DeviceStatus } from "@/client/model"; const STATUS_CONFIG: Record< DeviceStatus, diff --git a/ui/apps/console/src/pages/admin/devices/__tests__/AdminDeviceDetails.test.tsx b/ui/apps/console/src/pages/admin/devices/__tests__/AdminDeviceDetails.test.tsx index e2d1635f5bf..841f3eee921 100644 --- a/ui/apps/console/src/pages/admin/devices/__tests__/AdminDeviceDetails.test.tsx +++ b/ui/apps/console/src/pages/admin/devices/__tests__/AdminDeviceDetails.test.tsx @@ -5,7 +5,7 @@ import { createTestWrapper } from "@/tests/wrapper"; import { useAuthStore } from "@/stores/authStore"; import { mockSdkResponse, makeSdkError } from "@/tests/sdk"; import AdminDeviceDetails from "../AdminDeviceDetails"; -import type { Device } from "@/client"; +import type { Device } from "@/client/model"; const sdk = vi.hoisted(() => mockSdkGen({ diff --git a/ui/apps/console/src/pages/admin/devices/index.tsx b/ui/apps/console/src/pages/admin/devices/index.tsx index 2458f1ec6ad..503bc1fcf63 100644 --- a/ui/apps/console/src/pages/admin/devices/index.tsx +++ b/ui/apps/console/src/pages/admin/devices/index.tsx @@ -1,14 +1,15 @@ import { useNavigate, Link } from "react-router-dom"; -import { - CpuChipIcon, -} from "@heroicons/react/24/outline"; +import { CpuChipIcon } from "@heroicons/react/24/outline"; import { Callout } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; +import { useGetDevicesAdmin } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { - useAdminDevices, - type NormalizedDevice, -} from "@/hooks/useAdminDevices"; -import type { DeviceStatus } from "@/client"; + normalizeDeviceTags, + type TaggedDevice as NormalizedDevice, +} from "@/utils/deviceTags"; +import { toBase64Json } from "@/utils/encoding"; +import type { DeviceStatus, GetDevicesAdminParams } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import DataTable, { type Column } from "@/components/common/DataTable"; import SearchField from "@/components/common/fields/SearchField"; @@ -50,7 +51,7 @@ const SORT_FIELDS = [ { field: "status", initialOrder: "desc" as const }, ]; -type SortField = typeof VALID_SORT_FIELDS[number]; +type SortField = (typeof VALID_SORT_FIELDS)[number]; type AdminDevicesParams = { page: number; @@ -104,16 +105,30 @@ export default function AdminDevices() { const debouncedSearch = useDebouncedValue(params.search, SEARCH_DEBOUNCE_MS); - const { devices, totalCount, isLoading, error } = useAdminDevices({ + const requestParams: GetDevicesAdminParams = { page: params.page, - perPage: PER_PAGE, - search: debouncedSearch, - status: params.status, - sortBy: params.sortField, - orderBy: params.sortOrder, - }); + per_page: PER_PAGE, + sort_by: params.sortField, + order_by: params.sortOrder, + }; + if (debouncedSearch) { + requestParams.filter = toBase64Json([ + { + type: "property", + params: { name: "name", operator: "contains", value: debouncedSearch }, + }, + ]); + } + if (params.status) requestParams.status = params.status; + const { + data: rawDevices = [], + isLoading, + error, + } = useGetDevicesAdmin(requestParams); + const devices = rawDevices.map(normalizeDeviceTags); + const total = totalCount(rawDevices); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -220,7 +235,12 @@ export default function AdminDevices() { role="tab" aria-selected={params.status === tab.value} onClick={() => setFilter("status", tab.value)} - className={cn("h-full px-3.5 text-xs font-medium rounded transition-all duration-150", params.status === tab.value ? "bg-primary/15 text-primary border border-primary/25" : "text-text-muted hover:text-text-secondary border border-transparent")} + className={cn( + "h-full px-3.5 text-xs font-medium rounded transition-all duration-150", + params.status === tab.value + ? "bg-primary/15 text-primary border border-primary/25" + : "text-text-muted hover:text-text-secondary border border-transparent", + )} > {tab.label} @@ -249,7 +269,7 @@ export default function AdminDevices() { loadingMessage="Loading devices..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="device" onPageChange={setPage} onRowClick={(device) => void navigate(`/admin/devices/${device.uid}`)} diff --git a/ui/apps/console/src/pages/admin/firewall-rules/AdminFirewallRuleDetails.tsx b/ui/apps/console/src/pages/admin/firewall-rules/AdminFirewallRuleDetails.tsx index aed6eb5e519..44a8bb3285d 100644 --- a/ui/apps/console/src/pages/admin/firewall-rules/AdminFirewallRuleDetails.tsx +++ b/ui/apps/console/src/pages/admin/firewall-rules/AdminFirewallRuleDetails.tsx @@ -7,7 +7,7 @@ import { NoSymbolIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminFirewallRule } from "@/hooks/useAdminFirewallRules"; +import { useGetFirewallRuleAdmin } from "@/client/api"; import ActiveBadge from "@/components/common/ActiveBadge"; import Breadcrumb from "@/components/common/Breadcrumb"; import CopyButton from "@/components/common/CopyButton"; @@ -21,8 +21,12 @@ import { Card } from "@shellhub/design-system/primitives"; * One firewall rule, seen from the admin area. */ export default function AdminFirewallRuleDetails() { - const { id } = useParams<{ id: string }>(); - const { data: rule, isLoading, error } = useAdminFirewallRule(id ?? ""); + const { id = "" } = useParams<{ id: string }>(); + const { + data: rule, + isLoading, + error, + } = useGetFirewallRuleAdmin(id, { query: { enabled: !!id } }); if (isLoading) { return ; diff --git a/ui/apps/console/src/pages/admin/firewall-rules/__tests__/AdminFirewallRuleDetails.test.tsx b/ui/apps/console/src/pages/admin/firewall-rules/__tests__/AdminFirewallRuleDetails.test.tsx index 69b55ee0860..bbbb4a59708 100644 --- a/ui/apps/console/src/pages/admin/firewall-rules/__tests__/AdminFirewallRuleDetails.test.tsx +++ b/ui/apps/console/src/pages/admin/firewall-rules/__tests__/AdminFirewallRuleDetails.test.tsx @@ -4,7 +4,7 @@ import { MemoryRouter } from "react-router-dom"; import { createTestWrapper } from "@/tests/wrapper"; import { useAuthStore } from "@/stores/authStore"; import { mockSdkResponse, makeSdkError } from "@/tests/sdk"; -import type { FirewallRulesResponse } from "@/client"; +import type { FirewallRulesResponse } from "@/client/model"; import AdminFirewallRuleDetails from "../AdminFirewallRuleDetails"; const sdk = vi.hoisted(() => diff --git a/ui/apps/console/src/pages/admin/firewall-rules/index.tsx b/ui/apps/console/src/pages/admin/firewall-rules/index.tsx index 3714b0d67df..36c5afaae2b 100644 --- a/ui/apps/console/src/pages/admin/firewall-rules/index.tsx +++ b/ui/apps/console/src/pages/admin/firewall-rules/index.tsx @@ -1,4 +1,3 @@ -import { useMemo } from "react"; import { useNavigate, Link } from "react-router-dom"; import { ShieldExclamationIcon, @@ -10,9 +9,10 @@ import DataTable, { type Column } from "@/components/common/DataTable"; import FilterBadge from "@/components/common/FilterBadge"; import PageHeader from "@/components/common/PageHeader"; import SearchField from "@/components/common/fields/SearchField"; -import { useAdminFirewallRules } from "@/hooks/useAdminFirewallRules"; +import { useGetFirewallRulesAdmin } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import { type FirewallRulesResponse as FirewallRule } from "@/client"; +import { type FirewallRulesResponse as FirewallRule } from "@/client/model"; import { Badge, Callout } from "@shellhub/design-system/primitives"; import { apiErrorMessage } from "@/api/errors"; import { PER_PAGE, pageCount } from "@/utils/pagination"; @@ -35,24 +35,29 @@ export default function AdminFirewallRules() { const { params, setPage, setSearch } = usePaginatedListState({ defaults: DEFAULTS }); - const { rules, totalCount, isLoading, error } = useAdminFirewallRules({ + const { + data: rules = [], + isLoading, + error, + } = useGetFirewallRulesAdmin({ page: params.page, - perPage: PER_PAGE, + per_page: PER_PAGE, }); + const total = totalCount(rules); - const filtered = useMemo(() => { - if (!params.search) return rules; - const q = params.search.toLowerCase(); - return rules.filter( - (r) => - r.action.toLowerCase().includes(q) || - r.source_ip.toLowerCase().includes(q) || - r.username.toLowerCase().includes(q) || - String(r.priority).includes(q), - ); - }, [rules, params.search]); + const filtered = params.search + ? rules.filter((r) => { + const q = params.search.toLowerCase(); + return ( + r.action.toLowerCase().includes(q) || + r.source_ip.toLowerCase().includes(q) || + r.username.toLowerCase().includes(q) || + String(r.priority).includes(q) + ); + }) + : rules; - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -164,7 +169,7 @@ export default function AdminFirewallRules() { {...(!params.search && { page: params.page, totalPages, - totalCount, + totalCount: total, itemLabel: "rule", onPageChange: setPage, })} diff --git a/ui/apps/console/src/pages/admin/instance-api-keys/GenerateInstanceKeyDrawer.tsx b/ui/apps/console/src/pages/admin/instance-api-keys/GenerateInstanceKeyDrawer.tsx index 620a6048059..77578470194 100644 --- a/ui/apps/console/src/pages/admin/instance-api-keys/GenerateInstanceKeyDrawer.tsx +++ b/ui/apps/console/src/pages/admin/instance-api-keys/GenerateInstanceKeyDrawer.tsx @@ -3,7 +3,7 @@ import { KeyIcon, CheckIcon } from "@heroicons/react/24/outline"; import { Card, Button } from "@shellhub/design-system/primitives"; import { isSdkError } from "@/api/errors"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; -import { useCreateInstanceApiKey } from "@/hooks/useInstanceApiKeyMutations"; +import { useCreateInstanceAPIKey } from "@/client/api"; import CopyButton from "@/components/common/CopyButton"; import Drawer from "@/components/common/Drawer"; import { @@ -33,7 +33,7 @@ function GenerateInstanceKeyDrawer({ open: boolean; onClose: () => void; }) { - const createKey = useCreateInstanceApiKey(); + const createKey = useCreateInstanceAPIKey(); const form = useDrawerForm( open, generateInstanceKeySchema, @@ -55,7 +55,7 @@ function GenerateInstanceKeyDrawer({ clearErrors("root"); try { const result = await createKey.mutateAsync({ - body: buildGenerateInstanceKeyBody({ + data: buildGenerateInstanceKeyBody({ name: values.name, expiresAt: values.expiresAt, }), diff --git a/ui/apps/console/src/pages/admin/instance-api-keys/InstanceApiKeys.tsx b/ui/apps/console/src/pages/admin/instance-api-keys/InstanceApiKeys.tsx index 531aac4dd3e..bce2fe4d11e 100644 --- a/ui/apps/console/src/pages/admin/instance-api-keys/InstanceApiKeys.tsx +++ b/ui/apps/console/src/pages/admin/instance-api-keys/InstanceApiKeys.tsx @@ -2,10 +2,10 @@ import { useState } from "react"; import { KeyIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Button, IconButton } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; -import { useInstanceApiKeys } from "@/hooks/useInstanceApiKeys"; -import { useDeleteInstanceApiKey } from "@/hooks/useInstanceApiKeyMutations"; +import { useListInstanceAPIKeys, useDeleteInstanceAPIKey } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import { type InstanceApiKey } from "@/client"; +import type { InstanceAPIKey } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import DataTable, { type Column } from "@/components/common/DataTable"; @@ -13,11 +13,11 @@ import { formatDateShort } from "@/utils/date"; import { pageCount } from "@/utils/pagination"; import GenerateInstanceKeyDrawer from "./GenerateInstanceKeyDrawer"; -type InstanceApiKeyListParams = { +type InstanceAPIKeyListParams = { page: number; }; -const INSTANCE_API_KEY_LIST_DEFAULTS: InstanceApiKeyListParams = { page: 1 }; +const INSTANCE_API_KEY_LIST_DEFAULTS: InstanceAPIKeyListParams = { page: 1 }; function hasExpired(expiresAt: string) { return new Date(expiresAt).getTime() <= Date.now(); @@ -28,16 +28,19 @@ function hasExpired(expiresAt: string) { * administrator rather than as a member of a namespace, so they are managed here rather than * alongside a namespace's own keys. */ -function InstanceApiKeys() { - const { params, setPage } = usePaginatedListState({ +function InstanceAPIKeys() { + const { params, setPage } = usePaginatedListState({ defaults: INSTANCE_API_KEY_LIST_DEFAULTS, }); const page = params.page; - const { apiKeys, totalCount, isLoading } = useInstanceApiKeys({ page }); + const keysQuery = useListInstanceAPIKeys({ page, per_page: 10, order_by: "desc" }); + const apiKeys = keysQuery.data ?? []; + const keyCount = totalCount(keysQuery.data); + const isLoading = keysQuery.isLoading; - const deleteKey = useDeleteInstanceApiKey(); + const deleteKey = useDeleteInstanceAPIKey(); const [generateOpen, setGenerateOpen] = useState(false); - const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); const [deleteError, setDeleteError] = useState(null); const closeDelete = () => { @@ -49,7 +52,7 @@ function InstanceApiKeys() { if (!deleteTarget) return; setDeleteError(null); try { - await deleteKey.mutateAsync({ path: { name: deleteTarget.name } }); + await deleteKey.mutateAsync({ name: deleteTarget.name }); if (apiKeys.length === 1 && page > 1) setPage(page - 1); closeDelete(); } catch (err) { @@ -61,7 +64,7 @@ function InstanceApiKeys() { } }; - const columns: Column[] = [ + const columns: Column[] = [ { key: "name", header: "Name", @@ -133,7 +136,7 @@ function InstanceApiKeys() {

- {totalCount} key{totalCount !== 1 ? "s" : ""} + {keyCount} key{keyCount !== 1 ? "s" : ""}

@@ -144,7 +147,7 @@ function InstanceApiKeys() { isLoading={isLoading} loadingMessage="Loading instance API keys..." page={page} - totalPages={pageCount(totalCount)} + totalPages={pageCount(keyCount)} onPageChange={setPage} rowClassName={(key) => hasExpired(key.expires_at) @@ -190,4 +193,4 @@ function InstanceApiKeys() { ); } -export default InstanceApiKeys; +export default InstanceAPIKeys; diff --git a/ui/apps/console/src/pages/admin/instance-api-keys/__tests__/InstanceApiKeys.test.tsx b/ui/apps/console/src/pages/admin/instance-api-keys/__tests__/InstanceApiKeys.test.tsx index 5ea455c331e..3d259150066 100644 --- a/ui/apps/console/src/pages/admin/instance-api-keys/__tests__/InstanceApiKeys.test.tsx +++ b/ui/apps/console/src/pages/admin/instance-api-keys/__tests__/InstanceApiKeys.test.tsx @@ -2,16 +2,16 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import InstanceApiKeys from "../InstanceApiKeys"; -import type { InstanceApiKey } from "@/client"; +import type { InstanceAPIKey } from "@/client/model"; import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse, paginatedResponse } from "@/tests/sdk"; import { ClipboardProvider } from "@/components/common/ClipboardProvider"; const sdk = vi.hoisted(() => mockSdkGen({ - listInstanceApiKeys: vi.fn(), - createInstanceApiKey: vi.fn(), - deleteInstanceApiKey: vi.fn(), + listInstanceAPIKeys: vi.fn(), + createInstanceAPIKey: vi.fn(), + deleteInstanceAPIKey: vi.fn(), }), ); @@ -19,9 +19,9 @@ vi.mock("@/components/common/ConfirmDialog", async () => ({ default: (await import("@/tests/mocks")).MockConfirmDialog, })); -function mockInstanceApiKey( - overrides: Partial = {}, -): InstanceApiKey { +function mockInstanceAPIKey( + overrides: Partial = {}, +): InstanceAPIKey { return { name: "billing-export", created_by: "3dd0d1f8-8246-4519-b11a-a3dd33717f65", @@ -34,10 +34,10 @@ function mockInstanceApiKey( beforeEach(() => { vi.clearAllMocks(); - sdk.listInstanceApiKeys.mockResolvedValue( - paginatedResponse([mockInstanceApiKey()]), + sdk.listInstanceAPIKeys.mockResolvedValue( + paginatedResponse([mockInstanceAPIKey()]), ); - sdk.deleteInstanceApiKey.mockResolvedValue(mockSdkResponse(undefined)); + sdk.deleteInstanceAPIKey.mockResolvedValue(mockSdkResponse(undefined)); }); function renderPage() { @@ -62,9 +62,9 @@ describe("InstanceApiKeys", () => { it("shows the plaintext key once after creating one", async () => { const user = userEvent.setup(); - sdk.createInstanceApiKey.mockResolvedValue( + sdk.createInstanceAPIKey.mockResolvedValue( mockSdkResponse({ - ...mockInstanceApiKey({ name: "license-sync" }), + ...mockInstanceAPIKey({ name: "license-sync" }), id: "sh_admin_cdfd3cb0-c44e-4e54-b931-6d57713ad159", }), ); @@ -126,7 +126,7 @@ describe("InstanceApiKeys", () => { await user.click(screen.getByRole("button", { name: /^revoke$/i })); await waitFor(() => { - expect(sdk.deleteInstanceApiKey).toHaveBeenCalledWith( + expect(sdk.deleteInstanceAPIKey).toHaveBeenCalledWith( expect.objectContaining({ path: { name: "billing-export" } }), ); }); diff --git a/ui/apps/console/src/pages/admin/namespaces/DeleteNamespaceDialog.tsx b/ui/apps/console/src/pages/admin/namespaces/DeleteNamespaceDialog.tsx index 436e3b1870b..5cc4bd75d99 100644 --- a/ui/apps/console/src/pages/admin/namespaces/DeleteNamespaceDialog.tsx +++ b/ui/apps/console/src/pages/admin/namespaces/DeleteNamespaceDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useAdminDeleteNamespace } from "@/hooks/useAdminNamespaceMutations"; +import { useDeleteNamespaceAdmin } from "@/client/api"; import ConfirmDialog from "@/components/common/ConfirmDialog"; interface DeleteNamespaceDialogProps { @@ -19,7 +19,7 @@ export default function DeleteNamespaceDialog({ namespace, onDeleted, }: DeleteNamespaceDialogProps) { - const deleteNamespace = useAdminDeleteNamespace(); + const deleteNamespace = useDeleteNamespaceAdmin(); const [error, setError] = useState(""); return ( @@ -34,7 +34,7 @@ export default function DeleteNamespaceDialog({ setError(""); try { await deleteNamespace.mutateAsync({ - path: { tenant: namespace.tenant_id }, + tenant: namespace.tenant_id, }); onClose(); onDeleted?.(); diff --git a/ui/apps/console/src/pages/admin/namespaces/EditNamespaceDrawer.tsx b/ui/apps/console/src/pages/admin/namespaces/EditNamespaceDrawer.tsx index 6ccde6e9f11..d52efdd1041 100644 --- a/ui/apps/console/src/pages/admin/namespaces/EditNamespaceDrawer.tsx +++ b/ui/apps/console/src/pages/admin/namespaces/EditNamespaceDrawer.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useAdminEditNamespace } from "@/hooks/useAdminNamespaceMutations"; +import { useEditNamespaceAdmin } from "@/client/api"; import { isSdkError } from "@/api/errors"; import FormDrawer from "@/components/common/FormDrawer"; import { @@ -18,7 +18,7 @@ import { buildEditNamespaceBody, type EditNamespaceFormValues, } from "./editNamespaceSchema"; -import type { Namespace } from "@/client"; +import type { Namespace } from "@/client/model"; interface EditNamespaceDrawerProps { open: boolean; @@ -35,7 +35,7 @@ export default function EditNamespaceDrawer({ onClose, namespace, }: EditNamespaceDrawerProps) { - const editNamespace = useAdminEditNamespace(); + const editNamespace = useEditNamespaceAdmin(); const schema = useMemo( () => editNamespaceSchema(namespace?.name ?? ""), @@ -54,8 +54,8 @@ export default function EditNamespaceDrawer({ clearErrors("root"); try { await editNamespace.mutateAsync({ - path: { tenantID: namespace.tenant_id }, - body: buildEditNamespaceBody(namespace, values), + tenantID: namespace.tenant_id, + data: buildEditNamespaceBody(namespace, values), }); onClose(); } catch (err) { diff --git a/ui/apps/console/src/pages/admin/namespaces/NamespaceDetails.tsx b/ui/apps/console/src/pages/admin/namespaces/NamespaceDetails.tsx index ea09f457679..e13435b9793 100644 --- a/ui/apps/console/src/pages/admin/namespaces/NamespaceDetails.tsx +++ b/ui/apps/console/src/pages/admin/namespaces/NamespaceDetails.tsx @@ -8,7 +8,8 @@ import { Cog6ToothIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminNamespace } from "@/hooks/useAdminNamespaces"; +import { useGetNamespaceAdmin } from "@/client/api"; +import { NamespaceMembersItem } from "@/client/model"; import Breadcrumb from "@/components/common/Breadcrumb"; import DataTable, { type Column } from "@/components/common/DataTable"; import EditNamespaceDrawer from "./EditNamespaceDrawer"; @@ -27,17 +28,17 @@ import { const ZERO_DATE = "0001-01-01T00:00:00Z"; -type Member = NonNullable< - NonNullable["data"]>["members"] ->[number]; - /** * One namespace, seen from the admin area: its members, its devices and its limits. */ export default function NamespaceDetails() { - const { id } = useParams<{ id: string }>(); + const { id = "" } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data: namespace, isLoading, error } = useAdminNamespace(id ?? ""); + const { + data: namespace, + isLoading, + error, + } = useGetNamespaceAdmin(id, { query: { enabled: !!id } }); const [editOpen, setEditOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); @@ -63,7 +64,7 @@ export default function NamespaceDetails() { (namespace.devices_pending_count || 0) + (namespace.devices_rejected_count || 0); - const memberColumns: Column[] = [ + const memberColumns: Column[] = [ { key: "email", header: "Email", @@ -235,7 +236,7 @@ export default function NamespaceDetails() { Members ({namespace.members?.length || 0}) - + columns={memberColumns} data={namespace.members ?? []} rowKey={(m, i) => m.id || m.email || `member-${i}`} diff --git a/ui/apps/console/src/pages/admin/namespaces/__tests__/EditNamespaceDrawer.test.tsx b/ui/apps/console/src/pages/admin/namespaces/__tests__/EditNamespaceDrawer.test.tsx index b8442156325..8150304e243 100644 --- a/ui/apps/console/src/pages/admin/namespaces/__tests__/EditNamespaceDrawer.test.tsx +++ b/ui/apps/console/src/pages/admin/namespaces/__tests__/EditNamespaceDrawer.test.tsx @@ -3,7 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse } from "@/tests/sdk"; -import type { Namespace } from "@/client"; +import type { Namespace } from "@/client/model"; import EditNamespaceDrawer from "../EditNamespaceDrawer"; const sdk = vi.hoisted(() => diff --git a/ui/apps/console/src/pages/admin/namespaces/editNamespaceSchema.ts b/ui/apps/console/src/pages/admin/namespaces/editNamespaceSchema.ts index b23e006afab..ed67dc961be 100644 --- a/ui/apps/console/src/pages/admin/namespaces/editNamespaceSchema.ts +++ b/ui/apps/console/src/pages/admin/namespaces/editNamespaceSchema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { validateNamespaceName } from "@/utils/validation"; -import type { Namespace } from "@/client"; +import type { Namespace } from "@/client/model"; const editNamespaceFields = z.object({ name: z.string(), diff --git a/ui/apps/console/src/pages/admin/namespaces/index.tsx b/ui/apps/console/src/pages/admin/namespaces/index.tsx index c8835f48589..a53e308d5e3 100644 --- a/ui/apps/console/src/pages/admin/namespaces/index.tsx +++ b/ui/apps/console/src/pages/admin/namespaces/index.tsx @@ -5,10 +5,13 @@ import { PencilSquareIcon, TrashIcon, } from "@heroicons/react/24/outline"; -import { useAdminNamespaces } from "@/hooks/useAdminNamespaces"; +import { useGetNamespacesAdmin } from "@/client/api"; +import type { GetNamespacesAdminParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; +import { toBase64Json } from "@/utils/encoding"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import type { Namespace } from "@/client"; +import type { Namespace } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import DataTable, { type Column } from "@/components/common/DataTable"; import SearchField from "@/components/common/fields/SearchField"; @@ -48,13 +51,26 @@ export default function AdminNamespaces() { const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); - const { namespaces, totalCount, isLoading, error } = useAdminNamespaces({ + const requestParams: GetNamespacesAdminParams = { page: params.page, - perPage: PER_PAGE, - search: debouncedSearch, - }); + per_page: PER_PAGE, + }; + if (debouncedSearch) { + requestParams.filter = toBase64Json([ + { + type: "property", + params: { name: "name", operator: "contains", value: debouncedSearch }, + }, + ]); + } + const { + data: namespaces = [], + isLoading, + error, + } = useGetNamespacesAdmin(requestParams); + const total = totalCount(namespaces); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -164,7 +180,7 @@ export default function AdminNamespaces() { loadingMessage="Loading namespaces..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="namespace" onPageChange={setPage} onRowClick={(ns) => void navigate(`/admin/namespaces/${ns.tenant_id}`)} diff --git a/ui/apps/console/src/pages/admin/settings/Authentication.tsx b/ui/apps/console/src/pages/admin/settings/Authentication.tsx index 671e7dcc374..5b0ad923369 100644 --- a/ui/apps/console/src/pages/admin/settings/Authentication.tsx +++ b/ui/apps/console/src/pages/admin/settings/Authentication.tsx @@ -6,9 +6,9 @@ import { import { getAuthenticationSettings, configureLocalAuthentication, - configureSamlAuthentication, -} from "@/client"; -import type { GetAuthenticationSettingsResponse } from "@/client"; + configureSAMLAuthentication, +} from "@/client/api"; +import type { GetAuthenticationSettings200 as GetAuthenticationSettingsResponse } from "@/client/model"; import { isSdkError } from "@/api/errors"; import PageHeader from "@/components/common/PageHeader"; import CopyButton from "@/components/common/CopyButton"; @@ -44,9 +44,7 @@ export default function AdminAuthentication() { let cancelled = false; void (async () => { try { - const { data } = await getAuthenticationSettings({ - throwOnError: true, - }); + const data = await getAuthenticationSettings(); if (!cancelled) setSettings(data); } catch { if (!cancelled) setError("Failed to load authentication settings."); @@ -63,10 +61,7 @@ export default function AdminAuthentication() { setTogglingLocal(true); setError(null); try { - await configureLocalAuthentication({ - body: { enable: !settings?.local?.enabled }, - throwOnError: true, - }); + await configureLocalAuthentication({ enable: !settings?.local?.enabled }); refresh(); } catch (err) { setError( @@ -87,13 +82,10 @@ export default function AdminAuthentication() { setTogglingSaml(true); setError(null); try { - await configureSamlAuthentication({ - body: { - enable: false, - idp: { entity_id: "", binding: {}, certificate: "" }, - sp: {}, - }, - throwOnError: true, + await configureSAMLAuthentication({ + enable: false, + idp: { entity_id: "", binding: {}, certificate: "" }, + sp: {}, }); refresh(); } catch (err) { diff --git a/ui/apps/console/src/pages/admin/settings/SamlConfigDrawer.tsx b/ui/apps/console/src/pages/admin/settings/SamlConfigDrawer.tsx index 1594a92f084..2b4ce306d3e 100644 --- a/ui/apps/console/src/pages/admin/settings/SamlConfigDrawer.tsx +++ b/ui/apps/console/src/pages/admin/settings/SamlConfigDrawer.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useWatch } from "react-hook-form"; import { KeyIcon, ChevronDownIcon, ExclamationCircleIcon } from "@heroicons/react/24/outline"; -import { configureSamlAuthentication } from "@/client"; +import { configureSAMLAuthentication } from "@/client/api"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; import { useDrawerForm } from "@/hooks/useDrawerForm"; import { cn } from "@shellhub/design-system/cn"; @@ -51,10 +51,7 @@ export default function SamlConfigDrawer({ const onSubmit = async (values: SamlFormValues) => { clearErrors("root"); try { - await configureSamlAuthentication({ - body: buildSamlBody(values), - throwOnError: true, - }); + await configureSAMLAuthentication(buildSamlBody(values)); onSaved(); onClose(); } catch { diff --git a/ui/apps/console/src/pages/admin/settings/samlSchema.ts b/ui/apps/console/src/pages/admin/settings/samlSchema.ts index 67cd35d9c56..2608cdd7d89 100644 --- a/ui/apps/console/src/pages/admin/settings/samlSchema.ts +++ b/ui/apps/console/src/pages/admin/settings/samlSchema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { GetAuthenticationSettingsResponse } from "@/client"; +import type { GetAuthenticationSettings200 as GetAuthenticationSettingsResponse } from "@/client/model"; /** * The stored SAML configuration, taken from the generated response type so the form and the API diff --git a/ui/apps/console/src/pages/admin/users/CreateUserDrawer.tsx b/ui/apps/console/src/pages/admin/users/CreateUserDrawer.tsx index 3828778fd2d..8620308e3a3 100644 --- a/ui/apps/console/src/pages/admin/users/CreateUserDrawer.tsx +++ b/ui/apps/console/src/pages/admin/users/CreateUserDrawer.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { PlusIcon } from "@heroicons/react/24/outline"; -import { useCreateUser } from "@/hooks/useAdminUserMutations"; +import { useCreateUserAdmin } from "@/client/api"; import { isSdkError } from "@/api/errors"; import FormDrawer from "@/components/common/FormDrawer"; import { useDrawerForm } from "@/hooks/useDrawerForm"; @@ -25,7 +25,7 @@ export default function CreateUserDrawer({ open, onClose, }: CreateUserDrawerProps) { - const createUser = useCreateUser(); + const createUser = useCreateUserAdmin(); const schema = useMemo(() => userSchema("create"), []); const defaults = useMemo(() => buildUserDefaults(), []); @@ -36,12 +36,15 @@ export default function CreateUserDrawer({ const onValid = async (values: UserFormValues) => { clearErrors("root"); try { - await createUser.mutateAsync({ body: buildUserPayload("create", values) }); + await createUser.mutateAsync({ + data: buildUserPayload("create", values), + }); onClose(); } catch (err) { - const message = isSdkError(err) && err.status === 409 - ? "A user with this email or username already exists." - : "Failed to create user. Please try again."; + const message = + isSdkError(err) && err.status === 409 + ? "A user with this email or username already exists." + : "Failed to create user. Please try again."; setError("root", { message }); } diff --git a/ui/apps/console/src/pages/admin/users/DeleteUserDialog.tsx b/ui/apps/console/src/pages/admin/users/DeleteUserDialog.tsx index 61f36931cc7..820ce0b5139 100644 --- a/ui/apps/console/src/pages/admin/users/DeleteUserDialog.tsx +++ b/ui/apps/console/src/pages/admin/users/DeleteUserDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useDeleteUser } from "@/hooks/useAdminUserMutations"; +import { useAdminDeleteUser } from "@/client/api"; import ConfirmDialog from "@/components/common/ConfirmDialog"; interface DeleteUserDialogProps { @@ -18,7 +18,7 @@ export default function DeleteUserDialog({ user, onDeleted, }: DeleteUserDialogProps) { - const deleteUser = useDeleteUser(); + const deleteUser = useAdminDeleteUser(); const [error, setError] = useState(""); return ( @@ -32,7 +32,7 @@ export default function DeleteUserDialog({ if (!user) return; setError(""); try { - await deleteUser.mutateAsync({ path: { id: user.id } }); + await deleteUser.mutateAsync({ id: user.id }); onClose(); onDeleted?.(); } catch { diff --git a/ui/apps/console/src/pages/admin/users/EditUserDrawer.tsx b/ui/apps/console/src/pages/admin/users/EditUserDrawer.tsx index a0d9a513099..c225a7f077d 100644 --- a/ui/apps/console/src/pages/admin/users/EditUserDrawer.tsx +++ b/ui/apps/console/src/pages/admin/users/EditUserDrawer.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useUpdateUser } from "@/hooks/useAdminUserMutations"; +import { useAdminUpdateUser } from "@/client/api"; import { useAuthStore } from "@/stores/authStore"; import { isSdkError } from "@/api/errors"; import FormDrawer from "@/components/common/FormDrawer"; @@ -11,7 +11,7 @@ import { buildUserPayload, type UserFormValues, } from "./userSchema"; -import type { UserAdminResponse } from "@/client"; +import type { UserAdminResponse } from "@/client/model"; interface EditUserDrawerProps { open: boolean; @@ -27,7 +27,7 @@ export default function EditUserDrawer({ onClose, user, }: EditUserDrawerProps) { - const updateUser = useUpdateUser(); + const updateUser = useAdminUpdateUser(); const currentUserId = useAuthStore((s) => s.userId); const schema = useMemo(() => userSchema("edit"), []); @@ -45,8 +45,8 @@ export default function EditUserDrawer({ clearErrors("root"); try { await updateUser.mutateAsync({ - path: { id: user.id }, - body: buildUserPayload("edit", values, user), + id: user.id, + data: buildUserPayload("edit", values, user), }); onClose(); } catch (err) { diff --git a/ui/apps/console/src/pages/admin/users/ResetPasswordDialog.tsx b/ui/apps/console/src/pages/admin/users/ResetPasswordDialog.tsx index 4d863ede5f4..a377223f671 100644 --- a/ui/apps/console/src/pages/admin/users/ResetPasswordDialog.tsx +++ b/ui/apps/console/src/pages/admin/users/ResetPasswordDialog.tsx @@ -1,7 +1,7 @@ import { useState, useId } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; -import { useResetUserPassword } from "@/hooks/useAdminUserMutations"; +import { useAdminResetUserPassword } from "@/client/api"; import { isSdkError } from "@/api/errors"; import CopyButton from "@/components/common/CopyButton"; import BaseDialog from "@/components/common/BaseDialog"; @@ -23,7 +23,7 @@ export default function ResetPasswordDialog({ onClose, userId, }: ResetPasswordDialogProps) { - const resetPassword = useResetUserPassword(); + const resetPassword = useAdminResetUserPassword(); const [step, setStep] = useState<"confirm" | "result">("confirm"); const [generatedPassword, setGeneratedPassword] = useState(""); const [error, setError] = useState(""); @@ -41,7 +41,7 @@ export default function ResetPasswordDialog({ const handleEnable = async () => { setError(""); try { - const data = await resetPassword.mutateAsync({ path: { id: userId } }); + const data = await resetPassword.mutateAsync({ id: userId }); setGeneratedPassword(data?.password ?? ""); setStep("result"); } catch (err) { diff --git a/ui/apps/console/src/pages/admin/users/UserDetails.tsx b/ui/apps/console/src/pages/admin/users/UserDetails.tsx index ff7c3c6853f..42f5debb345 100644 --- a/ui/apps/console/src/pages/admin/users/UserDetails.tsx +++ b/ui/apps/console/src/pages/admin/users/UserDetails.tsx @@ -10,7 +10,7 @@ import { KeyIcon, } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { useAdminUser } from "@/hooks/useAdminUsers"; +import { useGetUser } from "@/client/api"; import Breadcrumb from "@/components/common/Breadcrumb"; import { useLoginAsUser } from "@/hooks/useLoginAsUser"; import UserStatusChip from "./UserStatusChip"; @@ -40,10 +40,13 @@ function formatMaxNamespaces(value: number): string { * One user, seen from the admin area: their namespaces, their status, and the actions on them. */ export default function UserDetails() { - const { id } = useParams<{ id: string }>(); + const { id = "" } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data, isLoading, error } = useAdminUser(id ?? ""); - const user = data; + const { + data: user, + isLoading, + error, + } = useGetUser(id, { query: { enabled: !!id } }); const [editOpen, setEditOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); diff --git a/ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx b/ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx index 96118725ef8..c5ea1f9f812 100644 --- a/ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx +++ b/ui/apps/console/src/pages/admin/users/__tests__/AdminUsers.test.tsx @@ -9,7 +9,7 @@ import { import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import AdminUsers from "../index"; -import type { UserAdminResponse } from "@/client"; +import type { UserAdminResponse } from "@/client/model"; import { makeSdkError, paginatedResponse } from "@/tests/sdk"; import { createTestWrapper } from "@/tests/wrapper"; import { useAuthStore } from "@/stores/authStore"; diff --git a/ui/apps/console/src/pages/admin/users/__tests__/EditUserDrawer.test.tsx b/ui/apps/console/src/pages/admin/users/__tests__/EditUserDrawer.test.tsx index aebd78dc9e4..3ffd1d92a08 100644 --- a/ui/apps/console/src/pages/admin/users/__tests__/EditUserDrawer.test.tsx +++ b/ui/apps/console/src/pages/admin/users/__tests__/EditUserDrawer.test.tsx @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"; import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse } from "@/tests/sdk"; import { useAuthStore } from "@/stores/authStore"; -import type { UserAdminResponse } from "@/client"; +import type { UserAdminResponse } from "@/client/model"; import EditUserDrawer from "../EditUserDrawer"; const sdk = vi.hoisted(() => diff --git a/ui/apps/console/src/pages/admin/users/index.tsx b/ui/apps/console/src/pages/admin/users/index.tsx index f40c86b2c62..8fedd4be1e2 100644 --- a/ui/apps/console/src/pages/admin/users/index.tsx +++ b/ui/apps/console/src/pages/admin/users/index.tsx @@ -8,12 +8,15 @@ import { CheckIcon, ArrowRightStartOnRectangleIcon, } from "@heroicons/react/24/outline"; -import { useAdminUsers } from "@/hooks/useAdminUsers"; -import { useApproveAccountRequest } from "@/hooks/useAdminAccountRequestMutations"; +import { useGetUsers } from "@/client/api"; +import type { GetUsersParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; +import { toBase64Json } from "@/utils/encoding"; +import { useApproveUser } from "@/client/api"; import { useLoginAsUser } from "@/hooks/useLoginAsUser"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import type { UserAdminResponse } from "@/client"; +import type { UserAdminResponse } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import DataTable, { type Column } from "@/components/common/DataTable"; import ConfirmDialog from "@/components/common/ConfirmDialog"; @@ -65,15 +68,28 @@ export default function AdminUsers() { loadingId: loginAsId, errorId: loginAsError, } = useLoginAsUser(); - const approve = useApproveAccountRequest(); + const approve = useApproveUser(); - const { users, totalCount, isLoading, error } = useAdminUsers({ + const requestParams: GetUsersParams = { page: params.page, - perPage: PER_PAGE, - search: debouncedSearch, - }); + per_page: PER_PAGE, + }; + if (debouncedSearch) { + requestParams.filter = toBase64Json([ + { + type: "property", + params: { + name: "username", + operator: "contains", + value: debouncedSearch, + }, + }, + ]); + } + const { data: users = [], isLoading, error } = useGetUsers(requestParams); + const total = totalCount(users); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -222,7 +238,7 @@ export default function AdminUsers() { loadingMessage="Loading users..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="user" onPageChange={setPage} onRowClick={(user) => void navigate(`/admin/users/${user.id}`)} @@ -268,7 +284,7 @@ export default function AdminUsers() { if (!approveTarget) return; setApproveError(""); try { - await approve.mutateAsync({ path: { id: approveTarget.id } }); + await approve.mutateAsync({ id: approveTarget.id }); setApproveTarget(null); } catch { setApproveError("Failed to approve the account. Please try again."); diff --git a/ui/apps/console/src/pages/admin/users/userSchema.ts b/ui/apps/console/src/pages/admin/users/userSchema.ts index 152974bc1a7..543b87801ef 100644 --- a/ui/apps/console/src/pages/admin/users/userSchema.ts +++ b/ui/apps/console/src/pages/admin/users/userSchema.ts @@ -3,7 +3,7 @@ import type { UserAdminCreateRequest, UserAdminResponse, UserAdminUpdateRequest, -} from "@/client"; +} from "@/client/model"; import { MAX_NAMESPACES_ERROR, isMaxNamespacesValid, diff --git a/ui/apps/console/src/pages/containers/__tests__/ContainerDetails.test.tsx b/ui/apps/console/src/pages/containers/__tests__/ContainerDetails.test.tsx index 6f47a826178..05bdbe26ec2 100644 --- a/ui/apps/console/src/pages/containers/__tests__/ContainerDetails.test.tsx +++ b/ui/apps/console/src/pages/containers/__tests__/ContainerDetails.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; -import type { Device } from "@/client"; +import type { Device } from "@/client/model"; import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse } from "@/tests/sdk"; import { diff --git a/ui/apps/console/src/pages/containers/index.tsx b/ui/apps/console/src/pages/containers/index.tsx index 25069c2f907..b0372a13ccc 100644 --- a/ui/apps/console/src/pages/containers/index.tsx +++ b/ui/apps/console/src/pages/containers/index.tsx @@ -1,6 +1,10 @@ -import { useState, useMemo, useCallback } from "react"; +import { useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; -import { useContainers, type NormalizedContainer } from "@/hooks/useContainers"; +import { useGetContainers } from "@/client/api"; +import type { GetContainersParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; +import { normalizeDeviceTags, type TaggedDevice as NormalizedContainer } from "@/utils/deviceTags"; +import { toBase64Json } from "@/utils/encoding"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { useTableSort } from "@/hooks/useTableSort"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; @@ -17,10 +21,8 @@ import TagFilterDropdown from "@/components/common/TagFilterDropdown"; import { formatRelative } from "@/utils/date"; import { buildSshid } from "@/utils/sshid"; import TagsPopover from "@/components/common/TagsPopover"; -import { - useAddContainerTag, - useRemoveContainerTag, -} from "@/hooks/useContainerMutations"; +import { usePullTagFromContainer } from "@/client/api"; +import { useAddContainerTag } from "@/hooks/useContainerMutations"; import { useActionDialog } from "@/hooks/useActionDialog"; import { useContainerActionRunner } from "@/hooks/useContainerActionRunner"; import ActionDialog from "@/components/common/ActionDialog"; @@ -97,7 +99,7 @@ export default function Containers() { ); const addContainerTag = useAddContainerTag(); - const removeContainerTag = useRemoveContainerTag(); + const removeContainerTag = usePullTagFromContainer(); const containerActions = useActionDialog(); const { requestAction: requestContainerAction } = containerActions; const runContainerAction = useContainerActionRunner(); @@ -113,21 +115,32 @@ export default function Containers() { onSortChange: () => setPage(1), }); - const { containers, totalCount, isLoading, error, refetch } = useContainers({ + const requestParams: GetContainersParams = { page: params.page, - perPage: PER_PAGE, - status: params.status, - search: debouncedSearch, - filterTags: params.tags, - sortBy, - orderBy, - }); + per_page: PER_PAGE, + sort_by: sortBy, + order_by: orderBy, + }; + if (params.status) requestParams.status = params.status; + if (debouncedSearch || params.tags.length > 0) { + const filters: Record[] = []; + if (debouncedSearch) { + filters.push({ type: "property", params: { name: "name", operator: "contains", value: debouncedSearch } }); + } + if (params.tags.length > 0) { + filters.push({ type: "property", params: { name: "tags.name", operator: "contains", value: params.tags } }); + } + requestParams.filter = toBase64Json(filters); + } + const { data: rawContainers = [], isLoading, error, refetch } = useGetContainers(requestParams); + const containers = rawContainers.map(normalizeDeviceTags); + const total = totalCount(rawContainers); const tenantId = useAuthStore((s) => s.tenant) ?? ""; const { namespace: currentNamespace } = useNamespace(tenantId); const navigate = useNavigate(); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const nsName = currentNamespace?.name ?? ""; const handleStatusChange = (newStatus: ValidStatus) => { @@ -151,8 +164,7 @@ export default function Containers() { setArrayFilter("tags", []); }; - const columns = useMemo[]>(() => { - const baseColumns: Column[] = [ + const baseColumns: Column[] = [ { key: "name", header: "Hostname", @@ -196,10 +208,12 @@ export default function Containers() { ), }, - ]; + ]; + + let columns: Column[]; - if (params.status === "accepted") { - return [ + if (params.status === "accepted") { + columns = [ { key: "online", header: "", @@ -280,11 +294,9 @@ export default function Containers() { ), }, - ]; - } - - if (params.status === "pending") { - return [ + ]; + } else if (params.status === "pending") { + columns = [ ...baseColumns, { key: "actions", @@ -319,10 +331,9 @@ export default function Containers() { ), }, - ]; - } - - return [ + ]; + } else { + columns = [ ...baseColumns, { key: "actions", @@ -358,14 +369,7 @@ export default function Containers() { ), }, ]; - }, [ - params.status, - nsName, - addFilterTag, - requestContainerAction, - addContainerTag.mutateAsync, - removeContainerTag.mutateAsync, - ]); + } return (
@@ -472,7 +476,7 @@ export default function Containers() { loadingMessage="Loading containers..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="container" onPageChange={setPage} onRowClick={(container) => diff --git a/ui/apps/console/src/pages/devices/CustomFieldsSection.tsx b/ui/apps/console/src/pages/devices/CustomFieldsSection.tsx index b920f603ab8..05d58533b3d 100644 --- a/ui/apps/console/src/pages/devices/CustomFieldsSection.tsx +++ b/ui/apps/console/src/pages/devices/CustomFieldsSection.tsx @@ -1,10 +1,7 @@ import { useState } from "react"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { IconButton } from "@shellhub/design-system/primitives"; -import { - useSetDeviceCustomField, - useDeleteDeviceCustomField, -} from "@/hooks/useDeviceMutations"; +import { useSetDeviceCustomField, useDeleteDeviceCustomField } from "@/client/api"; import { useHasPermission } from "@/hooks/useHasPermission"; const LABEL = @@ -43,8 +40,8 @@ export default function CustomFieldsSection({ setAdding(true); try { await setMutation.mutateAsync({ - path: { uid, key }, - body: { value }, + uid, key, + data: { value }, }); setKeyInput(""); setValueInput(""); @@ -55,7 +52,7 @@ export default function CustomFieldsSection({ }; const handleRemove = (key: string) => { - deleteMutation.mutate({ path: { uid, key } }); + deleteMutation.mutate({ uid, key }); }; return ( diff --git a/ui/apps/console/src/pages/devices/__tests__/DeviceDetails.test.tsx b/ui/apps/console/src/pages/devices/__tests__/DeviceDetails.test.tsx index 157372b9959..c4abb6baf55 100644 --- a/ui/apps/console/src/pages/devices/__tests__/DeviceDetails.test.tsx +++ b/ui/apps/console/src/pages/devices/__tests__/DeviceDetails.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; -import type { Device } from "@/client"; +import type { Device } from "@/client/model"; import { createTestWrapper } from "@/tests/wrapper"; import { mockSdkResponse, paginatedResponse } from "@/tests/sdk"; import { diff --git a/ui/apps/console/src/pages/devices/index.tsx b/ui/apps/console/src/pages/devices/index.tsx index 6bc3a92809a..0d5a174f601 100644 --- a/ui/apps/console/src/pages/devices/index.tsx +++ b/ui/apps/console/src/pages/devices/index.tsx @@ -19,10 +19,8 @@ import SearchField from "@/components/common/fields/SearchField"; import { buildSshid } from "@/utils/sshid"; import TagFilterDropdown from "@/components/common/TagFilterDropdown"; import TagsPopover from "@/components/common/TagsPopover"; -import { - useAddDeviceTag, - useRemoveDeviceTag, -} from "@/hooks/useDeviceMutations"; +import { usePullTagFromDevice } from "@/client/api"; +import { useAddDeviceTag } from "@/hooks/useDeviceMutations"; import { PlusIcon, TagIcon, @@ -72,7 +70,7 @@ export default function Devices() { ); const addDeviceTag = useAddDeviceTag(); - const removeDeviceTag = useRemoveDeviceTag(); + const removeDeviceTag = usePullTagFromDevice(); const [connectTarget, setConnectTarget] = useState<{ uid: string; name: string; diff --git a/ui/apps/console/src/pages/firewall-rules/RuleDrawer.tsx b/ui/apps/console/src/pages/firewall-rules/RuleDrawer.tsx index 48b5cc7cfd6..c5cae2c4c73 100644 --- a/ui/apps/console/src/pages/firewall-rules/RuleDrawer.tsx +++ b/ui/apps/console/src/pages/firewall-rules/RuleDrawer.tsx @@ -1,9 +1,6 @@ import { useWatch } from "react-hook-form"; -import { - useCreateFirewallRule, - useUpdateFirewallRule, -} from "@/hooks/useFirewallRuleMutations"; -import type { FirewallRulesResponse } from "@/client"; +import { useCreateFirewallRule, useUpdateFirewallRule } from "@/client/api"; +import type { FirewallRulesResponse } from "@/client/model"; import RadioCard from "@/components/common/fields/RadioCard"; import { UserGroupIcon, @@ -72,9 +69,9 @@ export default function RuleDrawer({ const body = buildRuleBody(values); try { if (isEdit && editRule) { - await updateRule.mutateAsync({ path: { id: editRule.id }, body }); + await updateRule.mutateAsync({ id: editRule.id, data: body }); } else { - await createRule.mutateAsync({ body }); + await createRule.mutateAsync({ data: body }); } onClose(); } catch (err: unknown) { diff --git a/ui/apps/console/src/pages/firewall-rules/__tests__/RuleDrawer.test.tsx b/ui/apps/console/src/pages/firewall-rules/__tests__/RuleDrawer.test.tsx index 55d25f788d3..412483af0fb 100644 --- a/ui/apps/console/src/pages/firewall-rules/__tests__/RuleDrawer.test.tsx +++ b/ui/apps/console/src/pages/firewall-rules/__tests__/RuleDrawer.test.tsx @@ -8,7 +8,7 @@ import { } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import RuleDrawer from "../RuleDrawer"; -import type { FirewallRulesResponse } from "@/client"; +import type { FirewallRulesResponse } from "@/client/model"; import { mockSdkResponse } from "@/tests/sdk"; import { createTestWrapper } from "@/tests/wrapper"; import { mockFirewallRule } from "@/tests/factories"; diff --git a/ui/apps/console/src/pages/firewall-rules/__tests__/ruleSchema.test.ts b/ui/apps/console/src/pages/firewall-rules/__tests__/ruleSchema.test.ts index c1b102f8fbd..ec8d262f84e 100644 --- a/ui/apps/console/src/pages/firewall-rules/__tests__/ruleSchema.test.ts +++ b/ui/apps/console/src/pages/firewall-rules/__tests__/ruleSchema.test.ts @@ -5,7 +5,7 @@ import { buildRuleDefaults, type RuleFormValues, } from "../ruleSchema"; -import type { FirewallRulesResponse } from "@/client"; +import type { FirewallRulesResponse } from "@/client/model"; function makeValues(overrides: Partial = {}): RuleFormValues { return { diff --git a/ui/apps/console/src/pages/firewall-rules/index.tsx b/ui/apps/console/src/pages/firewall-rules/index.tsx index f456e697667..6d8cd9e7f26 100644 --- a/ui/apps/console/src/pages/firewall-rules/index.tsx +++ b/ui/apps/console/src/pages/firewall-rules/index.tsx @@ -10,7 +10,7 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; import { Badge, Button, IconButton } from "@shellhub/design-system/primitives"; -import { type FirewallRulesResponse as FirewallRule } from "@/client"; +import { type FirewallRulesResponse as FirewallRule } from "@/client/model"; import ActiveBadge from "@/components/common/ActiveBadge"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import DataTable, { type Column } from "@/components/common/DataTable"; @@ -19,8 +19,8 @@ import FilterBadge from "@/components/common/FilterBadge"; import PageHeader from "@/components/common/PageHeader"; import RestrictedAction from "@/components/common/RestrictedAction"; import SearchField from "@/components/common/fields/SearchField"; -import { useDeleteFirewallRule } from "@/hooks/useFirewallRuleMutations"; -import { useFirewallRules } from "@/hooks/useFirewallRules"; +import { useGetFirewallRules, useDeleteFirewallRule } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; import RuleDrawer from "./RuleDrawer"; import { pageCount } from "@/utils/pagination"; @@ -42,7 +42,8 @@ export default function FirewallRules() { const { params, setPage, setSearch } = usePaginatedListState({ defaults: DEFAULTS }); - const { rules, totalCount, isLoading } = useFirewallRules({ page: params.page }); + const { data: rules = [], isLoading } = useGetFirewallRules({ page: params.page, per_page: 10 }); + const total = totalCount(rules); const deleteRule = useDeleteFirewallRule(); const [drawerOpen, setDrawerOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); @@ -61,8 +62,9 @@ export default function FirewallRules() { if (!deleteTarget) return; setDeleteError(null); try { - await deleteRule.mutateAsync({ path: { id: deleteTarget.id } }); - if (rules.length === 1 && params.page > 1 && !params.search) setPage(params.page - 1); + await deleteRule.mutateAsync({ id: deleteTarget.id }); + if (rules.length === 1 && params.page > 1 && !params.search) + setPage(params.page - 1); closeDelete(); } catch (err) { setDeleteError( @@ -86,7 +88,7 @@ export default function FirewallRules() { setEditTarget(null); }; - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const filtered = params.search ? rules.filter( @@ -289,7 +291,7 @@ export default function FirewallRules() { {...(!params.search && { page: params.page, totalPages, - totalCount, + totalCount: total, itemLabel: "rule", onPageChange: setPage, })} diff --git a/ui/apps/console/src/pages/firewall-rules/ruleSchema.ts b/ui/apps/console/src/pages/firewall-rules/ruleSchema.ts index fd672e083cd..5c1404bd9b2 100644 --- a/ui/apps/console/src/pages/firewall-rules/ruleSchema.ts +++ b/ui/apps/console/src/pages/firewall-rules/ruleSchema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { FirewallRulesRequest, FirewallRulesResponse } from "@/client"; +import type { FirewallRulesRequest, FirewallRulesResponse } from "@/client/model"; function isValidRegex(pattern: string): boolean { try { diff --git a/ui/apps/console/src/pages/install-keys/CreateInstallKeyDrawer.tsx b/ui/apps/console/src/pages/install-keys/CreateInstallKeyDrawer.tsx index 717ee476da0..5b96b061766 100644 --- a/ui/apps/console/src/pages/install-keys/CreateInstallKeyDrawer.tsx +++ b/ui/apps/console/src/pages/install-keys/CreateInstallKeyDrawer.tsx @@ -3,7 +3,7 @@ import { CheckIcon, TicketIcon } from "@heroicons/react/24/outline"; import { Button, Card, WindowChrome } from "@shellhub/design-system/primitives"; import { isSdkError } from "@/api/errors"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; -import { useCreateInstallKey } from "@/hooks/useInstallKeyMutations"; +import { useInstallKeyCreate } from "@/client/api"; import { buildInstallCommand } from "@/utils/installCommand"; import CopyButton from "@/components/common/CopyButton"; import Drawer from "@/components/common/Drawer"; @@ -34,7 +34,7 @@ function CreateInstallKeyDrawer({ onClose: () => void; onCreated?: (name: string) => void; }) { - const createKey = useCreateInstallKey(); + const createKey = useInstallKeyCreate(); const [name, setName] = useState(""); const [mode, setMode] = useState("automatic"); const [webhookUrl, setWebhookUrl] = useState(""); @@ -104,7 +104,7 @@ function CreateInstallKeyDrawer({ setError(""); try { const result = await createKey.mutateAsync({ - body: { + data: { name: name.trim(), mode, ...(mode === "webhook" diff --git a/ui/apps/console/src/pages/install-keys/EditInstallKeyDrawer.tsx b/ui/apps/console/src/pages/install-keys/EditInstallKeyDrawer.tsx index 2e4d79a8a75..b23095bfe36 100644 --- a/ui/apps/console/src/pages/install-keys/EditInstallKeyDrawer.tsx +++ b/ui/apps/console/src/pages/install-keys/EditInstallKeyDrawer.tsx @@ -3,8 +3,8 @@ import { CheckIcon } from "@heroicons/react/24/outline"; import { Button, Callout } from "@shellhub/design-system/primitives"; import { isSdkError } from "@/api/errors"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; -import { useUpdateInstallKey } from "@/hooks/useInstallKeyMutations"; -import { type InstallKey, type InstallKeyUpdate } from "@/client"; +import { useInstallKeyUpdate } from "@/client/api"; +import { type InstallKey, type InstallKeyUpdate } from "@/client/model"; import { getRemainingDays, isSystemKey, @@ -29,7 +29,7 @@ function EditInstallKeyDrawer({ installKey: InstallKey | null; onClose: () => void; }) { - const updateKey = useUpdateInstallKey(); + const updateKey = useInstallKeyUpdate(); const open = installKey !== null; const isSystem = installKey ? isSystemKey(installKey) : false; const [name, setName] = useState(""); @@ -150,7 +150,7 @@ function EditInstallKeyDrawer({ ...(ephemeral ? { ephemeral_timeout: ephemeralTimeout } : {}), }; - await updateKey.mutateAsync({ path: { key: installKey.name }, body }); + await updateKey.mutateAsync({ key: installKey.name, data: body }); onClose(); } catch (err) { if (isSdkError(err) && err.status === 409) { diff --git a/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx b/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx index c0e7906e9e8..f73aabc9c1e 100644 --- a/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx +++ b/ui/apps/console/src/pages/install-keys/EventPublicKey.tsx @@ -1,6 +1,6 @@ import { useId, useState } from "react"; import { Button, Card } from "@shellhub/design-system/primitives"; -import { type InstallKeyEvent } from "@/client"; +import { type InstallKeyEvent } from "@/client/model"; import CopyButton from "@/components/common/CopyButton"; import BaseDialog from "@/components/common/BaseDialog"; import { LABEL } from "@/utils/styles"; diff --git a/ui/apps/console/src/pages/install-keys/ExpiryLabel.tsx b/ui/apps/console/src/pages/install-keys/ExpiryLabel.tsx index 7a039f8d42b..baf47d2177d 100644 --- a/ui/apps/console/src/pages/install-keys/ExpiryLabel.tsx +++ b/ui/apps/console/src/pages/install-keys/ExpiryLabel.tsx @@ -1,6 +1,6 @@ import { ClockIcon } from "@heroicons/react/24/outline"; import { ExclamationCircleIcon } from "@heroicons/react/24/solid"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import { getExpiryInfo, getKeyBlockers } from "./helpers"; import { cn } from "@shellhub/design-system/cn"; diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyActions.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyActions.tsx index e2e42932833..df55f93aa82 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyActions.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyActions.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import InstallKeyActionsMenu from "./InstallKeyActionsMenu"; import EditInstallKeyDrawer from "./EditInstallKeyDrawer"; import RevokeInstallKeyDialog from "./RevokeInstallKeyDialog"; diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyActionsMenu.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyActionsMenu.tsx index 5919cc18f0d..42d39d38dfd 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyActionsMenu.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyActionsMenu.tsx @@ -7,7 +7,7 @@ import { PlayIcon, } from "@heroicons/react/24/outline"; import { Dropdown, IconButton } from "@shellhub/design-system/primitives"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import RestrictedAction from "@/components/common/RestrictedAction"; import { type Action } from "@/utils/permission"; import { isPairingKey, isSystemKey } from "./helpers"; diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyEventReview.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyEventReview.tsx index 4ba209fe1d1..d17c8902d74 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyEventReview.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyEventReview.tsx @@ -5,7 +5,7 @@ import { } from "@heroicons/react/24/outline"; import RestrictedAction from "@/components/common/RestrictedAction"; import { formatDateFull } from "@/utils/date"; -import { type InstallKeyEvent } from "@/client"; +import { type InstallKeyEvent } from "@/client/model"; import type { RequestDeviceAction } from "./installKeyEventColumns"; import StatusChip from "./StatusChip"; diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyEventsTable.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyEventsTable.tsx index c4aa8c130b6..5bb831d4a6f 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyEventsTable.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyEventsTable.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react"; -import { useInstallKeyEvents } from "@/hooks/useInstallKeyEvents"; +import { useInstallKeyHistory } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { useActionDialog } from "@/hooks/useActionDialog"; import { useInvalidateByIds } from "@/hooks/useInvalidateQueries"; import DataTable from "@/components/common/DataTable"; @@ -18,7 +19,7 @@ const EMPTY_MESSAGE = */ export default function InstallKeyEventsTable({ id }: { id: string }) { const [page, setPage] = useState(1); - const refreshHistory = useInvalidateByIds("installKeyHistory"); + const refreshHistory = useInvalidateByIds("/api/namespaces/install-key"); const deviceActions = useActionDialog({ onSuccess: () => void refreshHistory(), }); @@ -27,11 +28,17 @@ export default function InstallKeyEventsTable({ id }: { id: string }) { () => getInstallKeyEventColumns(deviceActions.requestAction), [deviceActions.requestAction], ); - const { events, totalCount, isLoading, error } = useInstallKeyEvents({ - id, + const { + data: events = [], + isLoading, + error, + } = useInstallKeyHistory(id, { page, - perPage: PER_PAGE, + per_page: PER_PAGE, + sort_by: "created_at", + order_by: "desc", }); + const total = totalCount(events); if (error) { return ( @@ -57,8 +64,8 @@ export default function InstallKeyEventsTable({ id }: { id: string }) { loadingMessage="Loading activity..." emptyMessage={EMPTY_MESSAGE} page={page} - totalPages={pageCount(totalCount, PER_PAGE)} - totalCount={totalCount} + totalPages={pageCount(total, PER_PAGE)} + totalCount={total} itemLabel="registration" onPageChange={setPage} /> diff --git a/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx b/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx index 13c9ebbede2..5391c8c219f 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeyHistoryPage.tsx @@ -6,8 +6,8 @@ import { TicketIcon, } from "@heroicons/react/24/outline"; import { IconBadge } from "@shellhub/design-system/primitives"; -import { type InstallKey } from "@/client"; -import { useInstallKeys } from "@/hooks/useInstallKeys"; +import { type InstallKey } from "@/client/model"; +import { useInstallKeyList } from "@/client/api"; import PageLoader from "@/components/common/PageLoader"; import Breadcrumb from "@/components/common/Breadcrumb"; import ResourceNotFound from "@/components/common/ResourceNotFound"; @@ -41,7 +41,7 @@ export default function InstallKeyHistoryPage() { const location = useLocation(); const state = location.state as { name?: string; key?: InstallKey } | null; - const { installKeys, isLoading } = useInstallKeys({ perPage: 100 }); + const { data: installKeys = [], isLoading } = useInstallKeyList({ page: 1, per_page: 100, sort_by: "created_at", order_by: "desc" }); const key = installKeys.find((k) => k.id === id) ?? state?.key ?? null; const name = key ? installKeyDisplayName(key) : (state?.name ?? ""); const [revealOpen, setRevealOpen] = useState(false); diff --git a/ui/apps/console/src/pages/install-keys/InstallKeysTable.tsx b/ui/apps/console/src/pages/install-keys/InstallKeysTable.tsx index 28d1a9e7eb6..e7a85b8cb0a 100644 --- a/ui/apps/console/src/pages/install-keys/InstallKeysTable.tsx +++ b/ui/apps/console/src/pages/install-keys/InstallKeysTable.tsx @@ -8,7 +8,7 @@ import { TicketIcon, } from "@heroicons/react/24/outline"; import { Button } from "@shellhub/design-system/primitives"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import DataTable, { type Column } from "@/components/common/DataTable"; import RestrictedAction from "@/components/common/RestrictedAction"; import InstallKeyActionsMenu from "./InstallKeyActionsMenu"; diff --git a/ui/apps/console/src/pages/install-keys/RevealInstallKeyDialog.tsx b/ui/apps/console/src/pages/install-keys/RevealInstallKeyDialog.tsx index aa03e445df7..cd84e29190b 100644 --- a/ui/apps/console/src/pages/install-keys/RevealInstallKeyDialog.tsx +++ b/ui/apps/console/src/pages/install-keys/RevealInstallKeyDialog.tsx @@ -10,8 +10,8 @@ import { Card, Spinner, } from "@shellhub/design-system/primitives"; -import { useRevealInstallKey } from "@/hooks/useRevealInstallKey"; -import { type InstallKey } from "@/client"; +import { useInstallKeyReveal } from "@/client/api"; +import { type InstallKey } from "@/client/model"; import { installKeyDisplayName } from "./helpers"; import CopyButton from "@/components/common/CopyButton"; import BaseDialog from "@/components/common/BaseDialog"; @@ -48,10 +48,14 @@ export default function RevealInstallKeyDialog({ setRevealed(false); } - const { key, isLoading, error } = useRevealInstallKey( - hasSecret ? name : null, - revealed, - ); + const { + data: revealData, + isLoading, + error, + } = useInstallKeyReveal(name ?? "", { + query: { enabled: hasSecret && !!name && revealed, gcTime: 0 }, + }); + const key = revealData?.key ?? ""; return ( void; }) { - const updateKey = useUpdateInstallKey(); + const updateKey = useInstallKeyUpdate(); const [confirmText, setConfirmText] = useState(""); const [error, setError] = useState(null); @@ -33,8 +33,8 @@ export default function RevokeInstallKeyDialog({ setError(null); try { await updateKey.mutateAsync({ - path: { key: installKey.name }, - body: { revoked: true }, + key: installKey.name, + data: { revoked: true }, }); onRevoked(); } catch { diff --git a/ui/apps/console/src/pages/install-keys/UsageMeter.tsx b/ui/apps/console/src/pages/install-keys/UsageMeter.tsx index c50a1f50041..f07713b248d 100644 --- a/ui/apps/console/src/pages/install-keys/UsageMeter.tsx +++ b/ui/apps/console/src/pages/install-keys/UsageMeter.tsx @@ -1,6 +1,6 @@ import { ClockIcon } from "@heroicons/react/24/outline"; import { cn } from "@shellhub/design-system/cn"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import StatusChip from "./StatusChip"; import { getKeyBlockers, diff --git a/ui/apps/console/src/pages/install-keys/__tests__/helpers.test.ts b/ui/apps/console/src/pages/install-keys/__tests__/helpers.test.ts index 69fc59b5024..01180f04ec1 100644 --- a/ui/apps/console/src/pages/install-keys/__tests__/helpers.test.ts +++ b/ui/apps/console/src/pages/install-keys/__tests__/helpers.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import { getExpiryInfo, getKeyBlockers, diff --git a/ui/apps/console/src/pages/install-keys/helpers.ts b/ui/apps/console/src/pages/install-keys/helpers.ts index 50e4e894843..60a60c76703 100644 --- a/ui/apps/console/src/pages/install-keys/helpers.ts +++ b/ui/apps/console/src/pages/install-keys/helpers.ts @@ -1,5 +1,5 @@ import { differenceInCalendarDays } from "date-fns"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import { formatDateShort } from "@/utils/date"; /** diff --git a/ui/apps/console/src/pages/install-keys/index.tsx b/ui/apps/console/src/pages/install-keys/index.tsx index cfc1e4c055f..1dae27ce128 100644 --- a/ui/apps/console/src/pages/install-keys/index.tsx +++ b/ui/apps/console/src/pages/install-keys/index.tsx @@ -1,9 +1,10 @@ import { useState } from "react"; import { TicketIcon } from "@heroicons/react/24/outline"; import { Button, Spinner } from "@shellhub/design-system/primitives"; -import { useInstallKeys } from "@/hooks/useInstallKeys"; +import { useInstallKeyList } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import { type InstallKey } from "@/client"; +import { type InstallKey } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import RestrictedAction from "@/components/common/RestrictedAction"; import InstallKeysTable from "./InstallKeysTable"; @@ -30,9 +31,10 @@ export default function InstallKeys() { defaults: INSTALL_KEY_LIST_DEFAULTS, }); const page = params.page; - const { installKeys, totalCount, isLoading } = useInstallKeys({ page }); + const { data: installKeys = [], isLoading } = useInstallKeyList({ page, per_page: 10, sort_by: "created_at", order_by: "desc" }); + const total = totalCount(installKeys); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const [createOpen, setCreateOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); @@ -74,7 +76,7 @@ export default function InstallKeys() { data={installKeys} page={page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} noCustomKeys={noCustomKeys} onPageChange={setPage} onCreate={() => setCreateOpen(true)} diff --git a/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx b/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx index a7973167788..aeb4a6feacc 100644 --- a/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx +++ b/ui/apps/console/src/pages/install-keys/installKeyEventColumns.tsx @@ -1,5 +1,5 @@ import { ArrowPathIcon, PlusCircleIcon } from "@heroicons/react/24/outline"; -import { type InstallKeyEvent } from "@/client"; +import { type InstallKeyEvent } from "@/client/model"; import { formatDateFull } from "@/utils/date"; import { type Column } from "@/components/common/DataTable"; import DistroIcon from "@/components/common/DistroIcon"; diff --git a/ui/apps/console/src/pages/install-keys/useToggleInstallKey.ts b/ui/apps/console/src/pages/install-keys/useToggleInstallKey.ts index 3b68522eca4..22ba91230c9 100644 --- a/ui/apps/console/src/pages/install-keys/useToggleInstallKey.ts +++ b/ui/apps/console/src/pages/install-keys/useToggleInstallKey.ts @@ -1,26 +1,24 @@ import { useState } from "react"; -import { useUpdateInstallKey } from "@/hooks/useInstallKeyMutations"; -import { type InstallKey } from "@/client"; +import { useInstallKeyUpdate } from "@/client/api"; +import { type InstallKey } from "@/client/model"; /** * Enables and disables an install key, holding the failure so the row can show it. Disabling is * reversible, which is what distinguishes it from revoking. */ export function useToggleInstallKey() { - const updateKey = useUpdateInstallKey(); + const updateKey = useInstallKeyUpdate(); const [error, setError] = useState(null); const toggle = async (key: InstallKey) => { setError(null); try { await updateKey.mutateAsync({ - path: { key: key.name }, - body: { disabled: !key.disabled }, + key: key.name, + data: { disabled: !key.disabled }, }); } catch { - setError( - `Failed to ${key.disabled ? "enable" : "disable"} Install Key.`, - ); + setError(`Failed to ${key.disabled ? "enable" : "disable"} Install Key.`); } }; diff --git a/ui/apps/console/src/pages/public-keys/KeyDrawer.tsx b/ui/apps/console/src/pages/public-keys/KeyDrawer.tsx index 64b8d2d31e4..941c3d53509 100644 --- a/ui/apps/console/src/pages/public-keys/KeyDrawer.tsx +++ b/ui/apps/console/src/pages/public-keys/KeyDrawer.tsx @@ -8,11 +8,8 @@ import { ClipboardDocumentListIcon, } from "@heroicons/react/24/outline"; import { DevicesIcon } from "@shellhub/design-system/primitives"; -import { - useCreatePublicKey, - useUpdatePublicKey, -} from "@/hooks/usePublicKeyMutations"; -import type { PublicKeyResponse } from "@/client"; +import { useCreatePublicKey, useUpdatePublicKey } from "@/client/api"; +import type { PublicKeyResponse } from "@/client/model"; import RadioCard from "@/components/common/fields/RadioCard"; import FormDrawer from "@/components/common/FormDrawer"; import { @@ -71,15 +68,15 @@ export default function KeyDrawer({ if (isEdit && editKey) { const body = buildKeyBody(values); await updateKey.mutateAsync({ - path: { fingerprint: editKey.fingerprint }, - body: { + fingerprint: editKey.fingerprint, + data: { name: body.name, username: body.username, filter: body.filter, }, }); } else { - await createKey.mutateAsync({ body: buildKeyBody(values) }); + await createKey.mutateAsync({ data: buildKeyBody(values) }); } onClose(); } catch (err: unknown) { diff --git a/ui/apps/console/src/pages/public-keys/__tests__/KeyDrawer.test.tsx b/ui/apps/console/src/pages/public-keys/__tests__/KeyDrawer.test.tsx index b425773dd99..c04aded934b 100644 --- a/ui/apps/console/src/pages/public-keys/__tests__/KeyDrawer.test.tsx +++ b/ui/apps/console/src/pages/public-keys/__tests__/KeyDrawer.test.tsx @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import KeyDrawer from "../KeyDrawer"; -import type { PublicKeyResponse } from "@/client"; +import type { PublicKeyResponse } from "@/client/model"; import type { KeyFormValues } from "../keySchema"; import { mockSdkResponse } from "@/tests/sdk"; import { createTestWrapper } from "@/tests/wrapper"; diff --git a/ui/apps/console/src/pages/public-keys/__tests__/keySchema.test.ts b/ui/apps/console/src/pages/public-keys/__tests__/keySchema.test.ts index ff77a1e3fe6..044ae008090 100644 --- a/ui/apps/console/src/pages/public-keys/__tests__/keySchema.test.ts +++ b/ui/apps/console/src/pages/public-keys/__tests__/keySchema.test.ts @@ -6,7 +6,7 @@ import { type KeyFormValues, type KeyMode, } from "../keySchema"; -import type { PublicKeyResponse } from "@/client"; +import type { PublicKeyResponse } from "@/client/model"; function makeValues(overrides: Partial = {}): KeyFormValues { return { diff --git a/ui/apps/console/src/pages/public-keys/index.tsx b/ui/apps/console/src/pages/public-keys/index.tsx index a512d01fefe..f6fdb43b7ee 100644 --- a/ui/apps/console/src/pages/public-keys/index.tsx +++ b/ui/apps/console/src/pages/public-keys/index.tsx @@ -1,8 +1,11 @@ import { useState } from "react"; -import { usePublicKeys } from "@/hooks/usePublicKeys"; +import { useGetPublicKeys } from "@/client/api"; +import type { GetPublicKeysParams } from "@/client/model"; +import { totalCount } from "@/api/pagination"; +import { toBase64Json } from "@/utils/encoding"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { usePaginatedListState } from "@/hooks/usePaginatedListState"; -import { useDeletePublicKey } from "@/hooks/usePublicKeyMutations"; +import { useDeletePublicKey } from "@/client/api"; import PageHeader from "@/components/common/PageHeader"; import EmptyState from "@/components/common/EmptyState"; import ConfirmDialog from "@/components/common/ConfirmDialog"; @@ -24,7 +27,7 @@ import { PencilSquareIcon, TrashIcon, } from "@heroicons/react/24/outline"; -import { PublicKeyResponse as PublicKey } from "@/client"; +import { PublicKeyResponse as PublicKey } from "@/client/model"; import { Button, IconButton } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; import { pageCount } from "@/utils/pagination"; @@ -111,10 +114,17 @@ export default function PublicKeys() { }); const debouncedSearch = useDebouncedValue(params.search, SEARCH_DEBOUNCE_MS); - const { publicKeys, totalCount, isLoading } = usePublicKeys({ - page: params.page, - search: debouncedSearch, - }); + const requestParams: GetPublicKeysParams = { page: params.page, per_page: 10 }; + if (debouncedSearch) { + requestParams.filter = toBase64Json([ + { type: "operator", params: { name: "or" } }, + { type: "property", params: { name: "name", operator: "contains", value: debouncedSearch } }, + { type: "operator", params: { name: "or" } }, + { type: "property", params: { name: "fingerprint", operator: "contains", value: debouncedSearch } }, + ]); + } + const { data: publicKeys = [], isLoading } = useGetPublicKeys(requestParams); + const total = totalCount(publicKeys); const deleteKey = useDeletePublicKey(); const [drawerOpen, setDrawerOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); @@ -134,7 +144,7 @@ export default function PublicKeys() { setDeleteError(null); try { await deleteKey.mutateAsync({ - path: { fingerprint: deleteTarget.fingerprint }, + fingerprint: deleteTarget.fingerprint, }); if (publicKeys.length === 1 && params.page > 1) setPage(params.page - 1); closeDelete(); @@ -158,7 +168,7 @@ export default function PublicKeys() { setEditTarget(null); }; - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -317,7 +327,7 @@ export default function PublicKeys() { loadingMessage="Loading public keys..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="key" onPageChange={setPage} emptyMessage={ diff --git a/ui/apps/console/src/pages/public-keys/keySchema.ts b/ui/apps/console/src/pages/public-keys/keySchema.ts index 77becdfacce..55514043374 100644 --- a/ui/apps/console/src/pages/public-keys/keySchema.ts +++ b/ui/apps/console/src/pages/public-keys/keySchema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { PublicKeyRequest, PublicKeyResponse } from "@/client"; +import type { PublicKeyRequest, PublicKeyResponse } from "@/client/model"; import { isPublicKeyValid } from "@/utils/sshKeys"; import { validateName } from "@/utils/validation"; diff --git a/ui/apps/console/src/pages/sessions/index.tsx b/ui/apps/console/src/pages/sessions/index.tsx index 65da69baa22..bf164cf6586 100644 --- a/ui/apps/console/src/pages/sessions/index.tsx +++ b/ui/apps/console/src/pages/sessions/index.tsx @@ -8,12 +8,12 @@ import { XCircleIcon, } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/solid"; -import { useSessions } from "@/hooks/useSessions"; -import { useCloseSession } from "@/hooks/useSessionMutations"; +import { useGetSessions, useClsoeSession } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { useSessionRecording } from "@/hooks/useSessionRecording"; import { useRecordingsStore } from "@/stores/recordingsStore"; import { isRecordingSupported, readRecording } from "@/utils/recordings"; -import type { Session } from "@/client"; +import type { Session } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import DeviceChip from "@/components/common/DeviceChip"; import DataTable, { type Column } from "@/components/common/DataTable"; @@ -74,11 +74,9 @@ export default function Sessions() { const { params, setPage } = usePaginatedListState({ defaults: DEFAULTS, }); - const { sessions, totalCount, isLoading, error } = useSessions({ - page: params.page, - perPage: PER_PAGE, - }); - const closeSession = useCloseSession(); + const { data: sessions = [], isLoading, error } = useGetSessions({ page: params.page, per_page: PER_PAGE }); + const total = totalCount(sessions); + const closeSession = useClsoeSession(); const navigate = useNavigate(); const premium = isEnterpriseOrCloud(); const [playTarget, setPlayTarget] = useState(null); @@ -109,7 +107,7 @@ export default function Sessions() { [recordings], ); - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const handlePlayClick = async (e: React.MouseEvent, s: Session) => { e.stopPropagation(); @@ -300,8 +298,8 @@ export default function Sessions() { closeSession.mutateAsync({ - path: { uid: s.uid }, - body: { device: s.device_uid ?? s.device?.uid ?? "" }, + uid: s.uid, + data: { device: s.device_uid ?? s.device?.uid ?? "" }, }) } /> @@ -342,7 +340,7 @@ export default function Sessions() { loadingMessage="Loading sessions..." page={params.page} totalPages={totalPages} - totalCount={totalCount} + totalCount={total} itemLabel="session" onPageChange={setPage} onRowClick={(s) => void navigate(`/sessions/${s.uid}`)} diff --git a/ui/apps/console/src/pages/ssh-identities/IdentityDrawer.tsx b/ui/apps/console/src/pages/ssh-identities/IdentityDrawer.tsx index c1cc4a4d38a..87c9bd5cee3 100644 --- a/ui/apps/console/src/pages/ssh-identities/IdentityDrawer.tsx +++ b/ui/apps/console/src/pages/ssh-identities/IdentityDrawer.tsx @@ -7,12 +7,12 @@ import { import { Button } from "@shellhub/design-system/primitives"; import { useResetOnOpen } from "@/hooks/useResetOnOpen"; import { - useCreateSSHIdentity, - useRenameSSHIdentity, -} from "@/hooks/useSSHIdentityMutations"; -import { useCreateServiceAccount } from "@/hooks/useServiceAccountMutations"; + useCreateSshIdentity, + useRenameSshIdentity, + useCreateServiceAccount, +} from "@/client/api"; import { useHasPermission } from "@/hooks/useHasPermission"; -import type { SshIdentity } from "@/client"; +import type { SshIdentity } from "@/client/model"; import { isPublicKeyValid } from "@/utils/sshKeys"; import Drawer from "@/components/common/Drawer"; import InputField from "@/components/common/fields/InputField"; @@ -47,8 +47,8 @@ function IdentityDrawer({ editIdentity: SshIdentity | null; onClose: () => void; }) { - const createIdentity = useCreateSSHIdentity(); - const renameIdentity = useRenameSSHIdentity(); + const createIdentity = useCreateSshIdentity(); + const renameIdentity = useRenameSshIdentity(); const createServiceAccount = useCreateServiceAccount(); const canCreateServiceAccount = useHasPermission("serviceAccount:create"); const browserKeyFingerprint = useBrowserKeyFingerprint(); @@ -99,12 +99,12 @@ function IdentityDrawer({ try { if (isEdit && editIdentity) { await renameIdentity.mutateAsync({ - path: { id: editIdentity.id }, - body: { name: name.trim() }, + id: editIdentity.id, + data: { name: name.trim() }, }); } else if (isServiceAccount) { await createServiceAccount.mutateAsync({ - body: { + data: { name: name.trim(), data: keyData.trim(), ...serviceAccountLifecyclePayload(expiresIn, singleUse), @@ -112,7 +112,7 @@ function IdentityDrawer({ }); } else { await createIdentity.mutateAsync({ - body: { + data: { name: name.trim(), data: keyData.trim(), ...keyExpiryPayload(expiresIn), diff --git a/ui/apps/console/src/pages/ssh-identities/__tests__/index.test.tsx b/ui/apps/console/src/pages/ssh-identities/__tests__/index.test.tsx index 3fbf5705791..f88959d870b 100644 --- a/ui/apps/console/src/pages/ssh-identities/__tests__/index.test.tsx +++ b/ui/apps/console/src/pages/ssh-identities/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import { render, screen, within } from "@testing-library/react"; import userEvent, { type UserEvent } from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import SSHIdentities from "../index"; -import type { SshIdentity } from "@/client"; +import type { SshIdentity } from "@/client/model"; import { ClipboardProvider } from "@/components/common/ClipboardProvider"; import { mockSdkResponse } from "@/tests/sdk"; import { createTestWrapper } from "@/tests/wrapper"; diff --git a/ui/apps/console/src/pages/ssh-identities/index.tsx b/ui/apps/console/src/pages/ssh-identities/index.tsx index 08307146318..968f5a867c9 100644 --- a/ui/apps/console/src/pages/ssh-identities/index.tsx +++ b/ui/apps/console/src/pages/ssh-identities/index.tsx @@ -20,10 +20,9 @@ import { IconButton, } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; -import { useSSHIdentities } from "@/hooks/useSSHIdentities"; -import { useDeleteSSHIdentity } from "@/hooks/useSSHIdentityMutations"; +import { useListSshIdentities, useDeleteSshIdentity } from "@/client/api"; import { useAuthStore } from "@/stores/authStore"; -import type { SshIdentity } from "@/client"; +import type { SshIdentity } from "@/client/model"; import PageHeader from "@/components/common/PageHeader"; import EmptyState from "@/components/common/EmptyState"; import ConfirmDialog from "@/components/common/ConfirmDialog"; @@ -70,11 +69,13 @@ const EXPIRY_TONE: Record = { export default function SSHIdentities() { const userId = useAuthStore((s) => s.userId); - const { identities, isLoading } = useSSHIdentities(true); + const { data: identities = [], isLoading } = useListSshIdentities({ + all: true, + }); const browserKeyFingerprint = useBrowserKeyFingerprint(); const isCurrentBrowser = (i: SshIdentity) => i.source === "browser" && i.fingerprint === browserKeyFingerprint; - const deleteIdentity = useDeleteSSHIdentity(); + const deleteIdentity = useDeleteSshIdentity(); const [drawerOpen, setDrawerOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); @@ -89,7 +90,7 @@ export default function SSHIdentities() { if (!deleteTarget) return; setDeleteError(null); try { - await deleteIdentity.mutateAsync({ path: { id: deleteTarget.id } }); + await deleteIdentity.mutateAsync({ id: deleteTarget.id }); closeDelete(); } catch (err) { setDeleteError( @@ -246,7 +247,10 @@ export default function SSHIdentities() {
- + diff --git a/ui/apps/console/src/pages/team/AddMemberDrawer.tsx b/ui/apps/console/src/pages/team/AddMemberDrawer.tsx index 8cda7633450..72bbfbd03ad 100644 --- a/ui/apps/console/src/pages/team/AddMemberDrawer.tsx +++ b/ui/apps/console/src/pages/team/AddMemberDrawer.tsx @@ -4,7 +4,7 @@ import { useResetOnOpen } from "@/hooks/useResetOnOpen"; import { useWatch } from "react-hook-form"; import { Card, Button } from "@shellhub/design-system/primitives"; import { CheckCircleIcon } from "@heroicons/react/24/outline"; -import { useGenerateInvitationLink } from "@/hooks/useInvitationMutations"; +import { useGenerateInvitationLink } from "@/client/api"; import Drawer from "@/components/common/Drawer"; import CopyButton from "@/components/common/CopyButton"; import { FormInputField } from "@/components/common/fields/rhf"; @@ -58,8 +58,8 @@ function AddMemberDrawer({ open, onClose, tenantId }: AddMemberDrawerProps) { clearErrors("root"); try { const result = await generateLink.mutateAsync({ - path: { tenant: tenantId }, - body: buildAddMemberBody(values), + tenant: tenantId, + data: buildAddMemberBody(values), }); const link = result.link ?? ""; if (link) setGeneratedLink(link); @@ -72,11 +72,19 @@ function AddMemberDrawer({ open, onClose, tenantId }: AddMemberDrawerProps) { return; } - const sdkErrorHandlers: Partial> = { + const sdkErrorHandlers: Partial< + Record + > = { 400: { name: "email", message: "Invalid email or role." }, - 403: { name: "root", message: "You don't have permission to invite members." }, + 403: { + name: "root", + message: "You don't have permission to invite members.", + }, 404: { name: "email", message: "No account exists for this email." }, - 409: { name: "email", message: "This user is already a member or has a pending invitation." }, + 409: { + name: "email", + message: "This user is already a member or has a pending invitation.", + }, }; const sdkError = sdkErrorHandlers[err.status] ?? { diff --git a/ui/apps/console/src/pages/team/ApiKeysTab.tsx b/ui/apps/console/src/pages/team/ApiKeysTab.tsx index fec2657c5c7..a9d9026b429 100644 --- a/ui/apps/console/src/pages/team/ApiKeysTab.tsx +++ b/ui/apps/console/src/pages/team/ApiKeysTab.tsx @@ -6,10 +6,10 @@ import { } from "@heroicons/react/24/outline"; import { Button, IconButton } from "@shellhub/design-system/primitives"; import { cn } from "@shellhub/design-system/cn"; -import { useApiKeys } from "@/hooks/useApiKeys"; -import { useDeleteApiKey } from "@/hooks/useApiKeyMutations"; +import { useApiKeyList, useApiKeyDelete } from "@/client/api"; +import { totalCount } from "@/api/pagination"; import { useTableSort } from "@/hooks/useTableSort"; -import { type ApiKey } from "@/client"; +import { type ApiKey } from "@/client/model"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import DataTable, { type Column } from "@/components/common/DataTable"; import RestrictedAction from "@/components/common/RestrictedAction"; @@ -43,13 +43,15 @@ function ApiKeysTab() { defaultField: "created_at", onSortChange: () => setPage(1), }); - const { apiKeys, totalCount, isLoading } = useApiKeys({ + const { data: apiKeys = [], isLoading } = useApiKeyList({ page, - sortBy, - orderBy, + per_page: 10, + sort_by: sortBy, + order_by: orderBy, }); + const total = totalCount(apiKeys); - const deleteKey = useDeleteApiKey(); + const deleteKey = useApiKeyDelete(); const [generateOpen, setGenerateOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); @@ -64,7 +66,7 @@ function ApiKeysTab() { if (!deleteTarget) return; setDeleteError(null); try { - await deleteKey.mutateAsync({ path: { key: deleteTarget.name } }); + await deleteKey.mutateAsync({ key: deleteTarget.name }); if (apiKeys.length === 1 && page > 1) setPage(page - 1); closeDelete(); } catch (err) { @@ -74,7 +76,7 @@ function ApiKeysTab() { } }; - const totalPages = pageCount(totalCount); + const totalPages = pageCount(total); const columns: Column[] = [ { @@ -116,7 +118,10 @@ function ApiKeysTab() { const expired = isExpired(key.expires_in); return ( {formatExpiry(key.expires_in)} @@ -158,8 +163,8 @@ function ApiKeysTab() {

- {totalCount} key - {totalCount !== 1 ? "s" : ""} + {total} key + {total !== 1 ? "s" : ""}