diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 65a4aaa40a937..08a25543b0fb6 100644 --- a/site/e2e/api.ts +++ b/site/e2e/api.ts @@ -1,7 +1,7 @@ import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; import { formatDuration, intervalToDuration } from "date-fns"; -import * as API from "api/api"; +import { type DeploymentConfig, API } from "api/api"; import type { SerpentOption } from "api/typesGenerated"; import { coderPort } from "./constants"; import { findSessionToken, randomName } from "./helpers"; @@ -15,6 +15,7 @@ export const setupApiCalls = async (page: Page) => { } catch { // If this fails, we have an unauthenticated client. } + API.setHost(`http://127.0.0.1:${coderPort}`); }; @@ -53,7 +54,7 @@ export const createGroup = async (orgId: string) => { export async function verifyConfigFlagBoolean( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -68,7 +69,7 @@ export async function verifyConfigFlagBoolean( export async function verifyConfigFlagNumber( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -80,7 +81,7 @@ export async function verifyConfigFlagNumber( export async function verifyConfigFlagString( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -100,7 +101,7 @@ export async function verifyConfigFlagEmpty(page: Page, flag: string) { export async function verifyConfigFlagArray( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -116,7 +117,7 @@ export async function verifyConfigFlagArray( export async function verifyConfigFlagEntries( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -138,7 +139,7 @@ export async function verifyConfigFlagEntries( export async function verifyConfigFlagDuration( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ) { const opt = findConfigOption(config, flag); @@ -157,7 +158,7 @@ export async function verifyConfigFlagDuration( } export function findConfigOption( - config: API.DeploymentConfig, + config: DeploymentConfig, flag: string, ): SerpentOption { const opt = config.options.find((option) => option.flag === flag); diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts index 8c8526af9acc1..b23f6bbaa1cd3 100644 --- a/site/e2e/global.setup.ts +++ b/site/e2e/global.setup.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { hasFirstUser } from "api/api"; +import { API } from "api/api"; import { Language } from "pages/CreateUserPage/CreateUserForm"; import { setupApiCalls } from "./api"; import * as constants from "./constants"; @@ -9,7 +9,7 @@ import { storageState } from "./playwright.config"; test("setup deployment", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await setupApiCalls(page); - const exists = await hasFirstUser(); + const exists = await API.hasFirstUser(); // First user already exists, abort early. All tests execute this as a dependency, // if you run multiple tests in the UI, this will fail unless we check this. if (exists) { diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 3f58184b1c1ac..2aa45193806d0 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -6,7 +6,7 @@ import capitalize from "lodash/capitalize"; import path from "path"; import * as ssh from "ssh2"; import { Duplex } from "stream"; -import { axiosInstance } from "api/api"; +import { API } from "api/api"; import type { WorkspaceBuildParameter, UpdateTemplateMeta, @@ -423,6 +423,7 @@ export const waitUntilUrlIsNotResponding = async (url: string) => { const retryIntervalMs = 1000; let retries = 0; + const axiosInstance = API.getAxiosInstance(); while (retries < maxRetries) { try { await axiosInstance.get(url); diff --git a/site/e2e/reporter.ts b/site/e2e/reporter.ts index 466bc564d238a..8c9a0d163acc0 100644 --- a/site/e2e/reporter.ts +++ b/site/e2e/reporter.ts @@ -10,7 +10,7 @@ import type { } from "@playwright/test/reporter"; import * as fs from "fs/promises"; import type { Writable } from "stream"; -import { axiosInstance } from "api/api"; +import { API } from "api/api"; import { coderdPProfPort, enterpriseLicense } from "./constants"; class CoderReporter implements Reporter { @@ -143,6 +143,7 @@ const logLines = (chunk: string | Buffer): string[] => { }; const exportDebugPprof = async (outputFile: string) => { + const axiosInstance = API.getAxiosInstance(); const response = await axiosInstance.get( `http://127.0.0.1:${coderdPProfPort}/debug/pprof/goroutine?debug=1`, ); diff --git a/site/e2e/tests/deployment/general.spec.ts b/site/e2e/tests/deployment/general.spec.ts index de334a95b05e3..47e9a22e1a67f 100644 --- a/site/e2e/tests/deployment/general.spec.ts +++ b/site/e2e/tests/deployment/general.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import * as API from "api/api"; +import { API } from "api/api"; import { setupApiCalls } from "../../api"; import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants"; diff --git a/site/e2e/tests/deployment/network.spec.ts b/site/e2e/tests/deployment/network.spec.ts index c979bb8e1022f..d125a100d30bb 100644 --- a/site/e2e/tests/deployment/network.spec.ts +++ b/site/e2e/tests/deployment/network.spec.ts @@ -1,5 +1,5 @@ import { test } from "@playwright/test"; -import { getDeploymentConfig } from "api/api"; +import { API } from "api/api"; import { setupApiCalls, verifyConfigFlagArray, @@ -11,7 +11,7 @@ import { test("enabled network settings", async ({ page }) => { await setupApiCalls(page); - const config = await getDeploymentConfig(); + const config = await API.getDeploymentConfig(); await page.goto("/deployment/network", { waitUntil: "domcontentloaded" }); diff --git a/site/e2e/tests/deployment/observability.spec.ts b/site/e2e/tests/deployment/observability.spec.ts index e94f14b6ceebc..7030ea35081a3 100644 --- a/site/e2e/tests/deployment/observability.spec.ts +++ b/site/e2e/tests/deployment/observability.spec.ts @@ -1,5 +1,5 @@ import { test } from "@playwright/test"; -import { getDeploymentConfig } from "api/api"; +import { API } from "api/api"; import { setupApiCalls, verifyConfigFlagArray, @@ -11,7 +11,7 @@ import { test("enabled observability settings", async ({ page }) => { await setupApiCalls(page); - const config = await getDeploymentConfig(); + const config = await API.getDeploymentConfig(); await page.goto("/deployment/observability", { waitUntil: "domcontentloaded", diff --git a/site/e2e/tests/deployment/security.spec.ts b/site/e2e/tests/deployment/security.spec.ts index ede966260ca44..45675089852e1 100644 --- a/site/e2e/tests/deployment/security.spec.ts +++ b/site/e2e/tests/deployment/security.spec.ts @@ -1,7 +1,6 @@ import type { Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; -import type * as API from "api/api"; -import { getDeploymentConfig } from "api/api"; +import { type DeploymentConfig, API } from "api/api"; import { findConfigOption, setupApiCalls, @@ -12,7 +11,7 @@ import { test("enabled security settings", async ({ page }) => { await setupApiCalls(page); - const config = await getDeploymentConfig(); + const config = await API.getDeploymentConfig(); await page.goto("/deployment/security", { waitUntil: "domcontentloaded" }); @@ -31,7 +30,7 @@ test("enabled security settings", async ({ page }) => { async function verifyStrictTransportSecurity( page: Page, - config: API.DeploymentConfig, + config: DeploymentConfig, ) { const flag = "strict-transport-security"; const opt = findConfigOption(config, flag); diff --git a/site/e2e/tests/deployment/userAuth.spec.ts b/site/e2e/tests/deployment/userAuth.spec.ts index cf656c99fae3f..8dd8a3af49af7 100644 --- a/site/e2e/tests/deployment/userAuth.spec.ts +++ b/site/e2e/tests/deployment/userAuth.spec.ts @@ -1,5 +1,5 @@ import { test } from "@playwright/test"; -import { getDeploymentConfig } from "api/api"; +import { API } from "api/api"; import { setupApiCalls, verifyConfigFlagArray, @@ -10,7 +10,7 @@ import { test("login with OIDC", async ({ page }) => { await setupApiCalls(page); - const config = await getDeploymentConfig(); + const config = await API.getDeploymentConfig(); await page.goto("/deployment/userauth", { waitUntil: "domcontentloaded" }); diff --git a/site/e2e/tests/deployment/workspaceProxies.spec.ts b/site/e2e/tests/deployment/workspaceProxies.spec.ts index 5f67bda7d7ad4..47f8d48895466 100644 --- a/site/e2e/tests/deployment/workspaceProxies.spec.ts +++ b/site/e2e/tests/deployment/workspaceProxies.spec.ts @@ -1,5 +1,5 @@ import { test, expect, type Page } from "@playwright/test"; -import { createWorkspaceProxy } from "api/api"; +import { API } from "api/api"; import { setupApiCalls } from "../../api"; import { coderPort, workspaceProxyPort } from "../../constants"; import { randomName, requiresEnterpriseLicense } from "../../helpers"; @@ -34,7 +34,7 @@ test("custom proxy is online", async ({ page }) => { const proxyName = randomName(); // Register workspace proxy - const proxyResponse = await createWorkspaceProxy({ + const proxyResponse = await API.createWorkspaceProxy({ name: proxyName, display_name: "", icon: "/emojis/1f1e7-1f1f7.png", diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts index 716c86af84a8d..468d9d4851441 100644 --- a/site/e2e/tests/groups/removeMember.spec.ts +++ b/site/e2e/tests/groups/removeMember.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test"; -import * as API from "api/api"; +import { API } from "api/api"; import { createGroup, createUser, diff --git a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts index 1eb272a665edb..5678f015c917c 100644 --- a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts +++ b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { createTemplate, createTemplateVersion, getTemplate } from "api/api"; +import { API } from "api/api"; import { getCurrentOrgId, setupApiCalls } from "../../api"; import { beforeCoderTest } from "../../hooks"; @@ -11,14 +11,14 @@ test("update template schedule settings without override other settings", async }) => { await setupApiCalls(page); const orgId = await getCurrentOrgId(); - const templateVersion = await createTemplateVersion(orgId, { + const templateVersion = await API.createTemplateVersion(orgId, { storage_method: "file" as const, provisioner: "echo", user_variable_values: [], example_id: "docker", tags: {}, }); - const template = await createTemplate(orgId, { + const template = await API.createTemplate(orgId, { name: "test-template", display_name: "Test Template", template_version_id: templateVersion.id, @@ -33,7 +33,7 @@ test("update template schedule settings without override other settings", async await page.getByRole("button", { name: "Submit" }).click(); await expect(page.getByText("Template updated successfully")).toBeVisible(); - const updatedTemplate = await getTemplate(template.id); + const updatedTemplate = await API.getTemplate(template.id); // Validate that the template data remains consistent, with the exception of // the 'default_ttl_ms' field (updated during the test) and the 'updated at' // field (automatically updated by the backend). diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index 18615306683c4..af5f5e22d61ba 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -6,10 +6,11 @@ import { MockWorkspaceBuild, MockWorkspaceBuildParameter1, } from "testHelpers/entities"; -import * as api from "./api"; -import { axiosInstance } from "./api"; +import { API, getURLWithSearchParams, MissingBuildParameters } from "./api"; import type * as TypesGen from "./typesGenerated"; +const axiosInstance = API.getAxiosInstance(); + describe("api.ts", () => { describe("login", () => { it("should return LoginResponse", async () => { @@ -23,7 +24,7 @@ describe("api.ts", () => { .mockResolvedValueOnce({ data: loginResponse }); // when - const result = await api.login("test", "123"); + const result = await API.login("test", "123"); // then expect(axiosInstance.post).toHaveBeenCalled(); @@ -44,7 +45,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await api.login("test", "123"); + await API.login("test", "123"); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -60,7 +61,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; // when - await api.logout(); + await API.logout(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -80,7 +81,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await api.logout(); + await API.logout(); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -100,7 +101,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; // when - const result = await api.getApiKey(); + const result = await API.getApiKey(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -121,7 +122,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await api.getApiKey(); + await API.getApiKey(); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -147,7 +148,7 @@ describe("api.ts", () => { ])( `Workspaces - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => { - expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected); + expect(getURLWithSearchParams(basePath, filter)).toBe(expected); }, ); }); @@ -164,7 +165,7 @@ describe("api.ts", () => { ])( `Users - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => { - expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected); + expect(getURLWithSearchParams(basePath, filter)).toBe(expected); }, ); }); @@ -172,11 +173,11 @@ describe("api.ts", () => { describe("update", () => { it("creates a build with start and the latest template", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); - await api.updateWorkspace(MockWorkspace); - expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { + jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); + await API.updateWorkspace(MockWorkspace); + expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { transition: "start", template_version_id: MockTemplate.active_version_id, rich_parameter_values: [], @@ -185,12 +186,12 @@ describe("api.ts", () => { it("fails when having missing parameters", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValue(MockWorkspaceBuild); - jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate); - jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]); + jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]); jest - .spyOn(api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValue([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, @@ -198,14 +199,14 @@ describe("api.ts", () => { let error = new Error(); try { - await api.updateWorkspace(MockWorkspace); + await API.updateWorkspace(MockWorkspace); } catch (e) { error = e as Error; } - expect(error).toBeInstanceOf(api.MissingBuildParameters); + expect(error).toBeInstanceOf(MissingBuildParameters); // Verify if the correct missing parameters are being passed - expect((error as api.MissingBuildParameters).parameters).toEqual([ + expect((error as MissingBuildParameters).parameters).toEqual([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, ]); @@ -213,19 +214,19 @@ describe("api.ts", () => { it("creates a build with the no parameters if it is already filled", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); jest - .spyOn(api, "getWorkspaceBuildParameters") + .spyOn(API, "getWorkspaceBuildParameters") .mockResolvedValue([MockWorkspaceBuildParameter1]); jest - .spyOn(api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValue([ { ...MockTemplateVersionParameter1, required: true, mutable: false }, ]); - await api.updateWorkspace(MockWorkspace); - expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { + await API.updateWorkspace(MockWorkspace); + expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { transition: "start", template_version_id: MockTemplate.active_version_id, rich_parameter_values: [], diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c677ffbcb1b3b..ed7f18ef1472c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -19,32 +19,128 @@ * * For example, `utils/delay` must be imported using `../utils/delay` instead. */ -import globalAxios, { isAxiosError } from "axios"; +import globalAxios, { type AxiosInstance, isAxiosError } from "axios"; import type dayjs from "dayjs"; import userAgentParser from "ua-parser-js"; import { delay } from "../utils/delay"; import * as TypesGen from "./typesGenerated"; -export const axiosInstance = globalAxios.create(); +const getMissingParameters = ( + oldBuildParameters: TypesGen.WorkspaceBuildParameter[], + newBuildParameters: TypesGen.WorkspaceBuildParameter[], + templateParameters: TypesGen.TemplateVersionParameter[], +) => { + const missingParameters: TypesGen.TemplateVersionParameter[] = []; + const requiredParameters: TypesGen.TemplateVersionParameter[] = []; + + templateParameters.forEach((p) => { + // It is mutable and required. Mutable values can be changed after so we + // don't need to ask them if they are not required. + const isMutableAndRequired = p.mutable && p.required; + // Is immutable, so we can check if it is its first time on the build + const isImmutable = !p.mutable; + + if (isMutableAndRequired || isImmutable) { + requiredParameters.push(p); + } + }); + + for (const parameter of requiredParameters) { + // Check if there is a new value + let buildParameter = newBuildParameters.find( + (p) => p.name === parameter.name, + ); + + // If not, get the old one + if (!buildParameter) { + buildParameter = oldBuildParameters.find( + (p) => p.name === parameter.name, + ); + } + + // If there is a value from the new or old one, it is not missed + if (buildParameter) { + continue; + } + + missingParameters.push(parameter); + } + + // Check if parameter "options" changed and we can't use old build parameters. + templateParameters.forEach((templateParameter) => { + if (templateParameter.options.length === 0) { + return; + } + + // Check if there is a new value + let buildParameter = newBuildParameters.find( + (p) => p.name === templateParameter.name, + ); + + // If not, get the old one + if (!buildParameter) { + buildParameter = oldBuildParameters.find( + (p) => p.name === templateParameter.name, + ); + } + + if (!buildParameter) { + return; + } + + const matchingOption = templateParameter.options.find( + (option) => option.value === buildParameter?.value, + ); + if (!matchingOption) { + missingParameters.push(templateParameter); + } + }); + return missingParameters; +}; + +/** + * + * @param agentId + * @returns An EventSource that emits agent metadata event objects + * (ServerSentEvent) + */ +export const watchAgentMetadata = (agentId: string): EventSource => { + return new EventSource( + `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`, + { withCredentials: true }, + ); +}; -// Adds 304 for the default axios validateStatus function -// https://github.com/axios/axios#handling-errors Check status here -// https://httpstatusdogs.com/ -axiosInstance.defaults.validateStatus = (status) => { - return (status >= 200 && status < 300) || status === 304; +/** + * @returns {EventSource} An EventSource that emits workspace event objects + * (ServerSentEvent) + */ +export const watchWorkspace = (workspaceId: string): EventSource => { + return new EventSource( + `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`, + { withCredentials: true }, + ); }; -export const hardCodedCSRFCookie = (): string => { - // This is a hard coded CSRF token/cookie pair for local development. In prod, - // the GoLang webserver generates a random cookie with a new token for each - // document request. For local development, we don't use the Go webserver for - // static files, so this is the 'hack' to make local development work with - // remote apis. The CSRF cookie for this token is - // "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" - const csrfToken = - "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=="; - axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; - return csrfToken; +export const getURLWithSearchParams = ( + basePath: string, + options?: SearchParamOptions, +): string => { + if (!options) { + return basePath; + } + + const searchParams = new URLSearchParams(); + const keys = Object.keys(options) as (keyof SearchParamOptions)[]; + keys.forEach((key) => { + const value = options[key]; + if (value !== undefined && value !== "") { + searchParams.append(key, value.toString()); + } + }); + + const searchString = searchParams.toString(); + return searchString ? `${basePath}?${searchString}` : basePath; }; // withDefaultFeatures sets all unspecified features to not_entitled and @@ -57,1808 +153,1792 @@ export const withDefaultFeatures = ( if (fs[feature] !== undefined) { continue; } + fs[feature] = { enabled: false, entitlement: "not_entitled", }; } - return fs as TypesGen.Entitlements["features"]; -}; - -// Always attach CSRF token to all requests. In puppeteer the document is -// undefined. In those cases, just do nothing. -const token = - typeof document !== "undefined" - ? document.head.querySelector('meta[property="csrf-token"]') - : null; - -if (token !== null && token.getAttribute("content") !== null) { - if (process.env.NODE_ENV === "development") { - // Development mode uses a hard-coded CSRF token - axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] = - hardCodedCSRFCookie(); - token.setAttribute("content", hardCodedCSRFCookie()); - } else { - axiosInstance.defaults.headers.common["X-CSRF-TOKEN"] = - token.getAttribute("content") ?? ""; - } -} else { - // Do not write error logs if we are in a FE unit test. - if (process.env.JEST_WORKER_ID === undefined) { - console.error("CSRF token not found"); - } -} - -export const setSessionToken = (token: string) => { - axiosInstance.defaults.headers.common["Coder-Session-Token"] = token; -}; -export const setHost = (host?: string) => { - axiosInstance.defaults.baseURL = host; + return fs as TypesGen.Entitlements["features"]; }; -const CONTENT_TYPE_JSON = { - "Content-Type": "application/json", +type WatchBuildLogsByTemplateVersionIdOptions = { + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone?: () => void; + onError: (error: Error) => void; }; -export const provisioners: TypesGen.ProvisionerDaemon[] = [ - { - id: "terraform", - name: "Terraform", - created_at: "", - provisioners: [], - tags: {}, - version: "v2.34.5", - api_version: "1.0", - }, +export const watchBuildLogsByTemplateVersionId = ( + versionId: string, { - id: "cdr-basic", - name: "Basic", - created_at: "", - provisioners: [], - tags: {}, - version: "v2.34.5", - api_version: "1.0", - }, -]; - -export const login = async ( - email: string, - password: string, -): Promise => { - const payload = JSON.stringify({ - email, - password, - }); + onMessage, + onDone, + onError, + after, + }: WatchBuildLogsByTemplateVersionIdOptions, +) => { + const searchParams = new URLSearchParams({ follow: "true" }); + if (after !== undefined) { + searchParams.append("after", after.toString()); + } - const response = await axiosInstance.post( - "/api/v2/users/login", - payload, - { - headers: { ...CONTENT_TYPE_JSON }, - }, + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket( + `${proto}//${ + location.host + }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, ); - return response.data; -}; + socket.binaryType = "blob"; -export const convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { - const response = await axiosInstance.post( - "/api/v2/users/me/convert-login", - request, + socket.addEventListener("message", (event) => + onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); - return response.data; -}; -export const logout = async (): Promise => { - await axiosInstance.post("/api/v2/users/logout"); -}; + socket.addEventListener("error", () => { + onError(new Error("Connection for logs failed.")); + socket.close(); + }); -export const getAuthenticatedUser = async () => { - const response = await axiosInstance.get("/api/v2/users/me"); - return response.data; -}; + socket.addEventListener("close", () => { + // When the socket closes, logs have finished streaming! + onDone?.(); + }); -export const getUserParameters = async (templateID: string) => { - const response = await axiosInstance.get( - "/api/v2/users/me/autofill-parameters?template_id=" + templateID, - ); - return response.data; + return socket; }; -export const getAuthMethods = async (): Promise => { - const response = await axiosInstance.get( - "/api/v2/users/authmethods", - ); - return response.data; -}; +export const watchWorkspaceAgentLogs = ( + agentId: string, + { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions, +) => { + // WebSocket compression in Safari (confirmed in 16.5) is broken when + // the server sends large messages. The following error is seen: + // + // WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error + // + const noCompression = + userAgentParser(navigator.userAgent).browser.name === "Safari" + ? "&no_compression" + : ""; -export const getUserLoginType = async (): Promise => { - const response = await axiosInstance.get( - "/api/v2/users/me/login-type", + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket( + `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`, ); - return response.data; -}; + socket.binaryType = "blob"; -export const checkAuthorization = async ( - params: TypesGen.AuthorizationRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/authcheck`, - params, - ); - return response.data; -}; + socket.addEventListener("message", (event) => { + const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; + onMessage(logs); + }); -export const getApiKey = async (): Promise => { - const response = await axiosInstance.post( - "/api/v2/users/me/keys", - ); - return response.data; -}; + socket.addEventListener("error", () => { + onError(new Error("socket errored")); + }); -export const getTokens = async ( - params: TypesGen.TokensFilter, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/users/me/keys/tokens`, - { - params, - }, - ); - return response.data; -}; + socket.addEventListener("close", () => { + onDone && onDone(); + }); -export const deleteToken = async (keyId: string): Promise => { - await axiosInstance.delete("/api/v2/users/me/keys/" + keyId); + return socket; }; -export const createToken = async ( - params: TypesGen.CreateTokenRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/users/me/keys/tokens`, - params, - ); - return response.data; +type WatchWorkspaceAgentLogsOptions = { + after: number; + onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; + onDone?: () => void; + onError: (error: Error) => void; }; -export const getTokenConfig = async (): Promise => { - const response = await axiosInstance.get( - "/api/v2/users/me/keys/tokens/tokenconfig", - ); - return response.data; +type WatchBuildLogsByBuildIdOptions = { + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone?: () => void; + onError?: (error: Error) => void; }; - -export const getUsers = async ( - options: TypesGen.UsersRequest, - signal?: AbortSignal, -): Promise => { - const url = getURLWithSearchParams("/api/v2/users", options); - const response = await axiosInstance.get( - url.toString(), - { - signal, - }, +export const watchBuildLogsByBuildId = ( + buildId: string, + { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions, +) => { + const searchParams = new URLSearchParams({ follow: "true" }); + if (after !== undefined) { + searchParams.append("after", after.toString()); + } + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket( + `${proto}//${ + location.host + }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, ); - return response.data; -}; + socket.binaryType = "blob"; -export const getOrganization = async ( - organizationId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}`, + socket.addEventListener("message", (event) => + onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); - return response.data; -}; -export const getOrganizations = async (): Promise => { - const response = await axiosInstance.get( - "/api/v2/users/me/organizations", - ); - return response.data; -}; + socket.addEventListener("error", () => { + onError && onError(new Error("Connection for logs failed.")); + socket.close(); + }); -export const getTemplate = async ( - templateId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}`, - ); - return response.data; + socket.addEventListener("close", () => { + // When the socket closes, logs have finished streaming! + onDone && onDone(); + }); + + return socket; }; -export interface TemplateOptions { - readonly deprecated?: boolean; -} +// This is the base header that is used for several requests. This is defined as +// a readonly value, but only copies of it should be passed into the API calls, +// because Axios is able to mutate the headers +const BASE_CONTENT_TYPE_JSON = { + "Content-Type": "application/json", +} as const satisfies HeadersInit; -export const getTemplates = async ( - organizationId: string, - options?: TemplateOptions, -): Promise => { - const params = {} as Record; - if (options && options.deprecated !== undefined) { - // Just want to check if it isn't undefined. If it has - // a boolean value, convert it to a string and include - // it as a param. - params["deprecated"] = String(options.deprecated); - } +type TemplateOptions = Readonly<{ + readonly deprecated?: boolean; +}>; - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates`, - { - params, - }, - ); - return response.data; +type SearchParamOptions = TypesGen.Pagination & { + q?: string; }; -export const getTemplateByName = async ( - organizationId: string, - name: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates/${name}`, - ); - return response.data; -}; +type RestartWorkspaceParameters = Readonly<{ + workspace: TypesGen.Workspace; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; +}>; -export const getTemplateVersion = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}`, - ); - return response.data; -}; - -export const getTemplateVersionResources = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/resources`, - ); - return response.data; -}; +export type DeleteWorkspaceOptions = Pick< + TypesGen.CreateWorkspaceBuildRequest, + "log_level" & "orphan" +>; -export const getTemplateVersionVariables = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/variables`, - ); - return response.data; -}; +export type DeploymentConfig = Readonly<{ + config: TypesGen.DeploymentValues; + options: TypesGen.SerpentOption[]; +}>; -export const getTemplateVersions = async ( - templateId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}/versions`, - ); - return response.data; +type Claims = { + license_expires: number; + account_type?: string; + account_id?: string; + trial: boolean; + all_features: boolean; + version: number; + features: Record; + require_telemetry?: boolean; }; -export const getTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, - ); - return response.data; +export type GetLicensesResponse = Omit & { + claims: Claims; + expires_at: string; }; -export type GetPreviousTemplateVersionByNameResponse = - | TypesGen.TemplateVersion - | undefined; - -export const getPreviousTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, -) => { - try { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`, - ); - return response.data; - } catch (error) { - // When there is no previous version, like the first version of a template, - // the API returns 404 so in this case we can safely return undefined - if ( - isAxiosError(error) && - error.response && - error.response.status === 404 - ) { - return undefined; - } - - throw error; - } +export type InsightsParams = { + start_time: string; + end_time: string; + template_ids: string; }; -export const createTemplateVersion = async ( - organizationId: string, - data: TypesGen.CreateTemplateVersionRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/templateversions`, - data, - ); - return response.data; +export type InsightsTemplateParams = InsightsParams & { + interval: "day" | "week"; }; -export const getTemplateVersionExternalAuth = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/external-auth`, - ); - return response.data; +export type GetJFrogXRayScanParams = { + workspaceId: string; + agentId: string; }; -export const getTemplateVersionRichParameters = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/rich-parameters`, - ); - return response.data; -}; +export class MissingBuildParameters extends Error { + parameters: TypesGen.TemplateVersionParameter[] = []; + versionId: string; -export const createTemplate = async ( - organizationId: string, - data: TypesGen.CreateTemplateRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/templates`, - data, - ); - return response.data; -}; + constructor( + parameters: TypesGen.TemplateVersionParameter[], + versionId: string, + ) { + super("Missing build parameters."); + this.parameters = parameters; + this.versionId = versionId; + } +} -export const updateActiveTemplateVersion = async ( - templateId: string, - data: TypesGen.UpdateActiveTemplateVersion, -) => { - const response = await axiosInstance.patch( - `/api/v2/templates/${templateId}/versions`, - data, - ); - return response.data; -}; +/** + * This is the container for all API methods. It's split off to make it more + * clear where API methods should go, but it is eventually merged into the Api + * class with a more flat hierarchy + * + * All public methods should be defined as arrow functions to ensure that they + * can be passed around the React UI without losing their `this` context. + * + * This is one of the few cases where you have to worry about the difference + * between traditional methods and arrow function properties. Arrow functions + * disable JS's dynamic scope, and force all `this` references to resolve via + * lexical scope. + */ +class ApiMethods { + constructor(protected readonly axios: AxiosInstance) {} + + login = async ( + email: string, + password: string, + ): Promise => { + const payload = JSON.stringify({ email, password }); + const response = await this.axios.post( + "/api/v2/users/login", + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); -export const patchTemplateVersion = async ( - templateVersionId: string, - data: TypesGen.PatchTemplateVersionRequest, -) => { - const response = await axiosInstance.patch( - `/api/v2/templateversions/${templateVersionId}`, - data, - ); - return response.data; -}; + return response.data; + }; -export const archiveTemplateVersion = async (templateVersionId: string) => { - const response = await axiosInstance.post( - `/api/v2/templateversions/${templateVersionId}/archive`, - ); - return response.data; -}; + convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { + const response = await this.axios.post( + "/api/v2/users/me/convert-login", + request, + ); -export const unarchiveTemplateVersion = async (templateVersionId: string) => { - const response = await axiosInstance.post( - `/api/v2/templateversions/${templateVersionId}/unarchive`, - ); - return response.data; -}; + return response.data; + }; -export const updateTemplateMeta = async ( - templateId: string, - data: TypesGen.UpdateTemplateMeta, -): Promise => { - const response = await axiosInstance.patch( - `/api/v2/templates/${templateId}`, - data, - ); - // On 304 response there is no data payload. - if (response.status === 304) { - return null; - } + logout = async (): Promise => { + return this.axios.post("/api/v2/users/logout"); + }; - return response.data; -}; + getAuthenticatedUser = async () => { + const response = await this.axios.get("/api/v2/users/me"); + return response.data; + }; -export const deleteTemplate = async ( - templateId: string, -): Promise => { - const response = await axiosInstance.delete( - `/api/v2/templates/${templateId}`, - ); - return response.data; -}; + getUserParameters = async (templateID: string) => { + const response = await this.axios.get( + `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + ); -export const getWorkspace = async ( - workspaceId: string, - params?: TypesGen.WorkspaceOptions, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspaces/${workspaceId}`, - { - params, - }, - ); - return response.data; -}; + return response.data; + }; -/** - * - * @param workspaceId - * @returns An EventSource that emits workspace event objects (ServerSentEvent) - */ -export const watchWorkspace = (workspaceId: string): EventSource => { - return new EventSource( - `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`, - { withCredentials: true }, - ); -}; + getAuthMethods = async (): Promise => { + const response = await this.axios.get( + "/api/v2/users/authmethods", + ); -interface SearchParamOptions extends TypesGen.Pagination { - q?: string; -} + return response.data; + }; -export const getURLWithSearchParams = ( - basePath: string, - options?: SearchParamOptions, -): string => { - if (options) { - const searchParams = new URLSearchParams(); - const keys = Object.keys(options) as (keyof SearchParamOptions)[]; - keys.forEach((key) => { - const value = options[key]; - if (value !== undefined && value !== "") { - searchParams.append(key, value.toString()); - } - }); - const searchString = searchParams.toString(); - return searchString ? `${basePath}?${searchString}` : basePath; - } else { - return basePath; - } -}; + getUserLoginType = async (): Promise => { + const response = await this.axios.get( + "/api/v2/users/me/login-type", + ); -export const getWorkspaces = async ( - options: TypesGen.WorkspacesRequest, -): Promise => { - const url = getURLWithSearchParams("/api/v2/workspaces", options); - const response = await axiosInstance.get(url); - return response.data; -}; + return response.data; + }; -export const getWorkspaceByOwnerAndName = async ( - username = "me", - workspaceName: string, - params?: TypesGen.WorkspaceOptions, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/users/${username}/workspace/${workspaceName}`, - { + checkAuthorization = async ( + params: TypesGen.AuthorizationRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/authcheck`, params, - }, - ); - return response.data; -}; + ); -export function waitForBuild(build: TypesGen.WorkspaceBuild) { - return new Promise((res, reject) => { - void (async () => { - let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; - - while ( - !["succeeded", "canceled"].some( - (status) => latestJobInfo?.status.includes(status), - ) - ) { - const { job } = await getWorkspaceBuildByNumber( - build.workspace_owner_name, - build.workspace_name, - build.build_number, - ); - latestJobInfo = job; - - if (latestJobInfo.status === "failed") { - return reject(latestJobInfo); - } + return response.data; + }; - await delay(1000); - } + getApiKey = async (): Promise => { + const response = await this.axios.post( + "/api/v2/users/me/keys", + ); - return res(latestJobInfo); - })(); - }); -} + return response.data; + }; -export const postWorkspaceBuild = async ( - workspaceId: string, - data: TypesGen.CreateWorkspaceBuildRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/workspaces/${workspaceId}/builds`, - data, - ); - return response.data; -}; + getTokens = async ( + params: TypesGen.TokensFilter, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/me/keys/tokens`, + { params }, + ); -export const startWorkspace = ( - workspaceId: string, - templateVersionId: string, - logLevel?: TypesGen.ProvisionerLogLevel, - buildParameters?: TypesGen.WorkspaceBuildParameter[], -) => - postWorkspaceBuild(workspaceId, { - transition: "start", - template_version_id: templateVersionId, - log_level: logLevel, - rich_parameter_values: buildParameters, - }); -export const stopWorkspace = ( - workspaceId: string, - logLevel?: TypesGen.ProvisionerLogLevel, -) => - postWorkspaceBuild(workspaceId, { - transition: "stop", - log_level: logLevel, - }); + return response.data; + }; -export type DeleteWorkspaceOptions = Pick< - TypesGen.CreateWorkspaceBuildRequest, - "log_level" & "orphan" ->; + deleteToken = async (keyId: string): Promise => { + await this.axios.delete("/api/v2/users/me/keys/" + keyId); + }; -export const deleteWorkspace = ( - workspaceId: string, - options?: DeleteWorkspaceOptions, -) => - postWorkspaceBuild(workspaceId, { - transition: "delete", - ...options, - }); + createToken = async ( + params: TypesGen.CreateTokenRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/users/me/keys/tokens`, + params, + ); -export const cancelWorkspaceBuild = async ( - workspaceBuildId: TypesGen.WorkspaceBuild["id"], -): Promise => { - const response = await axiosInstance.patch( - `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, - ); - return response.data; -}; + return response.data; + }; + + getTokenConfig = async (): Promise => { + const response = await this.axios.get( + "/api/v2/users/me/keys/tokens/tokenconfig", + ); -export const updateWorkspaceDormancy = async ( - workspaceId: string, - dormant: boolean, -): Promise => { - const data: TypesGen.UpdateWorkspaceDormancy = { - dormant: dormant, + return response.data; }; - const response = await axiosInstance.put( - `/api/v2/workspaces/${workspaceId}/dormant`, - data, - ); - return response.data; -}; + getUsers = async ( + options: TypesGen.UsersRequest, + signal?: AbortSignal, + ): Promise => { + const url = getURLWithSearchParams("/api/v2/users", options); + const response = await this.axios.get( + url.toString(), + { signal }, + ); -export const updateWorkspaceAutomaticUpdates = async ( - workspaceId: string, - automaticUpdates: TypesGen.AutomaticUpdates, -): Promise => { - const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { - automatic_updates: automaticUpdates, + return response.data; }; - const response = await axiosInstance.put( - `/api/v2/workspaces/${workspaceId}/autoupdates`, - req, - ); - return response.data; -}; + getOrganization = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}`, + ); -export const restartWorkspace = async ({ - workspace, - buildParameters, -}: { - workspace: TypesGen.Workspace; - buildParameters?: TypesGen.WorkspaceBuildParameter[]; -}) => { - const stopBuild = await stopWorkspace(workspace.id); - const awaitedStopBuild = await waitForBuild(stopBuild); + return response.data; + }; - // If the restart is canceled halfway through, make sure we bail - if (awaitedStopBuild?.status === "canceled") { - return; - } + getOrganizations = async (): Promise => { + const response = await this.axios.get( + "/api/v2/users/me/organizations", + ); + return response.data; + }; - const startBuild = await startWorkspace( - workspace.id, - workspace.latest_build.template_version_id, - undefined, - buildParameters, - ); - await waitForBuild(startBuild); -}; + getTemplate = async (templateId: string): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}`, + ); -export const cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"], -): Promise => { - const response = await axiosInstance.patch( - `/api/v2/templateversions/${templateVersionId}/cancel`, - ); - return response.data; -}; + return response.data; + }; -export const createUser = async ( - user: TypesGen.CreateUserRequest, -): Promise => { - const response = await axiosInstance.post( - "/api/v2/users", - user, - ); - return response.data; -}; + getTemplates = async ( + organizationId: string, + options?: TemplateOptions, + ): Promise => { + const params: Record = {}; + if (options?.deprecated !== undefined) { + // Just want to check if it isn't undefined. If it has + // a boolean value, convert it to a string and include + // it as a param. + params["deprecated"] = String(options.deprecated); + } -export const createWorkspace = async ( - organizationId: string, - userId = "me", - workspace: TypesGen.CreateWorkspaceRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, - workspace, - ); - return response.data; -}; + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates`, + { params }, + ); -export const patchWorkspace = async ( - workspaceId: string, - data: TypesGen.UpdateWorkspaceRequest, -) => { - await axiosInstance.patch(`/api/v2/workspaces/${workspaceId}`, data); -}; + return response.data; + }; -export const getBuildInfo = async (): Promise => { - const response = await axiosInstance.get("/api/v2/buildinfo"); - return response.data; -}; + getTemplateByName = async ( + organizationId: string, + name: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${name}`, + ); -export const getUpdateCheck = - async (): Promise => { - const response = await axiosInstance.get("/api/v2/updatecheck"); return response.data; }; -export const putWorkspaceAutostart = async ( - workspaceID: string, - autostart: TypesGen.UpdateWorkspaceAutostartRequest, -): Promise => { - const payload = JSON.stringify(autostart); - await axiosInstance.put( - `/api/v2/workspaces/${workspaceID}/autostart`, - payload, - { - headers: { ...CONTENT_TYPE_JSON }, - }, - ); -}; + getTemplateVersion = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}`, + ); -export const putWorkspaceAutostop = async ( - workspaceID: string, - ttl: TypesGen.UpdateWorkspaceTTLRequest, -): Promise => { - const payload = JSON.stringify(ttl); - await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { - headers: { ...CONTENT_TYPE_JSON }, - }); -}; + return response.data; + }; -export const updateProfile = async ( - userId: string, - data: TypesGen.UpdateUserProfileRequest, -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/profile`, - data, - ); - return response.data; -}; + getTemplateVersionResources = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/resources`, + ); -export const updateAppearanceSettings = async ( - userId: string, - data: TypesGen.UpdateUserAppearanceSettingsRequest, -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/appearance`, - data, - ); - return response.data; -}; + return response.data; + }; -export const getUserQuietHoursSchedule = async ( - userId: TypesGen.User["id"], -): Promise => { - const response = await axiosInstance.get( - `/api/v2/users/${userId}/quiet-hours`, - ); - return response.data; -}; + getTemplateVersionVariables = async ( + versionId: string, + ): Promise => { + // Defined as separate variable to avoid wonky Prettier formatting because + // the type definition is so long + type VerArray = TypesGen.TemplateVersionVariable[]; -export const updateUserQuietHoursSchedule = async ( - userId: TypesGen.User["id"], - data: TypesGen.UpdateUserQuietHoursScheduleRequest, -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/quiet-hours`, - data, - ); - return response.data; -}; + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/variables`, + ); -export const activateUser = async ( - userId: TypesGen.User["id"], -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/status/activate`, - ); - return response.data; -}; + return response.data; + }; -export const suspendUser = async ( - userId: TypesGen.User["id"], -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/status/suspend`, - ); - return response.data; -}; + getTemplateVersions = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/versions`, + ); + return response.data; + }; -export const deleteUser = async ( - userId: TypesGen.User["id"], -): Promise => { - return await axiosInstance.delete(`/api/v2/users/${userId}`); -}; + getTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, + ); -// API definition: -// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 -export const hasFirstUser = async (): Promise => { - try { - // If it is success, it is true - await axiosInstance.get("/api/v2/users/first"); - return true; - } catch (error) { - // If it returns a 404, it is false - if (isAxiosError(error) && error.response?.status === 404) { - return false; - } + return response.data; + }; - throw error; - } -}; + getPreviousTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ) => { + try { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`, + ); -export const createFirstUser = async ( - req: TypesGen.CreateFirstUserRequest, -): Promise => { - const response = await axiosInstance.post(`/api/v2/users/first`, req); - return response.data; -}; + return response.data; + } catch (error) { + // When there is no previous version, like the first version of a + // template, the API returns 404 so in this case we can safely return + // undefined + const is404 = + isAxiosError(error) && error.response && error.response.status === 404; + + if (is404) { + return undefined; + } -export const updateUserPassword = async ( - userId: TypesGen.User["id"], - updatePassword: TypesGen.UpdateUserPasswordRequest, -): Promise => - axiosInstance.put(`/api/v2/users/${userId}/password`, updatePassword); + throw error; + } + }; -export const getRoles = async (): Promise> => { - const response = - await axiosInstance.get>( - `/api/v2/users/roles`, + createTemplateVersion = async ( + organizationId: string, + data: TypesGen.CreateTemplateVersionRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templateversions`, + data, ); - return response.data; -}; -export const updateUserRoles = async ( - roles: TypesGen.Role["name"][], - userId: TypesGen.User["id"], -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/roles`, - { roles }, - ); - return response.data; -}; + return response.data; + }; -export const getUserSSHKey = async ( - userId = "me", -): Promise => { - const response = await axiosInstance.get( - `/api/v2/users/${userId}/gitsshkey`, - ); - return response.data; -}; + getTemplateVersionExternalAuth = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/external-auth`, + ); -export const regenerateUserSSHKey = async ( - userId = "me", -): Promise => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/gitsshkey`, - ); - return response.data; -}; + return response.data; + }; -export const getWorkspaceBuilds = async ( - workspaceId: string, - req?: TypesGen.WorkspaceBuildsRequest, -) => { - const response = await axiosInstance.get( - getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), - ); - return response.data; -}; + getTemplateVersionRichParameters = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ); + return response.data; + }; -export const getWorkspaceBuildByNumber = async ( - username = "me", - workspaceName: string, - buildNumber: number, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, - ); - return response.data; -}; + createTemplate = async ( + organizationId: string, + data: TypesGen.CreateTemplateRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templates`, + data, + ); -export const getWorkspaceBuildLogs = async ( - buildId: string, - before: Date, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, - ); - return response.data; -}; + return response.data; + }; -export const getWorkspaceAgentLogs = async ( - agentID: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspaceagents/${agentID}/logs`, - ); - return response.data; -}; + updateActiveTemplateVersion = async ( + templateId: string, + data: TypesGen.UpdateActiveTemplateVersion, + ) => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}/versions`, + data, + ); + return response.data; + }; -export const putWorkspaceExtension = async ( - workspaceId: string, - newDeadline: dayjs.Dayjs, -): Promise => { - await axiosInstance.put(`/api/v2/workspaces/${workspaceId}/extend`, { - deadline: newDeadline, - }); -}; + patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, + ) => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}`, + data, + ); -export const refreshEntitlements = async (): Promise => { - await axiosInstance.post("/api/v2/licenses/refresh-entitlements"); -}; + return response.data; + }; -export const getEntitlements = async (): Promise => { - try { - const response = await axiosInstance.get("/api/v2/entitlements"); - return response.data; - } catch (ex) { - if (isAxiosError(ex) && ex.response?.status === 404) { - return { - errors: [], - features: withDefaultFeatures({}), - has_license: false, - require_telemetry: false, - trial: false, - warnings: [], - refreshed_at: "", - }; - } - throw ex; - } -}; + archiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/archive`, + ); -export const getExperiments = async (): Promise => { - try { - const response = await axiosInstance.get("/api/v2/experiments"); return response.data; - } catch (error) { - if (isAxiosError(error) && error.response?.status === 404) { - return []; - } - throw error; - } -}; + }; -export const getAvailableExperiments = - async (): Promise => { - try { - const response = await axiosInstance.get("/api/v2/experiments/available"); - return response.data; - } catch (error) { - if (isAxiosError(error) && error.response?.status === 404) { - return { safe: [] }; - } - throw error; - } + unarchiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/unarchive`, + ); + return response.data; }; -export const getExternalAuthProvider = async ( - provider: string, -): Promise => { - const resp = await axiosInstance.get(`/api/v2/external-auth/${provider}`); - return resp.data; -}; + updateTemplateMeta = async ( + templateId: string, + data: TypesGen.UpdateTemplateMeta, + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}`, + data, + ); -export const getExternalAuthDevice = async ( - provider: string, -): Promise => { - const resp = await axiosInstance.get( - `/api/v2/external-auth/${provider}/device`, - ); - return resp.data; -}; + // On 304 response there is no data payload. + if (response.status === 304) { + return null; + } -export const exchangeExternalAuthDevice = async ( - provider: string, - req: TypesGen.ExternalAuthDeviceExchange, -): Promise => { - const resp = await axiosInstance.post( - `/api/v2/external-auth/${provider}/device`, - req, - ); - return resp.data; -}; + return response.data; + }; -export const getUserExternalAuthProviders = - async (): Promise => { - const resp = await axiosInstance.get(`/api/v2/external-auth`); - return resp.data; + deleteTemplate = async (templateId: string): Promise => { + const response = await this.axios.delete( + `/api/v2/templates/${templateId}`, + ); + + return response.data; }; -export const unlinkExternalAuthProvider = async ( - provider: string, -): Promise => { - const resp = await axiosInstance.delete(`/api/v2/external-auth/${provider}`); - return resp.data; -}; + getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}`, + { params }, + ); -export const getOAuth2ProviderApps = async ( - filter?: TypesGen.OAuth2ProviderAppFilter, -): Promise => { - const params = filter?.user_id - ? new URLSearchParams({ user_id: filter.user_id }) - : ""; - const resp = await axiosInstance.get( - `/api/v2/oauth2-provider/apps?${params}`, - ); - return resp.data; -}; + return response.data; + }; -export const getOAuth2ProviderApp = async ( - id: string, -): Promise => { - const resp = await axiosInstance.get(`/api/v2/oauth2-provider/apps/${id}`); - return resp.data; -}; + getWorkspaces = async ( + options: TypesGen.WorkspacesRequest, + ): Promise => { + const url = getURLWithSearchParams("/api/v2/workspaces", options); + const response = await this.axios.get(url); + return response.data; + }; -export const postOAuth2ProviderApp = async ( - data: TypesGen.PostOAuth2ProviderAppRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/oauth2-provider/apps`, - data, - ); - return response.data; -}; + getWorkspaceByOwnerAndName = async ( + username = "me", + workspaceName: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}`, + { params }, + ); -export const putOAuth2ProviderApp = async ( - id: string, - data: TypesGen.PutOAuth2ProviderAppRequest, -): Promise => { - const response = await axiosInstance.put( - `/api/v2/oauth2-provider/apps/${id}`, - data, - ); - return response.data; -}; + return response.data; + }; -export const deleteOAuth2ProviderApp = async (id: string): Promise => { - await axiosInstance.delete(`/api/v2/oauth2-provider/apps/${id}`); -}; + getWorkspaceBuildByNumber = async ( + username = "me", + workspaceName: string, + buildNumber: number, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ); -export const getOAuth2ProviderAppSecrets = async ( - id: string, -): Promise => { - const resp = await axiosInstance.get( - `/api/v2/oauth2-provider/apps/${id}/secrets`, - ); - return resp.data; -}; + return response.data; + }; -export const postOAuth2ProviderAppSecret = async ( - id: string, -): Promise => { - const resp = await axiosInstance.post( - `/api/v2/oauth2-provider/apps/${id}/secrets`, - ); - return resp.data; -}; + waitForBuild = (build: TypesGen.WorkspaceBuild) => { + return new Promise((res, reject) => { + void (async () => { + let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; + + while ( + !["succeeded", "canceled"].some( + (status) => latestJobInfo?.status.includes(status), + ) + ) { + const { job } = await this.getWorkspaceBuildByNumber( + build.workspace_owner_name, + build.workspace_name, + build.build_number, + ); + + latestJobInfo = job; + if (latestJobInfo.status === "failed") { + return reject(latestJobInfo); + } + + await delay(1000); + } -export const deleteOAuth2ProviderAppSecret = async ( - appId: string, - secretId: string, -): Promise => { - await axiosInstance.delete( - `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, - ); -}; + return res(latestJobInfo); + })(); + }); + }; -export const revokeOAuth2ProviderApp = async (appId: string): Promise => { - await axiosInstance.delete(`/oauth2/tokens?client_id=${appId}`); -}; + postWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.CreateWorkspaceBuildRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ); -export const getAuditLogs = async ( - options: TypesGen.AuditLogsRequest, -): Promise => { - const url = getURLWithSearchParams("/api/v2/audit", options); - const response = await axiosInstance.get(url); - return response.data; -}; + return response.data; + }; -export const getTemplateDAUs = async ( - templateId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}/daus`, - ); - return response.data; -}; + startWorkspace = ( + workspaceId: string, + templateVersionId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + buildParameters?: TypesGen.WorkspaceBuildParameter[], + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: "start", + template_version_id: templateVersionId, + log_level: logLevel, + rich_parameter_values: buildParameters, + }); + }; -export const getDeploymentDAUs = async ( - // Default to user's local timezone. - // As /api/v2/insights/daus only accepts whole-number values for tz_offset - // we truncate the tz offset down to the closest hour. - offset = Math.trunc(new Date().getTimezoneOffset() / 60), -): Promise => { - const response = await axiosInstance.get( - `/api/v2/insights/daus?tz_offset=${offset}`, - ); - return response.data; -}; + stopWorkspace = ( + workspaceId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: "stop", + log_level: logLevel, + }); + }; -export const getTemplateACLAvailable = async ( - templateId: string, - options: TypesGen.UsersRequest, -): Promise => { - const url = getURLWithSearchParams( - `/api/v2/templates/${templateId}/acl/available`, - options, - ); - const response = await axiosInstance.get(url.toString()); - return response.data; -}; + deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { + return this.postWorkspaceBuild(workspaceId, { + transition: "delete", + ...options, + }); + }; -export const getTemplateACL = async ( - templateId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}/acl`, - ); - return response.data; -}; + cancelWorkspaceBuild = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], + ): Promise => { + const response = await this.axios.patch( + `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + ); -export const updateTemplateACL = async ( - templateId: string, - data: TypesGen.UpdateTemplateACL, -): Promise<{ message: string }> => { - const response = await axiosInstance.patch( - `/api/v2/templates/${templateId}/acl`, - data, - ); - return response.data; -}; + return response.data; + }; + + updateWorkspaceDormancy = async ( + workspaceId: string, + dormant: boolean, + ): Promise => { + const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/dormant`, + data, + ); -export const getApplicationsHost = - async (): Promise => { - const response = await axiosInstance.get(`/api/v2/applications/host`); return response.data; }; -export const getGroups = async ( - organizationId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/groups`, - ); - return response.data; -}; + updateWorkspaceAutomaticUpdates = async ( + workspaceId: string, + automaticUpdates: TypesGen.AutomaticUpdates, + ): Promise => { + const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { + automatic_updates: automaticUpdates, + }; -export const createGroup = async ( - organizationId: string, - data: TypesGen.CreateGroupRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/groups`, - data, - ); - return response.data; -}; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/autoupdates`, + req, + ); -export const getGroup = async (groupId: string): Promise => { - const response = await axiosInstance.get(`/api/v2/groups/${groupId}`); - return response.data; -}; + return response.data; + }; -export const patchGroup = async ( - groupId: string, - data: TypesGen.PatchGroupRequest, -): Promise => { - const response = await axiosInstance.patch(`/api/v2/groups/${groupId}`, data); - return response.data; -}; + restartWorkspace = async ({ + workspace, + buildParameters, + }: RestartWorkspaceParameters): Promise => { + const stopBuild = await this.stopWorkspace(workspace.id); + const awaitedStopBuild = await this.waitForBuild(stopBuild); -export const addMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { - name: "", - add_users: [userId], - remove_users: [], - }); -}; + // If the restart is canceled halfway through, make sure we bail + if (awaitedStopBuild?.status === "canceled") { + return; + } -export const removeMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { - name: "", - display_name: "", - add_users: [], - remove_users: [userId], - }); -}; + const startBuild = await this.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + undefined, + buildParameters, + ); -export const deleteGroup = async (groupId: string): Promise => { - await axiosInstance.delete(`/api/v2/groups/${groupId}`); -}; + await this.waitForBuild(startBuild); + }; -export const getWorkspaceQuota = async ( - username: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspace-quota/${encodeURIComponent(username)}`, - ); - return response.data; -}; + cancelTemplateVersionBuild = async ( + templateVersionId: TypesGen.TemplateVersion["id"], + ): Promise => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ); -export const getAgentListeningPorts = async ( - agentID: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspaceagents/${agentID}/listening-ports`, - ); - return response.data; -}; + return response.data; + }; -export const getWorkspaceAgentSharedPorts = async ( - workspaceID: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspaces/${workspaceID}/port-share`, - ); - return response.data; -}; + createUser = async ( + user: TypesGen.CreateUserRequest, + ): Promise => { + const response = await this.axios.post( + "/api/v2/users", + user, + ); -export const upsertWorkspaceAgentSharedPort = async ( - workspaceID: string, - req: TypesGen.UpsertWorkspaceAgentPortShareRequest, -): Promise => { - const response = await axiosInstance.post( - `/api/v2/workspaces/${workspaceID}/port-share`, - req, - ); - return response.data; -}; + return response.data; + }; -export const deleteWorkspaceAgentSharedPort = async ( - workspaceID: string, - req: TypesGen.DeleteWorkspaceAgentPortShareRequest, -): Promise => { - const response = await axiosInstance.delete( - `/api/v2/workspaces/${workspaceID}/port-share`, - { - data: req, - }, - ); - return response.data; -}; + createWorkspace = async ( + organizationId: string, + userId = "me", + workspace: TypesGen.CreateWorkspaceRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + workspace, + ); -// getDeploymentSSHConfig is used by the VSCode-Extension. -export const getDeploymentSSHConfig = - async (): Promise => { - const response = await axiosInstance.get(`/api/v2/deployment/ssh`); return response.data; }; -export type DeploymentConfig = { - readonly config: TypesGen.DeploymentValues; - readonly options: TypesGen.SerpentOption[]; -}; + patchWorkspace = async ( + workspaceId: string, + data: TypesGen.UpdateWorkspaceRequest, + ): Promise => { + await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + }; -export const getDeploymentConfig = async (): Promise => { - const response = await axiosInstance.get(`/api/v2/deployment/config`); - return response.data; -}; + getBuildInfo = async (): Promise => { + const response = await this.axios.get("/api/v2/buildinfo"); + return response.data; + }; -export const getDeploymentStats = - async (): Promise => { - const response = await axiosInstance.get(`/api/v2/deployment/stats`); + getUpdateCheck = async (): Promise => { + const response = await this.axios.get("/api/v2/updatecheck"); return response.data; }; -export const getReplicas = async (): Promise => { - const response = await axiosInstance.get(`/api/v2/replicas`); - return response.data; -}; + putWorkspaceAutostart = async ( + workspaceID: string, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, + ): Promise => { + const payload = JSON.stringify(autostart); + await this.axios.put( + `/api/v2/workspaces/${workspaceID}/autostart`, + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + }; -export const getFile = async (fileId: string): Promise => { - const response = await axiosInstance.get( - `/api/v2/files/${fileId}`, - { - responseType: "arraybuffer", - }, - ); - return response.data; -}; + putWorkspaceAutostop = async ( + workspaceID: string, + ttl: TypesGen.UpdateWorkspaceTTLRequest, + ): Promise => { + const payload = JSON.stringify(ttl); + await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { + headers: { ...BASE_CONTENT_TYPE_JSON }, + }); + }; -export const getWorkspaceProxyRegions = async (): Promise< - TypesGen.RegionsResponse -> => { - const response = - await axiosInstance.get>( - `/api/v2/regions`, + updateProfile = async ( + userId: string, + data: TypesGen.UpdateUserProfileRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/profile`, + data, ); - return response.data; -}; + return response.data; + }; -export const getWorkspaceProxies = async (): Promise< - TypesGen.RegionsResponse -> => { - const response = await axiosInstance.get< - TypesGen.RegionsResponse - >(`/api/v2/workspaceproxies`); - return response.data; -}; + updateAppearanceSettings = async ( + userId: string, + data: TypesGen.UpdateUserAppearanceSettingsRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/appearance`, + data, + ); + return response.data; + }; -export const createWorkspaceProxy = async ( - b: TypesGen.CreateWorkspaceProxyRequest, -): Promise => { - const response = await axiosInstance.post(`/api/v2/workspaceproxies`, b); - return response.data; -}; + getUserQuietHoursSchedule = async ( + userId: TypesGen.User["id"], + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${userId}/quiet-hours`, + ); + return response.data; + }; -export const getAppearance = async (): Promise => { - try { - const response = await axiosInstance.get(`/api/v2/appearance`); - return response.data || {}; - } catch (ex) { - if (isAxiosError(ex) && ex.response?.status === 404) { - return { - application_name: "", - logo_url: "", - service_banner: { - enabled: false, - }, - notification_banners: [], - }; - } - throw ex; - } -}; + updateUserQuietHoursSchedule = async ( + userId: TypesGen.User["id"], + data: TypesGen.UpdateUserQuietHoursScheduleRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/quiet-hours`, + data, + ); -export const updateAppearance = async ( - b: TypesGen.AppearanceConfig, -): Promise => { - const response = await axiosInstance.put(`/api/v2/appearance`, b); - return response.data; -}; + return response.data; + }; -export const getTemplateExamples = async ( - organizationId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates/examples`, - ); - return response.data; -}; + activateUser = async ( + userId: TypesGen.User["id"], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/activate`, + ); + return response.data; + }; -export const uploadFile = async ( - file: File, -): Promise => { - const response = await axiosInstance.post("/api/v2/files", file, { - headers: { - "Content-Type": "application/x-tar", - }, - }); - return response.data; -}; + suspendUser = async (userId: TypesGen.User["id"]): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/suspend`, + ); -export const getTemplateVersionLogs = async ( - versionId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/logs`, - ); - return response.data; -}; + return response.data; + }; -export const updateWorkspaceVersion = async ( - workspace: TypesGen.Workspace, -): Promise => { - const template = await getTemplate(workspace.template_id); - return startWorkspace(workspace.id, template.active_version_id); -}; + deleteUser = async (userId: TypesGen.User["id"]): Promise => { + await this.axios.delete(`/api/v2/users/${userId}`); + }; -export const getWorkspaceBuildParameters = async ( - workspaceBuildId: TypesGen.WorkspaceBuild["id"], -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, - ); - return response.data; -}; -type Claims = { - license_expires: number; - account_type?: string; - account_id?: string; - trial: boolean; - all_features: boolean; - version: number; - features: Record; - require_telemetry?: boolean; -}; + // API definition: + // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 + hasFirstUser = async (): Promise => { + try { + // If it is success, it is true + await this.axios.get("/api/v2/users/first"); + return true; + } catch (error) { + // If it returns a 404, it is false + if (isAxiosError(error) && error.response?.status === 404) { + return false; + } -export type GetLicensesResponse = Omit & { - claims: Claims; - expires_at: string; -}; + throw error; + } + }; -export const getLicenses = async (): Promise => { - const response = await axiosInstance.get(`/api/v2/licenses`); - return response.data; -}; + createFirstUser = async ( + req: TypesGen.CreateFirstUserRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/users/first`, req); + return response.data; + }; -export const createLicense = async ( - data: TypesGen.AddLicenseRequest, -): Promise => { - const response = await axiosInstance.post(`/api/v2/licenses`, data); - return response.data; -}; + updateUserPassword = async ( + userId: TypesGen.User["id"], + updatePassword: TypesGen.UpdateUserPasswordRequest, + ): Promise => { + await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); + }; -export const removeLicense = async (licenseId: number): Promise => { - await axiosInstance.delete(`/api/v2/licenses/${licenseId}`); -}; + getRoles = async (): Promise> => { + const response = + await this.axios.get(`/api/v2/users/roles`); -export class MissingBuildParameters extends Error { - parameters: TypesGen.TemplateVersionParameter[] = []; - versionId: string; + return response.data; + }; - constructor( - parameters: TypesGen.TemplateVersionParameter[], - versionId: string, - ) { - super("Missing build parameters."); - this.parameters = parameters; - this.versionId = versionId; - } -} + updateUserRoles = async ( + roles: TypesGen.Role["name"][], + userId: TypesGen.User["id"], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/roles`, + { roles }, + ); -/** Steps to change the workspace version - * - Get the latest template to access the latest active version - * - Get the current build parameters - * - Get the template parameters - * - Update the build parameters and check if there are missed parameters for the new version - * - If there are missing parameters raise an error - * - Create a build with the version and updated build parameters - */ -export const changeWorkspaceVersion = async ( - workspace: TypesGen.Workspace, - templateVersionId: string, - newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], -): Promise => { - const [currentBuildParameters, templateParameters] = await Promise.all([ - getWorkspaceBuildParameters(workspace.latest_build.id), - getTemplateVersionRichParameters(templateVersionId), - ]); - - const missingParameters = getMissingParameters( - currentBuildParameters, - newBuildParameters, - templateParameters, - ); + return response.data; + }; - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, templateVersionId); - } + getUserSSHKey = async (userId = "me"): Promise => { + const response = await this.axios.get( + `/api/v2/users/${userId}/gitsshkey`, + ); - return postWorkspaceBuild(workspace.id, { - transition: "start", - template_version_id: templateVersionId, - rich_parameter_values: newBuildParameters, - }); -}; + return response.data; + }; -/** Steps to update the workspace - * - Get the latest template to access the latest active version - * - Get the current build parameters - * - Get the template parameters - * - Update the build parameters and check if there are missed parameters for - * the newest version - * - If there are missing parameters raise an error - * - Create a build with the latest version and updated build parameters - */ -export const updateWorkspace = async ( - workspace: TypesGen.Workspace, - newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], -): Promise => { - const [template, oldBuildParameters] = await Promise.all([ - getTemplate(workspace.template_id), - getWorkspaceBuildParameters(workspace.latest_build.id), - ]); - const activeVersionId = template.active_version_id; - const templateParameters = - await getTemplateVersionRichParameters(activeVersionId); - const missingParameters = getMissingParameters( - oldBuildParameters, - newBuildParameters, - templateParameters, - ); + regenerateUserSSHKey = async (userId = "me"): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/gitsshkey`, + ); - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, activeVersionId); - } + return response.data; + }; - return postWorkspaceBuild(workspace.id, { - transition: "start", - template_version_id: activeVersionId, - rich_parameter_values: newBuildParameters, - }); -}; - -export const getWorkspaceResolveAutostart = async ( - workspaceId: string, -): Promise => { - const response = await axiosInstance.get( - `/api/v2/workspaces/${workspaceId}/resolve-autostart`, - ); - return response.data; -}; + getWorkspaceBuilds = async ( + workspaceId: string, + req?: TypesGen.WorkspaceBuildsRequest, + ) => { + const response = await this.axios.get( + getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), + ); -const getMissingParameters = ( - oldBuildParameters: TypesGen.WorkspaceBuildParameter[], - newBuildParameters: TypesGen.WorkspaceBuildParameter[], - templateParameters: TypesGen.TemplateVersionParameter[], -) => { - const missingParameters: TypesGen.TemplateVersionParameter[] = []; - const requiredParameters: TypesGen.TemplateVersionParameter[] = []; + return response.data; + }; - templateParameters.forEach((p) => { - // It is mutable and required. Mutable values can be changed after so we - // don't need to ask them if they are not required. - const isMutableAndRequired = p.mutable && p.required; - // Is immutable, so we can check if it is its first time on the build - const isImmutable = !p.mutable; + getWorkspaceBuildLogs = async ( + buildId: string, + before: Date, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, + ); - if (isMutableAndRequired || isImmutable) { - requiredParameters.push(p); - } - }); + return response.data; + }; - for (const parameter of requiredParameters) { - // Check if there is a new value - let buildParameter = newBuildParameters.find( - (p) => p.name === parameter.name, + getWorkspaceAgentLogs = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/logs`, ); - // If not, get the old one - if (!buildParameter) { - buildParameter = oldBuildParameters.find( - (p) => p.name === parameter.name, + return response.data; + }; + + putWorkspaceExtension = async ( + workspaceId: string, + newDeadline: dayjs.Dayjs, + ): Promise => { + await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { + deadline: newDeadline, + }); + }; + + refreshEntitlements = async (): Promise => { + await this.axios.post("/api/v2/licenses/refresh-entitlements"); + }; + + getEntitlements = async (): Promise => { + try { + const response = await this.axios.get( + "/api/v2/entitlements", ); - } - // If there is a value from the new or old one, it is not missed - if (buildParameter) { - continue; + return response.data; + } catch (ex) { + if (isAxiosError(ex) && ex.response?.status === 404) { + return { + errors: [], + features: withDefaultFeatures({}), + has_license: false, + require_telemetry: false, + trial: false, + warnings: [], + refreshed_at: "", + }; + } + throw ex; } + }; - missingParameters.push(parameter); - } + getExperiments = async (): Promise => { + try { + const response = await this.axios.get( + "/api/v2/experiments", + ); - // Check if parameter "options" changed and we can't use old build parameters. - templateParameters.forEach((templateParameter) => { - if (templateParameter.options.length === 0) { - return; + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + + throw error; } + }; - // Check if there is a new value - let buildParameter = newBuildParameters.find( - (p) => p.name === templateParameter.name, + getAvailableExperiments = + async (): Promise => { + try { + const response = await this.axios.get("/api/v2/experiments/available"); + + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return { safe: [] }; + } + throw error; + } + }; + + getExternalAuthProvider = async ( + provider: string, + ): Promise => { + const res = await this.axios.get(`/api/v2/external-auth/${provider}`); + return res.data; + }; + + getExternalAuthDevice = async ( + provider: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/external-auth/${provider}/device`, ); + return resp.data; + }; - // If not, get the old one - if (!buildParameter) { - buildParameter = oldBuildParameters.find( - (p) => p.name === templateParameter.name, + exchangeExternalAuthDevice = async ( + provider: string, + req: TypesGen.ExternalAuthDeviceExchange, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/external-auth/${provider}/device`, + req, + ); + + return resp.data; + }; + + getUserExternalAuthProviders = + async (): Promise => { + const resp = await this.axios.get(`/api/v2/external-auth`); + return resp.data; + }; + + unlinkExternalAuthProvider = async (provider: string): Promise => { + const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`); + return resp.data; + }; + + getOAuth2ProviderApps = async ( + filter?: TypesGen.OAuth2ProviderAppFilter, + ): Promise => { + const params = filter?.user_id + ? new URLSearchParams({ user_id: filter.user_id }).toString() + : ""; + + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`); + return resp.data; + }; + + getOAuth2ProviderApp = async ( + id: string, + ): Promise => { + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`); + return resp.data; + }; + + postOAuth2ProviderApp = async ( + data: TypesGen.PostOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/oauth2-provider/apps`, + data, + ); + return response.data; + }; + + putOAuth2ProviderApp = async ( + id: string, + data: TypesGen.PutOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/oauth2-provider/apps/${id}`, + data, + ); + return response.data; + }; + + deleteOAuth2ProviderApp = async (id: string): Promise => { + await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`); + }; + + getOAuth2ProviderAppSecrets = async ( + id: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + postOAuth2ProviderAppSecret = async ( + id: string, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + deleteOAuth2ProviderAppSecret = async ( + appId: string, + secretId: string, + ): Promise => { + await this.axios.delete( + `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, + ); + }; + + revokeOAuth2ProviderApp = async (appId: string): Promise => { + await this.axios.delete(`/oauth2/tokens?client_id=${appId}`); + }; + + getAuditLogs = async ( + options: TypesGen.AuditLogsRequest, + ): Promise => { + const url = getURLWithSearchParams("/api/v2/audit", options); + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateDAUs = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/daus`, + ); + + return response.data; + }; + + getDeploymentDAUs = async ( + // Default to user's local timezone. + // As /api/v2/insights/daus only accepts whole-number values for tz_offset + // we truncate the tz offset down to the closest hour. + offset = Math.trunc(new Date().getTimezoneOffset() / 60), + ): Promise => { + const response = await this.axios.get( + `/api/v2/insights/daus?tz_offset=${offset}`, + ); + + return response.data; + }; + + getTemplateACLAvailable = async ( + templateId: string, + options: TypesGen.UsersRequest, + ): Promise => { + const url = getURLWithSearchParams( + `/api/v2/templates/${templateId}/acl/available`, + options, + ).toString(); + + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateACL = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/acl`, + ); + + return response.data; + }; + + updateTemplateACL = async ( + templateId: string, + data: TypesGen.UpdateTemplateACL, + ): Promise<{ message: string }> => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}/acl`, + data, + ); + + return response.data; + }; + + getApplicationsHost = async (): Promise => { + const response = await this.axios.get(`/api/v2/applications/host`); + return response.data; + }; + + getGroups = async (organizationId: string): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/groups`, + ); + + return response.data; + }; + + createGroup = async ( + organizationId: string, + data: TypesGen.CreateGroupRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/groups`, + data, + ); + return response.data; + }; + + getGroup = async (groupId: string): Promise => { + const response = await this.axios.get(`/api/v2/groups/${groupId}`); + return response.data; + }; + + patchGroup = async ( + groupId: string, + data: TypesGen.PatchGroupRequest, + ): Promise => { + const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data); + return response.data; + }; + + addMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: "", + add_users: [userId], + remove_users: [], + }); + }; + + removeMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: "", + display_name: "", + add_users: [], + remove_users: [userId], + }); + }; + + deleteGroup = async (groupId: string): Promise => { + await this.axios.delete(`/api/v2/groups/${groupId}`); + }; + + getWorkspaceQuota = async ( + username: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspace-quota/${encodeURIComponent(username)}`, + ); + return response.data; + }; + + getAgentListeningPorts = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/listening-ports`, + ); + return response.data; + }; + + getWorkspaceAgentSharedPorts = async ( + workspaceID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceID}/port-share`, + ); + return response.data; + }; + + upsertWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.UpsertWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceID}/port-share`, + req, + ); + return response.data; + }; + + deleteWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.DeleteWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.delete( + `/api/v2/workspaces/${workspaceID}/port-share`, + { data: req }, + ); + + return response.data; + }; + + // getDeploymentSSHConfig is used by the VSCode-Extension. + getDeploymentSSHConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/ssh`); + return response.data; + }; + + getDeploymentConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/config`); + return response.data; + }; + + getDeploymentStats = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/stats`); + return response.data; + }; + + getReplicas = async (): Promise => { + const response = await this.axios.get(`/api/v2/replicas`); + return response.data; + }; + + getFile = async (fileId: string): Promise => { + const response = await this.axios.get( + `/api/v2/files/${fileId}`, + { responseType: "arraybuffer" }, + ); + + return response.data; + }; + + getWorkspaceProxyRegions = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = + await this.axios.get>( + `/api/v2/regions`, ); - } - if (!buildParameter) { - return; + return response.data; + }; + + getWorkspaceProxies = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse + >(`/api/v2/workspaceproxies`); + + return response.data; + }; + + createWorkspaceProxy = async ( + b: TypesGen.CreateWorkspaceProxyRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/workspaceproxies`, b); + return response.data; + }; + + getAppearance = async (): Promise => { + try { + const response = await this.axios.get(`/api/v2/appearance`); + return response.data || {}; + } catch (ex) { + if (isAxiosError(ex) && ex.response?.status === 404) { + return { + application_name: "", + logo_url: "", + notification_banners: [], + service_banner: { + enabled: false, + }, + }; + } + + throw ex; } + }; - const matchingOption = templateParameter.options.find( - (option) => option.value === buildParameter?.value, + updateAppearance = async ( + b: TypesGen.AppearanceConfig, + ): Promise => { + const response = await this.axios.put(`/api/v2/appearance`, b); + return response.data; + }; + + getTemplateExamples = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/examples`, ); - if (!matchingOption) { - missingParameters.push(templateParameter); - } - }); - return missingParameters; -}; -/** - * - * @param agentId - * @returns An EventSource that emits agent metadata event objects - * (ServerSentEvent) - */ -export const watchAgentMetadata = (agentId: string): EventSource => { - return new EventSource( - `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`, - { withCredentials: true }, - ); -}; + return response.data; + }; -type WatchBuildLogsByTemplateVersionIdOptions = { - after?: number; - onMessage: (log: TypesGen.ProvisionerJobLog) => void; - onDone?: () => void; - onError: (error: Error) => void; -}; -export const watchBuildLogsByTemplateVersionId = ( - versionId: string, - { - onMessage, - onDone, - onError, - after, - }: WatchBuildLogsByTemplateVersionIdOptions, -) => { - const searchParams = new URLSearchParams({ follow: "true" }); - if (after !== undefined) { - searchParams.append("after", after.toString()); - } - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${ - location.host - }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, - ); - socket.binaryType = "blob"; - socket.addEventListener("message", (event) => - onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), - ); - socket.addEventListener("error", () => { - onError(new Error("Connection for logs failed.")); - socket.close(); - }); - socket.addEventListener("close", () => { - // When the socket closes, logs have finished streaming! - onDone?.(); - }); - return socket; -}; + uploadFile = async (file: File): Promise => { + const response = await this.axios.post("/api/v2/files", file, { + headers: { "Content-Type": "application/x-tar" }, + }); -type WatchWorkspaceAgentLogsOptions = { - after: number; - onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; - onDone?: () => void; - onError: (error: Error) => void; -}; + return response.data; + }; -export const watchWorkspaceAgentLogs = ( - agentId: string, - { after, onMessage, onDone, onError }: WatchWorkspaceAgentLogsOptions, -) => { - // WebSocket compression in Safari (confirmed in 16.5) is broken when - // the server sends large messages. The following error is seen: - // - // WebSocket connection to 'wss://.../logs?follow&after=0' failed: The operation couldn’t be completed. Protocol error - // - const noCompression = - userAgentParser(navigator.userAgent).browser.name === "Safari" - ? "&no_compression" - : ""; + getTemplateVersionLogs = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/logs`, + ); + return response.data; + }; - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`, - ); - socket.binaryType = "blob"; - socket.addEventListener("message", (event) => { - const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; - onMessage(logs); - }); - socket.addEventListener("error", () => { - onError(new Error("socket errored")); - }); - socket.addEventListener("close", () => { - onDone && onDone(); - }); + updateWorkspaceVersion = async ( + workspace: TypesGen.Workspace, + ): Promise => { + const template = await this.getTemplate(workspace.template_id); + return this.startWorkspace(workspace.id, template.active_version_id); + }; - return socket; -}; + getWorkspaceBuildParameters = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, + ); -type WatchBuildLogsByBuildIdOptions = { - after?: number; - onMessage: (log: TypesGen.ProvisionerJobLog) => void; - onDone?: () => void; - onError?: (error: Error) => void; -}; -export const watchBuildLogsByBuildId = ( - buildId: string, - { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions, -) => { - const searchParams = new URLSearchParams({ follow: "true" }); - if (after !== undefined) { - searchParams.append("after", after.toString()); - } - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket( - `${proto}//${ - location.host - }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, - ); - socket.binaryType = "blob"; - socket.addEventListener("message", (event) => - onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), - ); - socket.addEventListener("error", () => { - onError && onError(new Error("Connection for logs failed.")); - socket.close(); - }); - socket.addEventListener("close", () => { - // When the socket closes, logs have finished streaming! - onDone && onDone(); - }); - return socket; -}; + return response.data; + }; -export const issueReconnectingPTYSignedToken = async ( - params: TypesGen.IssueReconnectingPTYSignedTokenRequest, -): Promise => { - const response = await axiosInstance.post( - "/api/v2/applications/reconnecting-pty-signed-token", - params, - ); - return response.data; -}; + getLicenses = async (): Promise => { + const response = await this.axios.get(`/api/v2/licenses`); + return response.data; + }; -export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { - const latestBuild = workspace.latest_build; - const [templateVersionRichParameters, buildParameters] = await Promise.all([ - getTemplateVersionRichParameters(latestBuild.template_version_id), - getWorkspaceBuildParameters(latestBuild.id), - ]); - return { - templateVersionRichParameters, - buildParameters, + createLicense = async ( + data: TypesGen.AddLicenseRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/licenses`, data); + return response.data; }; -}; -export type InsightsParams = { - start_time: string; - end_time: string; - template_ids: string; -}; + removeLicense = async (licenseId: number): Promise => { + await this.axios.delete(`/api/v2/licenses/${licenseId}`); + }; -export const getInsightsUserLatency = async ( - filters: InsightsParams, -): Promise => { - const params = new URLSearchParams(filters); - const response = await axiosInstance.get( - `/api/v2/insights/user-latency?${params}`, - ); - return response.data; -}; + /** Steps to change the workspace version + * - Get the latest template to access the latest active version + * - Get the current build parameters + * - Get the template parameters + * - Update the build parameters and check if there are missed parameters for + * the new version + * - If there are missing parameters raise an error + * - Create a build with the version and updated build parameters + */ + changeWorkspaceVersion = async ( + workspace: TypesGen.Workspace, + templateVersionId: string, + newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + ): Promise => { + const [currentBuildParameters, templateParameters] = await Promise.all([ + this.getWorkspaceBuildParameters(workspace.latest_build.id), + this.getTemplateVersionRichParameters(templateVersionId), + ]); + + const missingParameters = getMissingParameters( + currentBuildParameters, + newBuildParameters, + templateParameters, + ); -export const getInsightsUserActivity = async ( - filters: InsightsParams, -): Promise => { - const params = new URLSearchParams(filters); - const response = await axiosInstance.get( - `/api/v2/insights/user-activity?${params}`, - ); - return response.data; -}; + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, templateVersionId); + } -export type InsightsTemplateParams = InsightsParams & { - interval: "day" | "week"; -}; + return this.postWorkspaceBuild(workspace.id, { + transition: "start", + template_version_id: templateVersionId, + rich_parameter_values: newBuildParameters, + }); + }; -export const getInsightsTemplate = async ( - params: InsightsTemplateParams, -): Promise => { - const searchParams = new URLSearchParams(params); - const response = await axiosInstance.get( - `/api/v2/insights/templates?${searchParams}`, - ); - return response.data; -}; + /** Steps to update the workspace + * - Get the latest template to access the latest active version + * - Get the current build parameters + * - Get the template parameters + * - Update the build parameters and check if there are missed parameters for + * the newest version + * - If there are missing parameters raise an error + * - Create a build with the latest version and updated build parameters + */ + updateWorkspace = async ( + workspace: TypesGen.Workspace, + newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + ): Promise => { + const [template, oldBuildParameters] = await Promise.all([ + this.getTemplate(workspace.template_id), + this.getWorkspaceBuildParameters(workspace.latest_build.id), + ]); + + const activeVersionId = template.active_version_id; + const templateParameters = + await this.getTemplateVersionRichParameters(activeVersionId); + + const missingParameters = getMissingParameters( + oldBuildParameters, + newBuildParameters, + templateParameters, + ); -export const getHealth = async (force: boolean = false) => { - const params = new URLSearchParams({ force: force.toString() }); - const response = await axiosInstance.get( - `/api/v2/debug/health?${params}`, - ); - return response.data; -}; + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, activeVersionId); + } -export const getHealthSettings = async () => { - return ( - await axiosInstance.get( - `/api/v2/debug/health/settings`, - ) - ).data; -}; + return this.postWorkspaceBuild(workspace.id, { + transition: "start", + template_version_id: activeVersionId, + rich_parameter_values: newBuildParameters, + }); + }; -export const updateHealthSettings = async ( - data: TypesGen.UpdateHealthSettings, -) => { - const response = await axiosInstance.put( - `/api/v2/debug/health/settings`, - data, - ); - return response.data; -}; + getWorkspaceResolveAutostart = async ( + workspaceId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}/resolve-autostart`, + ); + return response.data; + }; -export const putFavoriteWorkspace = async (workspaceID: string) => { - await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/favorite`); -}; + issueReconnectingPTYSignedToken = async ( + params: TypesGen.IssueReconnectingPTYSignedTokenRequest, + ): Promise => { + const response = await this.axios.post( + "/api/v2/applications/reconnecting-pty-signed-token", + params, + ); -export const deleteFavoriteWorkspace = async (workspaceID: string) => { - await axiosInstance.delete(`/api/v2/workspaces/${workspaceID}/favorite`); -}; + return response.data; + }; -export type GetJFrogXRayScanParams = { - workspaceId: string; - agentId: string; -}; + getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { + const latestBuild = workspace.latest_build; + const [templateVersionRichParameters, buildParameters] = await Promise.all([ + this.getTemplateVersionRichParameters(latestBuild.template_version_id), + this.getWorkspaceBuildParameters(latestBuild.id), + ]); -export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { - const searchParams = new URLSearchParams({ - workspace_id: options.workspaceId, - agent_id: options.agentId, - }); + return { + templateVersionRichParameters, + buildParameters, + }; + }; + + getInsightsUserLatency = async ( + filters: InsightsParams, + ): Promise => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-latency?${params}`, + ); + + return response.data; + }; + + getInsightsUserActivity = async ( + filters: InsightsParams, + ): Promise => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-activity?${params}`, + ); + + return response.data; + }; + + getInsightsTemplate = async ( + params: InsightsTemplateParams, + ): Promise => { + const searchParams = new URLSearchParams(params); + const response = await this.axios.get( + `/api/v2/insights/templates?${searchParams}`, + ); + + return response.data; + }; - try { - const res = await axiosInstance.get( - `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, + getHealth = async (force: boolean = false) => { + const params = new URLSearchParams({ force: force.toString() }); + const response = await this.axios.get( + `/api/v2/debug/health?${params}`, ); + return response.data; + }; + + getHealthSettings = async (): Promise => { + const res = await this.axios.get( + `/api/v2/debug/health/settings`, + ); + return res.data; - } catch (error) { - if (isAxiosError(error) && error.response?.status === 404) { - // react-query library does not allow undefined to be returned as a query result - return null; + }; + + updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => { + const response = await this.axios.put( + `/api/v2/debug/health/settings`, + data, + ); + + return response.data; + }; + + putFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + deleteFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { + const searchParams = new URLSearchParams({ + workspace_id: options.workspaceId, + agent_id: options.agentId, + }); + + try { + const res = await this.axios.get( + `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, + ); + + return res.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + // react-query library does not allow undefined to be returned as a + // query result + return null; + } + + throw error; + } + }; +} + +// This is a hard coded CSRF token/cookie pair for local development. In prod, +// the GoLang webserver generates a random cookie with a new token for each +// document request. For local development, we don't use the Go webserver for +// static files, so this is the 'hack' to make local development work with +// remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" +const csrfToken = + "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=="; + +// Always attach CSRF token to all requests. In puppeteer the document is +// undefined. In those cases, just do nothing. +const tokenMetadataElement = + typeof document !== "undefined" + ? document.head.querySelector('meta[property="csrf-token"]') + : null; + +function getConfiguredAxiosInstance(): AxiosInstance { + const instance = globalAxios.create(); + + // Adds 304 for the default axios validateStatus function + // https://github.com/axios/axios#handling-errors Check status here + // https://httpstatusdogs.com/ + instance.defaults.validateStatus = (status) => { + return (status >= 200 && status < 300) || status === 304; + }; + + const metadataIsAvailable = + tokenMetadataElement !== null && + tokenMetadataElement.getAttribute("content") !== null; + + if (metadataIsAvailable) { + if (process.env.NODE_ENV === "development") { + // Development mode uses a hard-coded CSRF token + instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; + instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; + tokenMetadataElement.setAttribute("content", csrfToken); + } else { + instance.defaults.headers.common["X-CSRF-TOKEN"] = + tokenMetadataElement.getAttribute("content") ?? ""; + } + } else { + // Do not write error logs if we are in a FE unit test. + if (process.env.JEST_WORKER_ID === undefined) { + console.error("CSRF token not found"); } } -}; + + return instance; +} + +// Other non-API methods defined here to make it a little easier to find them. +interface ClientApi extends ApiMethods { + getCsrfToken: () => string; + setSessionToken: (token: string) => void; + setHost: (host: string | undefined) => void; + getAxiosInstance: () => AxiosInstance; +} + +export class Api extends ApiMethods implements ClientApi { + constructor() { + const scopedAxiosInstance = getConfiguredAxiosInstance(); + super(scopedAxiosInstance); + } + + // As with ApiMethods, all public methods should be defined with arrow + // function syntax to ensure they can be passed around the React UI without + // losing/detaching their `this` context! + + getCsrfToken = (): string => { + return csrfToken; + }; + + setSessionToken = (token: string): void => { + this.axios.defaults.headers.common["Coder-Session-Token"] = token; + }; + + setHost = (host: string | undefined): void => { + this.axios.defaults.baseURL = host; + }; + + getAxiosInstance = (): AxiosInstance => { + return this.axios; + }; +} + +export const API = new Api(); diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index 7fc6cd1a71b9d..8deab4a4e85e6 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -1,5 +1,5 @@ import type { QueryClient } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { AppearanceConfig } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts index 6430767480714..1dce9a29eaab8 100644 --- a/site/src/api/queries/audits.ts +++ b/site/src/api/queries/audits.ts @@ -1,4 +1,4 @@ -import { getAuditLogs } from "api/api"; +import { API } from "api/api"; import type { AuditLogResponse } from "api/typesGenerated"; import { useFilterParamsKey } from "components/Filter/filter"; import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery"; @@ -13,7 +13,7 @@ export function paginatedAudits( return ["auditLogs", payload, pageNumber] as const; }, queryFn: ({ payload, limit, offset }) => { - return getAuditLogs({ + return API.getAuditLogs({ offset, limit, q: payload, diff --git a/site/src/api/queries/authCheck.ts b/site/src/api/queries/authCheck.ts index be9e726ae074d..3248f35357f25 100644 --- a/site/src/api/queries/authCheck.ts +++ b/site/src/api/queries/authCheck.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; import type { AuthorizationRequest } from "api/typesGenerated"; export const AUTHORIZATION_KEY = "authorization"; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index 0f0eecafa9f49..43dac7d20334f 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; import type { BuildInfoResponse } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts index 1fba00c172c51..b84fdf1b7c2fb 100644 --- a/site/src/api/queries/debug.ts +++ b/site/src/api/queries/debug.ts @@ -1,5 +1,5 @@ import type { QueryClient, UseMutationOptions } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { HealthSettings, UpdateHealthSettings } from "api/typesGenerated"; export const HEALTH_QUERY_KEY = ["health"]; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 540c76ebd79e2..fa4d37967af18 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const deploymentConfig = () => { return { diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts index 48f43630ea29a..542aa6f0cf591 100644 --- a/site/src/api/queries/entitlements.ts +++ b/site/src/api/queries/entitlements.ts @@ -1,5 +1,5 @@ import type { QueryClient } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { Entitlements } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index e0a2749d75829..86fd9096ae9f2 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; import type { Experiments } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; diff --git a/site/src/api/queries/externalAuth.ts b/site/src/api/queries/externalAuth.ts index 18cc95a8839ff..eda68713aa5fc 100644 --- a/site/src/api/queries/externalAuth.ts +++ b/site/src/api/queries/externalAuth.ts @@ -1,5 +1,5 @@ import type { QueryClient, UseMutationOptions } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { ExternalAuth } from "api/typesGenerated"; // Returns all configured external auths for a given user. diff --git a/site/src/api/queries/files.ts b/site/src/api/queries/files.ts index cc840b52eb63f..a363e03f94473 100644 --- a/site/src/api/queries/files.ts +++ b/site/src/api/queries/files.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const uploadFile = () => { return { diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 71cba33354b9b..5c34758df069f 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -1,6 +1,5 @@ import type { QueryClient, UseQueryOptions } from "react-query"; -import * as API from "api/api"; -import { checkAuthorization } from "api/api"; +import { API } from "api/api"; import type { CreateGroupRequest, Group, @@ -72,7 +71,7 @@ export const groupPermissions = (groupId: string) => { return { queryKey: [...getGroupQueryKey(groupId), "permissions"], queryFn: () => - checkAuthorization({ + API.checkAuthorization({ checks: { canUpdateGroup: { object: { diff --git a/site/src/api/queries/insights.ts b/site/src/api/queries/insights.ts index 7d60565e83bb0..4b6dad8cd2fc8 100644 --- a/site/src/api/queries/insights.ts +++ b/site/src/api/queries/insights.ts @@ -1,20 +1,20 @@ -import * as API from "api/api"; +import { type InsightsParams, type InsightsTemplateParams, API } from "api/api"; -export const insightsTemplate = (params: API.InsightsTemplateParams) => { +export const insightsTemplate = (params: InsightsTemplateParams) => { return { queryKey: ["insights", "templates", params.template_ids, params], queryFn: () => API.getInsightsTemplate(params), }; }; -export const insightsUserLatency = (params: API.InsightsParams) => { +export const insightsUserLatency = (params: InsightsParams) => { return { queryKey: ["insights", "userLatency", params.template_ids, params], queryFn: () => API.getInsightsUserLatency(params), }; }; -export const insightsUserActivity = (params: API.InsightsParams) => { +export const insightsUserActivity = (params: InsightsParams) => { return { queryKey: ["insights", "userActivity", params.template_ids, params], queryFn: () => API.getInsightsUserActivity(params), diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts index de43a4c8f4cac..c0e7f6f28ce9d 100644 --- a/site/src/api/queries/integrations.ts +++ b/site/src/api/queries/integrations.ts @@ -1,5 +1,5 @@ import type { GetJFrogXRayScanParams } from "api/api"; -import * as API from "api/api"; +import { API } from "api/api"; export const xrayScan = (params: GetJFrogXRayScanParams) => { return { diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts index 78b31762b2aa5..26334955c4a86 100644 --- a/site/src/api/queries/oauth2.ts +++ b/site/src/api/queries/oauth2.ts @@ -1,5 +1,5 @@ import type { QueryClient } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type * as TypesGen from "api/typesGenerated"; const appsKey = ["oauth2-provider", "apps"]; diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 37b2af49f3e74..2a6c1700b53a7 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const roles = () => { return { diff --git a/site/src/api/queries/settings.ts b/site/src/api/queries/settings.ts index 4a086cf18532c..eb3468b68d978 100644 --- a/site/src/api/queries/settings.ts +++ b/site/src/api/queries/settings.ts @@ -1,5 +1,5 @@ import type { QueryClient, QueryOptions } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { UpdateUserQuietHoursScheduleRequest, UserQuietHoursScheduleResponse, diff --git a/site/src/api/queries/sshKeys.ts b/site/src/api/queries/sshKeys.ts index 6fc3593c318c7..43686ff1437b2 100644 --- a/site/src/api/queries/sshKeys.ts +++ b/site/src/api/queries/sshKeys.ts @@ -1,5 +1,5 @@ import type { QueryClient } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { GitSSHKey } from "api/typesGenerated"; const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"]; diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 83879415bacf6..2d0485b8f347b 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -1,5 +1,5 @@ import type { MutationOptions, QueryClient, QueryOptions } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { CreateTemplateRequest, CreateTemplateVersionRequest, diff --git a/site/src/api/queries/updateCheck.ts b/site/src/api/queries/updateCheck.ts index 40fcc6a3cfdde..e8dc1b2cc3e41 100644 --- a/site/src/api/queries/updateCheck.ts +++ b/site/src/api/queries/updateCheck.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const updateCheck = () => { return { diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index ded7c7a5f29c8..7dcd157f7bc6c 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -3,7 +3,7 @@ import type { UseMutationOptions, UseQueryOptions, } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { AuthorizationRequest, GetUsersResponse, diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 8960068b6169c..a7c0aaf4fdabe 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -1,5 +1,5 @@ import type { QueryOptions, UseInfiniteQueryOptions } from "react-query"; -import * as API from "api/api"; +import { API } from "api/api"; import type { WorkspaceBuild, WorkspaceBuildParameter, diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index f43adf616688e..1735b0f71279b 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const getWorkspaceQuotaQueryKey = (username: string) => [ username, diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts index 9e341d551a4f3..60bd99285aa54 100644 --- a/site/src/api/queries/workspaceportsharing.ts +++ b/site/src/api/queries/workspaceportsharing.ts @@ -1,8 +1,4 @@ -import { - deleteWorkspaceAgentSharedPort, - getWorkspaceAgentSharedPorts, - upsertWorkspaceAgentSharedPort, -} from "api/api"; +import { API } from "api/api"; import type { DeleteWorkspaceAgentPortShareRequest, UpsertWorkspaceAgentPortShareRequest, @@ -11,14 +7,14 @@ import type { export const workspacePortShares = (workspaceId: string) => { return { queryKey: ["sharedPorts", workspaceId], - queryFn: () => getWorkspaceAgentSharedPorts(workspaceId), + queryFn: () => API.getWorkspaceAgentSharedPorts(workspaceId), }; }; export const upsertWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { - await upsertWorkspaceAgentSharedPort(workspaceId, options); + await API.upsertWorkspaceAgentSharedPort(workspaceId, options); }, }; }; @@ -26,7 +22,7 @@ export const upsertWorkspacePortShare = (workspaceId: string) => { export const deleteWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { - await deleteWorkspaceAgentSharedPort(workspaceId, options); + await API.deleteWorkspaceAgentSharedPort(workspaceId, options); }, }; }; diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 816cc5613e99d..95df3b7f592f6 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -4,8 +4,7 @@ import type { QueryOptions, UseMutationOptions, } from "react-query"; -import * as API from "api/api"; -import { putWorkspaceExtension } from "api/api"; +import { type DeleteWorkspaceOptions, API } from "api/api"; import type { CreateWorkspaceRequest, ProvisionerLogLevel, @@ -28,7 +27,9 @@ export const workspaceByOwnerAndName = (owner: string, name: string) => { return { queryKey: workspaceByOwnerAndNameKey(owner, name), queryFn: () => - API.getWorkspaceByOwnerAndName(owner, name, { include_deleted: true }), + API.getWorkspaceByOwnerAndName(owner, name, { + include_deleted: true, + }), }; }; @@ -111,7 +112,7 @@ export const updateDeadline = ( ): UseMutationOptions => { return { mutationFn: (deadline: Dayjs) => { - return putWorkspaceExtension(workspace.id, deadline); + return API.putWorkspaceExtension(workspace.id, deadline); }, }; }; @@ -155,7 +156,7 @@ export const deleteWorkspace = ( queryClient: QueryClient, ) => { return { - mutationFn: (options: API.DeleteWorkspaceOptions) => { + mutationFn: (options: DeleteWorkspaceOptions) => { return API.deleteWorkspace(workspace.id, options); }, onSuccess: async (build: WorkspaceBuild) => { diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 74bbf91376a12..a42dbf07d791c 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -1,5 +1,5 @@ import type { FC } from "react"; -import { getUsers } from "api/api"; +import { API } from "api/api"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { UserAvatar } from "../UserAvatar/UserAvatar"; import { FilterSearchMenu, OptionItem } from "./filter"; @@ -42,7 +42,7 @@ export const useUserFilterMenu = ({ }; } - const usersRes = await getUsers({ q: value, limit: 1 }); + const usersRes = await API.getUsers({ q: value, limit: 1 }); const firstUser = usersRes.users.at(0); if (firstUser && firstUser.username === value) { return { @@ -54,7 +54,7 @@ export const useUserFilterMenu = ({ return null; }, getOptions: async (query) => { - const usersRes = await getUsers({ q: query, limit: 25 }); + const usersRes = await API.getUsers({ q: query, limit: 25 }); let options: UserOption[] = usersRes.users.map((user) => ({ label: user.username, value: user.username, diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index 0f5af37634092..767cdd54d1a67 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -8,7 +8,7 @@ import { useState, } from "react"; import { useQuery } from "react-query"; -import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api"; +import { API } from "api/api"; import { cachedQuery } from "api/queries/util"; import type { Region, WorkspaceProxy } from "api/typesGenerated"; import { useAuthenticated } from "contexts/auth/RequireAuth"; @@ -108,10 +108,11 @@ export const ProxyProvider: FC = ({ children }) => { metadata: metadata.regions, queryKey: ["get-proxies"], queryFn: async (): Promise => { - const endpoint = permissions.editWorkspaceProxies - ? getWorkspaceProxies - : getWorkspaceProxyRegions; - const resp = await endpoint(); + const apiCall = permissions.editWorkspaceProxies + ? API.getWorkspaceProxies + : API.getWorkspaceProxyRegions; + + const resp = await apiCall(); return resp.regions; }, }), diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx index 7f23da60c3dbb..2d6b14d3db69f 100644 --- a/site/src/contexts/auth/RequireAuth.tsx +++ b/site/src/contexts/auth/RequireAuth.tsx @@ -1,6 +1,6 @@ import { type FC, useEffect } from "react"; import { Outlet, Navigate, useLocation } from "react-router-dom"; -import { axiosInstance } from "api/api"; +import { API } from "api/api"; import { isApiError } from "api/errors"; import { Loader } from "components/Loader/Loader"; import { ProxyProvider } from "contexts/ProxyContext"; @@ -18,6 +18,7 @@ export const RequireAuth: FC = () => { return; } + const axiosInstance = API.getAxiosInstance(); const interceptorHandle = axiosInstance.interceptors.response.use( (okResponse) => okResponse, (error: unknown) => { diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index 497cd457ede51..df2afc277b44a 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -1,6 +1,6 @@ import PerformanceObserver from "@fastly/performance-observer-polyfill"; import { useEffect, useReducer, useState } from "react"; -import { axiosInstance } from "api/api"; +import { API } from "api/api"; import type { Region } from "api/typesGenerated"; import { generateRandomString } from "utils/random"; @@ -197,6 +197,7 @@ export const useProxyLatency = ( // The resource requests include xmlhttp requests. observer.observe({ entryTypes: ["resource"] }); + const axiosInstance = API.getAxiosInstance(); const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => { return axiosInstance.get(latencyURL, { withCredentials: false, diff --git a/site/src/modules/resources/AgentLogs/AgentLogs.tsx b/site/src/modules/resources/AgentLogs/AgentLogs.tsx index 2216b7eae24ae..407e3c12fe9b5 100644 --- a/site/src/modules/resources/AgentLogs/AgentLogs.tsx +++ b/site/src/modules/resources/AgentLogs/AgentLogs.tsx @@ -8,7 +8,7 @@ import { useState, } from "react"; import { FixedSizeList as List } from "react-window"; -import * as API from "api/api"; +import { watchWorkspaceAgentLogs } from "api/api"; import type { WorkspaceAgentLogSource } from "api/typesGenerated"; import { AGENT_LOG_LINE_HEIGHT, @@ -193,7 +193,7 @@ export const useAgentLogs = ( return; } - const socket = API.watchWorkspaceAgentLogs(agentId, { + const socket = watchWorkspaceAgentLogs(agentId, { // Get all logs after: 0, onMessage: (logs) => { diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 7243d2b1af5b6..7042c879385d0 100644 --- a/site/src/modules/resources/AppLink/AppLink.tsx +++ b/site/src/modules/resources/AppLink/AppLink.tsx @@ -4,7 +4,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import { type FC, useState } from "react"; -import { getApiKey } from "api/api"; +import { API } from "api/api"; import type * as TypesGen from "api/typesGenerated"; import { useProxy } from "contexts/ProxyContext"; import { createAppLinkHref } from "utils/apps"; @@ -145,7 +145,7 @@ export const AppLink: FC = ({ app, workspace, agent }) => { let url = href; if (hasMagicToken !== -1) { setFetchingSessionToken(true); - const key = await getApiKey(); + const key = await API.getApiKey(); url = href.replaceAll(magicTokenString, key.key); setFetchingSessionToken(false); } diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 6d46b1064ad46..d22e986a1c074 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -19,7 +19,7 @@ import { type FormikContextType, useFormik } from "formik"; import { useState, type FC } from "react"; import { useQuery, useMutation } from "react-query"; import * as Yup from "yup"; -import { getAgentListeningPorts } from "api/api"; +import { API } from "api/api"; import { deleteWorkspacePortShare, upsertWorkspacePortShare, @@ -70,7 +70,7 @@ export const PortForwardButton: FC = (props) => { const portsQuery = useQuery({ queryKey: ["portForward", agent.id], - queryFn: () => getAgentListeningPorts(agent.id), + queryFn: () => API.getAgentListeningPorts(agent.id), enabled: agent.status === "connected", refetchInterval: 5_000, }); diff --git a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx index 73597dd22b6d3..73763439076bd 100644 --- a/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx +++ b/site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx @@ -3,7 +3,7 @@ import ButtonGroup from "@mui/material/ButtonGroup"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import { type FC, useState, useRef } from "react"; -import { getApiKey } from "api/api"; +import { API } from "api/api"; import type { DisplayApp } from "api/typesGenerated"; import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; @@ -119,7 +119,7 @@ const VSCodeButton: FC = ({ disabled={loading} onClick={() => { setLoading(true); - getApiKey() + API.getApiKey() .then(({ key }) => { const query = new URLSearchParams({ owner: userName, @@ -163,7 +163,7 @@ const VSCodeInsidersButton: FC = ({ disabled={loading} onClick={() => { setLoading(true); - getApiKey() + API.getApiKey() .then(({ key }) => { const query = new URLSearchParams({ owner: userName, diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx index 24b00fa082430..be3317ee68099 100644 --- a/site/src/pages/AuditPage/AuditPage.test.tsx +++ b/site/src/pages/AuditPage/AuditPage.test.tsx @@ -1,7 +1,7 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { HttpResponse, http } from "msw"; -import * as API from "api/api"; +import { API } from "api/api"; import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; import { MockAuditLog, diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx index 38eacc38fd30e..38cf8994011c3 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockTemplateExample, MockTemplateVersion, diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx index f58eed5d1e6b2..630834cd5fa72 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx @@ -1,6 +1,6 @@ import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { renderWithAuth, waitForLoaderToBeRemoved, diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx index 4ea1f98144671..1fcf9daaa43fb 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx @@ -3,7 +3,7 @@ import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; -import { createToken, getTokenConfig } from "api/api"; +import { API } from "api/api"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { CodeExample } from "components/CodeExample/CodeExample"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; @@ -28,7 +28,7 @@ export const CreateTokenPage: FC = () => { isError: creationFailed, isSuccess: creationSuccessful, data: newToken, - } = useMutation(createToken); + } = useMutation(API.createToken); const { data: tokenConfig, isLoading: fetchingTokenConfig, @@ -36,7 +36,7 @@ export const CreateTokenPage: FC = () => { error: tokenFetchError, } = useQuery({ queryKey: ["tokenconfig"], - queryFn: getTokenConfig, + queryFn: API.getTokenConfig, }); const [formError, setFormError] = useState(undefined); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 85ffe8ea45896..02bde4b7134cf 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockTemplate, MockUser, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index b885b11d32f6c..df0bb38891f03 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -2,7 +2,7 @@ import { type FC, useCallback, useEffect, useState, useRef } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { getUserParameters } from "api/api"; +import { API } from "api/api"; import type { ApiErrorResponse } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; import { @@ -99,7 +99,7 @@ const CreateWorkspacePage: FC = () => { const autofillEnabled = experiments.includes("auto-fill-parameters"); const userParametersQuery = useQuery({ queryKey: ["userParameters"], - queryFn: () => getUserParameters(templateQuery.data!.id), + queryFn: () => API.getUserParameters(templateQuery.data!.id), enabled: autofillEnabled && templateQuery.isSuccess, }); const autofillParameters = getAutofillParameters( diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx index 98758f22f7b5b..b40d7a201dd55 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation } from "react-query"; import { useNavigate } from "react-router-dom"; -import { createLicense } from "api/api"; +import { API } from "api/api"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { pageTitle } from "utils/page"; import { AddNewLicensePageView } from "./AddNewLicensePageView"; @@ -14,7 +14,7 @@ const AddNewLicensePage: FC = () => { mutate: saveLicenseKeyApi, isLoading: isCreating, error: savingLicenseError, - } = useMutation(createLicense, { + } = useMutation(API.createLicense, { onSuccess: () => { displaySuccess("You have successfully added a license"); navigate("/deployment/licenses?success=true"); diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx index dcd219c99e8c9..c3e353b63074e 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx @@ -3,7 +3,7 @@ import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useSearchParams } from "react-router-dom"; import useToggle from "react-use/lib/useToggle"; -import { getLicenses, removeLicense } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import { entitlements, refreshEntitlements } from "api/queries/entitlements"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; @@ -36,7 +36,7 @@ const LicensesSettingsPage: FC = () => { }, [entitlementsQuery.error]); const { mutate: removeLicenseApi, isLoading: isRemovingLicense } = - useMutation(removeLicense, { + useMutation(API.removeLicense, { onSuccess: () => { displaySuccess("Successfully removed license"); void queryClient.invalidateQueries(["licenses"]); @@ -48,7 +48,7 @@ const LicensesSettingsPage: FC = () => { const { data: licenses, isLoading } = useQuery({ queryKey: ["licenses"], - queryFn: () => getLicenses(), + queryFn: () => API.getLicenses(), }); useEffect(() => { diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx index 580df7f645c7c..d2867a80085b6 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx @@ -1,6 +1,6 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { TemplateLayout } from "pages/TemplatePage/TemplateLayout"; import { MockTemplate, diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index 57716e0b91fe5..643f9c166fb7b 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx @@ -7,7 +7,7 @@ import RadioGroup from "@mui/material/RadioGroup"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { getTemplateVersionRichParameters } from "api/api"; +import { API } from "api/api"; import type { Template, TemplateVersionParameter } from "api/typesGenerated"; import { FormSection, VerticalForm } from "components/Form/Form"; import { Loader } from "components/Loader/Loader"; @@ -24,7 +24,8 @@ const TemplateEmbedPage: FC = () => { const { template } = useTemplateLayoutContext(); const { data: templateParameters } = useQuery({ queryKey: ["template", template.id, "embed"], - queryFn: () => getTemplateVersionRichParameters(template.active_version_id), + queryFn: () => + API.getTemplateVersionRichParameters(template.active_version_id), }); return ( diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index 43bf807c45df2..e388c81feb27e 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -7,11 +7,7 @@ import { } from "react"; import { useQuery } from "react-query"; import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; -import { - checkAuthorization, - getTemplateByName, - getTemplateVersion, -} from "api/api"; +import { API } from "api/api"; import type { AuthorizationRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; @@ -39,10 +35,11 @@ const templatePermissions = ( }); const fetchTemplate = async (organizationId: string, templateName: string) => { - const template = await getTemplateByName(organizationId, templateName); + const template = await API.getTemplateByName(organizationId, templateName); + const [activeVersion, permissions] = await Promise.all([ - getTemplateVersion(template.active_version_id), - checkAuthorization({ + API.getTemplateVersion(template.active_version_id), + API.checkAuthorization({ checks: templatePermissions(template.id), }), ]); diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index c0460c5b59d74..226f6d7fa07fb 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -1,7 +1,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; -import { getTemplateVersionResources } from "api/api"; +import { API } from "api/api"; import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout"; import { getTemplatePageTitle } from "../utils"; import { TemplateSummaryPageView } from "./TemplateSummaryPageView"; @@ -10,7 +10,7 @@ export const TemplateSummaryPage: FC = () => { const { template, activeVersion } = useTemplateLayoutContext(); const { data: resources } = useQuery({ queryKey: ["templates", template.id, "resources"], - queryFn: () => getTemplateVersionResources(activeVersion.id), + queryFn: () => API.getTemplateVersionResources(activeVersion.id), }); return ( diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx index 5d50f110d00de..df05f167e776e 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx @@ -1,11 +1,7 @@ import { useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; -import { - archiveTemplateVersion, - getTemplateVersions, - updateActiveTemplateVersion, -} from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; @@ -17,7 +13,7 @@ const TemplateVersionsPage = () => { const { template, permissions } = useTemplateLayoutContext(); const { data } = useQuery({ queryKey: ["template", "versions", template.id], - queryFn: () => getTemplateVersions(template.id), + queryFn: () => API.getTemplateVersions(template.id), }); // We use this to update the active version in the UI without having to refetch the template const [latestActiveVersion, setLatestActiveVersion] = useState( @@ -25,7 +21,7 @@ const TemplateVersionsPage = () => { ); const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({ mutationFn: (templateVersionId: string) => { - return updateActiveTemplateVersion(template.id, { + return API.updateActiveTemplateVersion(template.id, { id: templateVersionId, }); }, @@ -41,7 +37,7 @@ const TemplateVersionsPage = () => { const { mutate: archiveVersion, isLoading: isArchiving } = useMutation({ mutationFn: (templateVersionId: string) => { - return archiveTemplateVersion(templateVersionId); + return API.archiveTemplateVersion(templateVersionId); }, onSuccess: async () => { // The reload is unfortunate. When a version is archived, we should hide diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts index 63a53f3d1b682..d0dab66bbd975 100644 --- a/site/src/pages/TemplatePage/useDeletionDialogState.test.ts +++ b/site/src/pages/TemplatePage/useDeletionDialogState.test.ts @@ -1,5 +1,5 @@ import { act, renderHook, waitFor } from "@testing-library/react"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockTemplate } from "testHelpers/entities"; import { useDeletionDialogState } from "./useDeletionDialogState"; diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.ts b/site/src/pages/TemplatePage/useDeletionDialogState.ts index 7b3b7bbfcac63..cc7e55670e2be 100644 --- a/site/src/pages/TemplatePage/useDeletionDialogState.ts +++ b/site/src/pages/TemplatePage/useDeletionDialogState.ts @@ -1,5 +1,5 @@ import { useState } from "react"; -import { deleteTemplate } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -27,7 +27,7 @@ export const useDeletionDialogState = ( const confirmDelete = async () => { try { setState({ status: "deleting" }); - await deleteTemplate(templateId); + await API.deleteTemplate(templateId); onDelete(); } catch (e) { setState({ status: "confirming" }); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 2b9402bda94bd..716322f982288 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -1,7 +1,7 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; -import * as API from "api/api"; +import { API, withDefaultFeatures } from "api/api"; import type { Template, UpdateTemplateMeta } from "api/typesGenerated"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockEntitlements, MockTemplate } from "testHelpers/entities"; @@ -138,7 +138,7 @@ describe("TemplateSettingsPage", () => { http.get("/api/v2/entitlements", () => { return HttpResponse.json({ ...MockEntitlements, - features: API.withDefaultFeatures({ + features: withDefaultFeatures({ access_control: { enabled: true, entitlement: "entitled" }, }), }); @@ -163,7 +163,7 @@ describe("TemplateSettingsPage", () => { http.get("/api/v2/entitlements", () => { return HttpResponse.json({ ...MockEntitlements, - features: API.withDefaultFeatures({ + features: withDefaultFeatures({ access_control: { enabled: false, entitlement: "not_entitled" }, }), }); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 05e61630db9f1..4438cec0bea06 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; -import { updateTemplateMeta } from "api/api"; +import { API } from "api/api"; import { templateByNameKey } from "api/queries/templates"; import type { UpdateTemplateMeta } from "api/typesGenerated"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -30,7 +30,9 @@ export const TemplateSettingsPage: FC = () => { isLoading: isSubmitting, error: submitError, } = useMutation( - (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + (data: UpdateTemplateMeta) => { + return API.updateTemplateMeta(template.id, data); + }, { onSuccess: async (data) => { // This update has a chance to return a 304 which means nothing was updated. diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index cb0505e99b800..48d9d8ef44e4f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockEntitlementsWithScheduling, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index de45cbd38652e..db37ed32dbcc3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; -import { updateTemplateMeta } from "api/api"; +import { API } from "api/api"; import { templateByNameKey } from "api/queries/templates"; import type { UpdateTemplateMeta } from "api/typesGenerated"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -27,7 +27,7 @@ const TemplateSchedulePage: FC = () => { isLoading: isSubmitting, error: submitError, } = useMutation( - (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + (data: UpdateTemplateMeta) => API.updateTemplateMeta(template.id, data), { onSuccess: async () => { await queryClient.invalidateQueries( diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx index c123a317691e5..a99d599dd3947 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -1,6 +1,6 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockTemplate, diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 6f54255fbe23a..8c63b7db428d1 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -4,7 +4,7 @@ import WS from "jest-websocket-mock"; import { HttpResponse, http } from "msw"; import { QueryClient } from "react-query"; import { RouterProvider, createMemoryRouter } from "react-router-dom"; -import * as api from "api/api"; +import * as apiModule from "api/api"; import { templateVersionVariablesKey } from "api/queries/templates"; import type { TemplateVersion } from "api/typesGenerated"; import { AppProviders } from "App"; @@ -26,6 +26,8 @@ import type { MonacoEditorProps } from "./MonacoEditor"; import { Language } from "./PublishTemplateVersionDialog"; import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; +const { API } = apiModule; + // For some reason this component in Jest is throwing a MUI style warning so, // since we don't need it for this test, we can mock it out jest.mock( @@ -72,8 +74,8 @@ const buildTemplateVersion = async ( user: UserEvent, topbar: HTMLElement, ) => { - jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); - jest.spyOn(api, "createTemplateVersion").mockResolvedValue({ + jest.spyOn(API, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); + jest.spyOn(API, "createTemplateVersion").mockResolvedValue({ ...templateVersion, job: { ...templateVersion.job, @@ -81,10 +83,10 @@ const buildTemplateVersion = async ( }, }); jest - .spyOn(api, "getTemplateVersionByName") + .spyOn(API, "getTemplateVersionByName") .mockResolvedValue(templateVersion); jest - .spyOn(api, "watchBuildLogsByTemplateVersionId") + .spyOn(apiModule, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); options.onDone?.(); @@ -116,10 +118,10 @@ test("Use custom name, message and set it as active when publishing", async () = // Publish const patchTemplateVersion = jest - .spyOn(api, "patchTemplateVersion") + .spyOn(API, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(api, "updateActiveTemplateVersion") + .spyOn(API, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); const publishButton = within(topbar).getByRole("button", { name: "Publish", @@ -162,10 +164,10 @@ test("Do not mark as active if promote is not checked", async () => { // Publish const patchTemplateVersion = jest - .spyOn(api, "patchTemplateVersion") + .spyOn(API, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(api, "updateActiveTemplateVersion") + .spyOn(API, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); const publishButton = within(topbar).getByRole("button", { name: "Publish", @@ -207,7 +209,7 @@ test("Patch request is not send when there are no changes", async () => { // Publish const patchTemplateVersion = jest - .spyOn(api, "patchTemplateVersion") + .spyOn(API, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const publishButton = within(topbar).getByRole("button", { name: "Publish", diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 10412bd616a67..3a622630cd770 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -2,7 +2,7 @@ import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { patchTemplateVersion, updateActiveTemplateVersion } from "api/api"; +import { API } from "api/api"; import { file, uploadFile } from "api/queries/files"; import { createTemplateVersion, @@ -323,12 +323,12 @@ const publishVersion = async (options: { const publishActions: Promise[] = []; if (haveChanges) { - publishActions.push(patchTemplateVersion(version.id, data)); + publishActions.push(API.patchTemplateVersion(version.id, data)); } if (isActiveVersion) { publishActions.push( - updateActiveTemplateVersion(version.template_id!, { + API.updateActiveTemplateVersion(version.template_id!, { id: version.id, }), ); diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx index 8b910602474d0..26112b743d1e7 100644 --- a/site/src/pages/TerminalPage/TerminalPage.test.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx @@ -2,7 +2,7 @@ import "jest-canvas-mock"; import { waitFor } from "@testing-library/react"; import WS from "jest-websocket-mock"; import { HttpResponse, http } from "msw"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockUser, MockWorkspace, diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 4bce60a5fe465..7687e95e90a49 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, screen, waitFor } from "@testing-library/react"; -import * as API from "api/api"; +import { API } from "api/api"; import { mockApiError } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import * as AccountForm from "./AccountForm"; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index 01c0ad3addfd0..5cb6ad6d3edee 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx @@ -1,6 +1,6 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockUser } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import { AppearancePage } from "./AppearancePage"; diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index daa03d50ea839..c6e706f98e769 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, screen, within } from "@testing-library/react"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockGitSSHKey, mockApiError } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage"; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index f2f0c73c4d7c9..8289da7ee9e5b 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as API from "api/api"; +import { API } from "api/api"; import type { OAuthConversionResponse } from "api/typesGenerated"; import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities"; import { diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx index f0191310656c5..b3cb38969f1c0 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx @@ -1,6 +1,6 @@ import type { ComponentProps, FC } from "react"; import { useMutation, useQuery } from "react-query"; -import { getUserLoginType } from "api/api"; +import { API } from "api/api"; import { authMethods, updatePassword } from "api/queries/users"; import { displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; @@ -19,7 +19,7 @@ export const SecurityPage: FC = () => { const authMethodsQuery = useQuery(authMethods()); const { data: userLoginType } = useQuery({ queryKey: ["loginType"], - queryFn: getUserLoginType, + queryFn: API.getUserLoginType, }); const singleSignOnSection = useSingleSignOnSection(); diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index f2c14dcd45762..78d7cfb0cb23f 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -7,7 +7,7 @@ import Link from "@mui/material/Link"; import TextField from "@mui/material/TextField"; import { type FC, useState } from "react"; import { useMutation } from "react-query"; -import { convertToOAUTH } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import type { AuthMethods, @@ -52,7 +52,7 @@ export const useSingleSignOnSection = () => { const [loginTypeConfirmation, setLoginTypeConfirmation] = useState({ open: false, selectedType: undefined }); - const mutation = useMutation(convertToOAUTH, { + const mutation = useMutation(API.convertToOAUTH, { onSuccess: (data) => { const loginTypeMsg = data.to_type === "github" ? "Github" : "OpenID Connect"; diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts index a92252ecc8b8a..9909888dd0494 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts +++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts @@ -4,7 +4,7 @@ import { useQuery, useQueryClient, } from "react-query"; -import { getTokens, deleteToken } from "api/api"; +import { API } from "api/api"; import type { TokensFilter } from "api/typesGenerated"; // Load all tokens @@ -12,10 +12,7 @@ export const useTokensData = ({ include_all }: TokensFilter) => { const queryKey = ["tokens", include_all]; const result = useQuery({ queryKey, - queryFn: () => - getTokens({ - include_all, - }), + queryFn: () => API.getTokens({ include_all }), }); return { @@ -29,7 +26,7 @@ export const useDeleteToken = (queryKey: QueryKey) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: deleteToken, + mutationFn: API.deleteToken, onSuccess: () => { // Invalidate and refetch void queryClient.invalidateQueries(queryKey); diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 7ed98850dc401..ebc5e24a5e6b6 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { HttpResponse, http } from "msw"; -import * as API from "api/api"; +import { API } from "api/api"; import type { Role } from "api/typesGenerated"; import { MockUser, diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index e358c50954d03..db5628bfc0bb3 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor } from "@testing-library/react"; import WS from "jest-websocket-mock"; -import * as API from "api/api"; +import { API } from "api/api"; import { MockWorkspace, MockWorkspaceAgent, diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 13ec3028248bb..bc3a914a10bb1 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -3,7 +3,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useParams } from "react-router-dom"; -import { getWorkspaceBuilds } from "api/api"; +import { API } from "api/api"; import { workspaceBuildByNumber } from "api/queries/workspaceBuilds"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { pageTitle } from "utils/page"; @@ -26,7 +26,7 @@ export const WorkspaceBuildPage: FC = () => { const buildsQuery = useQuery({ queryKey: ["builds", username, build?.workspace_id], queryFn: () => { - return getWorkspaceBuilds(build?.workspace_id ?? "", { + return API.getWorkspaceBuilds(build?.workspace_id ?? "", { since: dayjs().add(-30, "day").toISOString(), }); }, diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx index e203ccd671366..5916557a1c409 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx @@ -5,7 +5,7 @@ import visuallyHidden from "@mui/utils/visuallyHidden"; import { useFormik } from "formik"; import type { FC } from "react"; import { useQuery } from "react-query"; -import { getWorkspaceParameters } from "api/api"; +import { API } from "api/api"; import type { TemplateVersionParameter, Workspace, @@ -49,7 +49,7 @@ export const BuildParametersPopover: FC = ({ }) => { const { data: parameters } = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], - queryFn: () => getWorkspaceParameters(workspace), + queryFn: () => API.getWorkspaceParameters(workspace), }); const ephemeralParameters = parameters ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 0a69834992638..9766d76f692a3 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,7 +1,7 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { HttpResponse, http } from "msw"; -import * as api from "api/api"; +import * as apiModule from "api/api"; import type { TemplateVersionParameter, Workspace } from "api/typesGenerated"; import EventSourceMock from "eventsourcemock"; import { @@ -22,16 +22,18 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; +const { API, MissingBuildParameters } = apiModule; + // Renders the workspace page and waits for it be loaded const renderWorkspacePage = async (workspace: Workspace) => { - jest.spyOn(api, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); - jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); + jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); + jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); jest - .spyOn(api, "getDeploymentConfig") + .spyOn(API, "getDeploymentConfig") .mockResolvedValueOnce(MockDeploymentConfig); jest - .spyOn(api, "watchWorkspaceAgentLogs") + .spyOn(apiModule, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { options.onDone?.(); return new WebSocket(""); @@ -87,7 +89,7 @@ describe("WorkspacePage", () => { it("requests a delete job when the user presses Delete and confirms", async () => { const user = userEvent.setup({ delay: 0 }); const deleteWorkspaceMock = jest - .spyOn(api, "deleteWorkspace") + .spyOn(API, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await renderWorkspacePage(MockWorkspace); @@ -127,7 +129,7 @@ describe("WorkspacePage", () => { ); const deleteWorkspaceMock = jest - .spyOn(api, "deleteWorkspace") + .spyOn(API, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuildDelete); await renderWorkspacePage(MockFailedWorkspace); @@ -173,7 +175,7 @@ describe("WorkspacePage", () => { ); const startWorkspaceMock = jest - .spyOn(api, "startWorkspace") + .spyOn(API, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); @@ -181,7 +183,7 @@ describe("WorkspacePage", () => { it("requests a stop job when the user presses Stop", async () => { const stopWorkspaceMock = jest - .spyOn(api, "stopWorkspace") + .spyOn(API, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await testButton(MockWorkspace, "Stop", stopWorkspaceMock); @@ -189,7 +191,7 @@ describe("WorkspacePage", () => { it("requests a stop when the user presses Restart", async () => { const stopWorkspaceMock = jest - .spyOn(api, "stopWorkspace") + .spyOn(API, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -215,7 +217,7 @@ describe("WorkspacePage", () => { ); const cancelWorkspaceMock = jest - .spyOn(api, "cancelWorkspaceBuild") + .spyOn(API, "cancelWorkspaceBuild") .mockImplementation(() => Promise.resolve({ message: "job canceled" })); await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock); @@ -224,11 +226,11 @@ describe("WorkspacePage", () => { it("requests an update when the user presses Update", async () => { // Mocks jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceMock = jest - .spyOn(api, "updateWorkspace") + .spyOn(API, "updateWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -249,12 +251,12 @@ describe("WorkspacePage", () => { it("updates the parameters when they are missing during update", async () => { // Mocks jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceSpy = jest - .spyOn(api, "updateWorkspace") + .spyOn(API, "updateWorkspace") .mockRejectedValueOnce( - new api.MissingBuildParameters( + new MissingBuildParameters( [MockTemplateVersionParameter1, MockTemplateVersionParameter2], MockOutdatedWorkspace.template_active_version_id, ), @@ -271,7 +273,7 @@ describe("WorkspacePage", () => { // The update was called await waitFor(() => { - expect(api.updateWorkspace).toBeCalled(); + expect(API.updateWorkspace).toBeCalled(); updateWorkspaceSpy.mockClear(); }); @@ -294,7 +296,7 @@ describe("WorkspacePage", () => { // Check if the update was called using the values from the form await waitFor(() => { - expect(api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ + expect(API.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ { name: MockTemplateVersionParameter1.name, value: "some-value", @@ -309,7 +311,7 @@ describe("WorkspacePage", () => { it("restart the workspace with one time parameters when having the confirmation dialog", async () => { localStorage.removeItem(`${MockUser.id}_ignoredWarnings`); - jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ + jest.spyOn(API, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { ...MockTemplateVersionParameter1, @@ -321,7 +323,7 @@ describe("WorkspacePage", () => { ], buildParameters: [{ name: "rebuild", value: "false" }], }); - const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); + const restartWorkspaceSpy = jest.spyOn(API, "restartWorkspace"); const user = userEvent.setup(); await renderWorkspacePage(MockWorkspace); await user.click(screen.getByTestId("build-parameters-button")); @@ -351,7 +353,7 @@ describe("WorkspacePage", () => { const retryDebugButtonRe = /^Debug$/i; describe("Retries a failed 'Start' transition", () => { - const mockStart = jest.spyOn(api, "startWorkspace"); + const mockStart = jest.spyOn(API, "startWorkspace"); const failedStart: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -384,7 +386,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Stop' transition", () => { - const mockStop = jest.spyOn(api, "stopWorkspace"); + const mockStop = jest.spyOn(API, "stopWorkspace"); const failedStop: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -405,7 +407,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Delete' transition", () => { - const mockDelete = jest.spyOn(api, "deleteWorkspace"); + const mockDelete = jest.spyOn(API, "deleteWorkspace"); const failedDelete: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -450,7 +452,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(api, "startWorkspace"); + const startWorkspaceSpy = jest.spyOn(API, "startWorkspace"); await renderWorkspacePage(workspace); const retryWithBuildParametersButton = await screen.findByRole("button", { @@ -496,7 +498,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(api, "startWorkspace"); + const startWorkspaceSpy = jest.spyOn(API, "startWorkspace"); await renderWorkspacePage(workspace); const retryWithBuildParametersButton = await screen.findByRole("button", { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e460c7163c1e6..f3750051823ff 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -3,7 +3,7 @@ import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate } from "react-router-dom"; -import { MissingBuildParameters, restartWorkspace } from "api/api"; +import { MissingBuildParameters, API } from "api/api"; import { getErrorMessage } from "api/errors"; import { buildInfo } from "api/queries/buildInfo"; import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; @@ -83,7 +83,7 @@ export const WorkspaceReadyPage: FC = ({ }>({ open: false }); const { mutate: mutateRestartWorkspace, isLoading: isRestarting } = useMutation({ - mutationFn: restartWorkspace, + mutationFn: API.restartWorkspace, }); // SSH Prefix diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx index 71e4174499305..07c13a10122c4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx @@ -5,7 +5,7 @@ import { HttpResponse, http } from "msw"; import type { FC } from "react"; import { QueryClient, QueryClientProvider, useQuery } from "react-query"; import { RouterProvider, createMemoryRouter } from "react-router-dom"; -import * as API from "api/api"; +import { API } from "api/api"; import { workspaceByOwnerAndName } from "api/queries/workspaces"; import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar"; import { ThemeProvider } from "contexts/ThemeProvider"; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 99b4f692d1db2..af15a4423a44a 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as api from "api/api"; +import { API } from "api/api"; import { MockWorkspace, MockTemplateVersionParameter1, @@ -20,15 +20,15 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockWorkspace); - jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([ + jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter2, // Immutable parameters MockTemplateVersionParameter4, ]); - jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValueOnce([ + jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([ MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, // Immutable value @@ -36,7 +36,7 @@ test("Submit the workspace settings page successfully", async () => { ]); // Mock the API calls that submit data const postWorkspaceBuildSpy = jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValue(MockWorkspaceBuild); // Setup event and rendering const user = userEvent.setup(); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index c10accb30e9a0..7da0fc203d401 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -4,7 +4,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; -import { getWorkspaceParameters, postWorkspaceBuild } from "api/api"; +import { API } from "api/api"; import { isApiValidationError } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; import { templateByName } from "api/queries/templates"; @@ -29,12 +29,12 @@ const WorkspaceParametersPage: FC = () => { const workspace = useWorkspaceSettings(); const parameters = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], - queryFn: () => getWorkspaceParameters(workspace), + queryFn: () => API.getWorkspaceParameters(workspace), }); const navigate = useNavigate(); const updateParameters = useMutation({ mutationFn: (buildParameters: WorkspaceBuildParameter[]) => - postWorkspaceBuild(workspace.id, { + API.postWorkspaceBuild(workspace.id, { transition: "start", rich_parameter_values: buildParameters, }), @@ -93,7 +93,7 @@ const WorkspaceParametersPage: FC = () => { export type WorkspaceParametersPageViewProps = { workspace: Workspace; canChangeVersions: boolean; - data: Awaited> | undefined; + data: Awaited> | undefined; submitError: unknown; isSubmitting: boolean; onSubmit: (formValues: WorkspaceParametersFormValues) => void; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx index 870a28b4f2f0b..dd5269758bb41 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.tsx @@ -1,5 +1,5 @@ import { screen } from "@testing-library/react"; -import * as API from "api/api"; +import { API } from "api/api"; import { defaultSchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; import { MockTemplate } from "testHelpers/entities"; import { render } from "testHelpers/renderHelpers"; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 7830a161c879e..79b14bec16184 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -3,11 +3,7 @@ import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; -import { - putWorkspaceAutostart, - putWorkspaceAutostop, - startWorkspace, -} from "api/api"; +import { API } from "api/api"; import { checkAuthorization } from "api/queries/authCheck"; import { templateByName } from "api/queries/templates"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; @@ -72,7 +68,7 @@ export const WorkspaceSchedulePage: FC = () => { const [isConfirmingApply, setIsConfirmingApply] = useState(false); const { mutate: updateWorkspace } = useMutation({ mutationFn: () => - startWorkspace(workspace.id, workspace.template_active_version_id), + API.startWorkspace(workspace.id, workspace.template_active_version_id), }); return ( @@ -167,11 +163,11 @@ const submitSchedule = async (data: SubmitScheduleData) => { const actions: Promise[] = []; if (autostartChanged) { - actions.push(putWorkspaceAutostart(workspace.id, autostart)); + actions.push(API.putWorkspaceAutostart(workspace.id, autostart)); } if (autostopChanged) { - actions.push(putWorkspaceAutostop(workspace.id, ttl)); + actions.push(API.putWorkspaceAutostop(workspace.id, ttl)); } return Promise.all(actions); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx index 4fa1bc8a4d536..a7ce4d63c897d 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import * as api from "api/api"; +import { API } from "api/api"; import { MockWorkspace } from "testHelpers/entities"; import { renderWithWorkspaceSettingsLayout, @@ -11,11 +11,11 @@ import WorkspaceSettingsPage from "./WorkspaceSettingsPage"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace }); // Mock the API calls that submit data const patchWorkspaceSpy = jest - .spyOn(api, "patchWorkspace") + .spyOn(API, "patchWorkspace") .mockResolvedValue(); // Setup event and rendering const user = userEvent.setup(); @@ -43,7 +43,7 @@ test("Submit the workspace settings page successfully", async () => { test("Name field is disabled if renames are disabled", async () => { // Mock the API calls that loads data jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false }); renderWithWorkspaceSettingsLayout(, { route: "/@test-user/test-workspace/settings", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index e289a58c5ce59..09bf002fd7cb9 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; -import { patchWorkspace, updateWorkspaceAutomaticUpdates } from "api/api"; +import { API } from "api/api"; import { displaySuccess } from "components/GlobalSnackbar/utils"; import { pageTitle } from "utils/page"; import type { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm"; @@ -22,8 +22,8 @@ const WorkspaceSettingsPage: FC = () => { const mutation = useMutation({ mutationFn: async (formValues: WorkspaceSettingsFormValues) => { await Promise.all([ - patchWorkspace(workspace.id, { name: formValues.name }), - updateWorkspaceAutomaticUpdates( + API.patchWorkspace(workspace.id, { name: formValues.name }), + API.updateWorkspaceAutomaticUpdates( workspace.id, formValues.automatic_updates, ), diff --git a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx index de19212bcbefa..f5ea3589e2af4 100644 --- a/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchUpdateConfirmation.tsx @@ -7,7 +7,7 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { type FC, type ReactNode, useMemo, useState, useEffect } from "react"; import { useQueries } from "react-query"; -import { getTemplateVersion } from "api/api"; +import { API } from "api/api"; import type { TemplateVersion, Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; @@ -129,7 +129,7 @@ export const BatchUpdateConfirmation: FC = ({ // ...but the query _also_ doesn't have everything we need, like the // template display name! ...version, - ...(await getTemplateVersion(version.id)), + ...(await API.getTemplateVersion(version.id)), }), })), }); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 5b0dc1b2a959e..9c152b1ac0534 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -1,7 +1,7 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { HttpResponse, http } from "msw"; -import * as API from "api/api"; +import { API } from "api/api"; import type { Workspace } from "api/typesGenerated"; import { MockStoppedWorkspace, diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx index a9e3eb1cf4c7c..38819cdf60c88 100644 --- a/site/src/pages/WorkspacesPage/batchActions.tsx +++ b/site/src/pages/WorkspacesPage/batchActions.tsx @@ -1,12 +1,5 @@ import { useMutation } from "react-query"; -import { - deleteWorkspace, - deleteFavoriteWorkspace, - putFavoriteWorkspace, - startWorkspace, - stopWorkspace, - updateWorkspace, -} from "api/api"; +import { API } from "api/api"; import type { Workspace } from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -21,7 +14,7 @@ export function useBatchActions(options: UseBatchActionsProps) { mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces.map((w) => - startWorkspace(w.id, w.latest_build.template_version_id), + API.startWorkspace(w.id, w.latest_build.template_version_id), ), ); }, @@ -33,7 +26,7 @@ export function useBatchActions(options: UseBatchActionsProps) { const stopAllMutation = useMutation({ mutationFn: (workspaces: readonly Workspace[]) => { - return Promise.all(workspaces.map((w) => stopWorkspace(w.id))); + return Promise.all(workspaces.map((w) => API.stopWorkspace(w.id))); }, onSuccess, onError: () => { @@ -43,7 +36,7 @@ export function useBatchActions(options: UseBatchActionsProps) { const deleteAllMutation = useMutation({ mutationFn: (workspaces: readonly Workspace[]) => { - return Promise.all(workspaces.map((w) => deleteWorkspace(w.id))); + return Promise.all(workspaces.map((w) => API.deleteWorkspace(w.id))); }, onSuccess, onError: () => { @@ -56,7 +49,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) - .map((w) => updateWorkspace(w)), + .map((w) => API.updateWorkspace(w)), ); }, onSuccess, @@ -70,7 +63,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => !w.favorite) - .map((w) => putFavoriteWorkspace(w.id)), + .map((w) => API.putFavoriteWorkspace(w.id)), ); }, onSuccess, @@ -84,7 +77,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.favorite) - .map((w) => deleteFavoriteWorkspace(w.id)), + .map((w) => API.deleteFavoriteWorkspace(w.id)), ); }, onSuccess, diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index a785d00d03122..e1b8eec25ccb3 100644 --- a/site/src/pages/WorkspacesPage/data.ts +++ b/site/src/pages/WorkspacesPage/data.ts @@ -5,7 +5,7 @@ import { useQuery, useQueryClient, } from "react-query"; -import { getWorkspaces, updateWorkspaceVersion } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import type { Workspace, @@ -30,7 +30,7 @@ export const useWorkspacesData = ({ const result = useQuery({ queryKey, queryFn: () => - getWorkspaces({ + API.getWorkspaces({ q: query, limit: limit, offset: page <= 0 ? 0 : (page - 1) * limit, @@ -54,7 +54,7 @@ export const useWorkspaceUpdate = (queryKey: QueryKey) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: updateWorkspaceVersion, + mutationFn: API.updateWorkspaceVersion, onMutate: async (workspace) => { await queryClient.cancelQueries({ queryKey }); queryClient.setQueryData(queryKey, (oldResponse) => { diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts index a8db56dd5a226..f8b6755f50e82 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.ts +++ b/site/src/pages/WorkspacesPage/filter/menus.ts @@ -1,4 +1,4 @@ -import { getTemplates } from "api/api"; +import { API } from "api/api"; import type { WorkspaceStatus } from "api/typesGenerated"; import { useFilterMenu, @@ -21,7 +21,7 @@ export const useTemplateFilterMenu = ({ id: "template", getSelectedOption: async () => { // Show all templates including deprecated - const templates = await getTemplates(organizationId); + const templates = await API.getTemplates(organizationId); const template = templates.find((template) => template.name === value); if (template) { return { @@ -37,7 +37,7 @@ export const useTemplateFilterMenu = ({ }, getOptions: async (query) => { // Show all templates including deprecated - const templates = await getTemplates(organizationId); + const templates = await API.getTemplates(organizationId); const filteredTemplates = templates.filter( (template) => template.name.toLowerCase().includes(query.toLowerCase()) || diff --git a/site/src/utils/terminal.ts b/site/src/utils/terminal.ts index d27a6efce379c..82c98a370a51f 100644 --- a/site/src/utils/terminal.ts +++ b/site/src/utils/terminal.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { API } from "api/api"; export const terminalWebsocketUrl = async ( baseUrl: string | undefined,