From 445abab14254a154c9c754f37cd76a144a6c839a Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Wed, 1 May 2024 14:44:53 +0000 Subject: [PATCH 1/7] wip: commit progress on code split-up --- site/src/api/api.ts | 183 +++++++++++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 44 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6b4102073b3d1..323a41ce2dd8e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -19,12 +19,150 @@ * * 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"; +//////////////////////////////////////////////////////////////////////////////// +// START OF API FILE +//////////////////////////////////////////////////////////////////////////////// + +// withDefaultFeatures sets all unspecified features to not_entitled and +// disabled. +export const withDefaultFeatures = ( + fs: Partial<TypesGen.Entitlements["features"]>, +): TypesGen.Entitlements["features"] => { + for (const feature of TypesGen.FeatureNames) { + // Skip fields that are already filled. + if (fs[feature] !== undefined) { + continue; + } + + fs[feature] = { + enabled: false, + entitlement: "not_entitled", + }; + } + + return fs as TypesGen.Entitlements["features"]; +}; + +const CONTENT_TYPE_JSON = { + "Content-Type": "application/json", +} as const satisfies HeadersInit; + +export class Api { + constructor(private readonly axios: AxiosInstance) {} + + login = async ( + email: string, + password: string, + ): Promise<TypesGen.LoginWithPasswordResponse> => { + const payload = JSON.stringify({ + email, + password, + }); + + const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>( + "/api/v2/users/login", + payload, + { headers: CONTENT_TYPE_JSON }, + ); + + return response.data; + }; +} + +//////////////////////////////////////////////////////////////////////////////// +// START OF CLIENT FILE +//////////////////////////////////////////////////////////////////////////////// + +// 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; + +interface ClientApi { + api: Api; + getCsrfToken: () => string; + setSessionToken: (token: string) => void; + setHost: (host: string | undefined) => void; +} + +export class Client implements ClientApi { + private readonly axios: AxiosInstance; + readonly api: Api; + + constructor() { + this.axios = this.getConfiguredAxiosInstance(); + this.api = new Api(this.axios); + } + + private getConfiguredAxiosInstance(): AxiosInstance { + const axios = globalAxios.create(); + + // Adds 304 for the default axios validateStatus function + // https://github.com/axios/axios#handling-errors Check status here + // https://httpstatusdogs.com/ + axios.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 + this.axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; + axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; + tokenMetadataElement.setAttribute("content", csrfToken); + } else { + axios.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 axios; + } + + 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; + }; +} + +export const client = new Client(); + +//////////////////////////////////////////////////////////////////////////////// +// START OF OLD CODE +//////////////////////////////////////////////////////////////////////////////// + export const axiosInstance = globalAxios.create(); // Adds 304 for the default axios validateStatus function @@ -47,24 +185,6 @@ export const hardCodedCSRFCookie = (): string => { return csrfToken; }; -// withDefaultFeatures sets all unspecified features to not_entitled and -// disabled. -export const withDefaultFeatures = ( - fs: Partial<TypesGen.Entitlements["features"]>, -): TypesGen.Entitlements["features"] => { - for (const feature of TypesGen.FeatureNames) { - // Skip fields that are already filled. - 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 = @@ -97,31 +217,6 @@ export const setHost = (host?: string) => { axiosInstance.defaults.baseURL = host; }; -const CONTENT_TYPE_JSON = { - "Content-Type": "application/json", -}; - -export const provisioners: TypesGen.ProvisionerDaemon[] = [ - { - id: "terraform", - name: "Terraform", - created_at: "", - provisioners: [], - tags: {}, - version: "v2.34.5", - api_version: "1.0", - }, - { - 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, From 08849639284d7ce1f151c9c70e4532735285f1b1 Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Wed, 1 May 2024 15:49:04 +0000 Subject: [PATCH 2/7] wip: commit more progress --- site/src/api/api.ts | 712 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 653 insertions(+), 59 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 323a41ce2dd8e..873d0addf4bdd 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -29,6 +29,38 @@ import * as TypesGen from "./typesGenerated"; // START OF API FILE //////////////////////////////////////////////////////////////////////////////// +/** + * @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 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 // disabled. export const withDefaultFeatures = ( @@ -49,10 +81,31 @@ export const withDefaultFeatures = ( return fs as TypesGen.Entitlements["features"]; }; -const CONTENT_TYPE_JSON = { +// 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; +type TemplateOptions = Readonly<{ + readonly deprecated?: boolean; +}>; + +type SearchParamOptions = TypesGen.Pagination & { + q?: string; +}; + +type RestartWorkspaceParameters = Readonly<{ + workspace: TypesGen.Workspace; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; +}>; + +export type DeleteWorkspaceOptions = Pick< + TypesGen.CreateWorkspaceBuildRequest, + "log_level" & "orphan" +>; + export class Api { constructor(private readonly axios: AxiosInstance) {} @@ -60,19 +113,609 @@ export class Api { email: string, password: string, ): Promise<TypesGen.LoginWithPasswordResponse> => { - const payload = JSON.stringify({ - email, - password, - }); - + const payload = JSON.stringify({ email, password }); const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>( "/api/v2/users/login", payload, - { headers: CONTENT_TYPE_JSON }, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + + return response.data; + }; + + convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { + const response = await this.axios.post<TypesGen.OAuthConversionResponse>( + "/api/v2/users/me/convert-login", + request, + ); + + return response.data; + }; + + logout = async (): Promise<void> => { + return this.axios.post("/api/v2/users/logout"); + }; + + getAuthenticatedUser = async () => { + const response = await this.axios.get<TypesGen.User>("/api/v2/users/me"); + return response.data; + }; + + getUserParameters = async (templateID: string) => { + const response = await this.axios.get<TypesGen.UserParameter[]>( + `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + ); + + return response.data; + }; + + getAuthMethods = async (): Promise<TypesGen.AuthMethods> => { + const response = await this.axios.get<TypesGen.AuthMethods>( + "/api/v2/users/authmethods", + ); + + return response.data; + }; + + getUserLoginType = async (): Promise<TypesGen.UserLoginType> => { + const response = await this.axios.get<TypesGen.UserLoginType>( + "/api/v2/users/me/login-type", + ); + + return response.data; + }; + + checkAuthorization = async ( + params: TypesGen.AuthorizationRequest, + ): Promise<TypesGen.AuthorizationResponse> => { + const response = await this.axios.post<TypesGen.AuthorizationResponse>( + `/api/v2/authcheck`, + params, + ); + + return response.data; + }; + + getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => { + const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>( + "/api/v2/users/me/keys", + ); + + return response.data; + }; + + getTokens = async ( + params: TypesGen.TokensFilter, + ): Promise<TypesGen.APIKeyWithOwner[]> => { + const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>( + `/api/v2/users/me/keys/tokens`, + { params }, + ); + + return response.data; + }; + + deleteToken = async (keyId: string): Promise<void> => { + await this.axios.delete("/api/v2/users/me/keys/" + keyId); + }; + + createToken = async ( + params: TypesGen.CreateTokenRequest, + ): Promise<TypesGen.GenerateAPIKeyResponse> => { + const response = await this.axios.post( + `/api/v2/users/me/keys/tokens`, + params, + ); + + return response.data; + }; + + getTokenConfig = async (): Promise<TypesGen.TokenConfig> => { + const response = await this.axios.get( + "/api/v2/users/me/keys/tokens/tokenconfig", + ); + + return response.data; + }; + + getUsers = async ( + options: TypesGen.UsersRequest, + signal?: AbortSignal, + ): Promise<TypesGen.GetUsersResponse> => { + const url = getURLWithSearchParams("/api/v2/users", options); + const response = await this.axios.get<TypesGen.GetUsersResponse>( + url.toString(), + { signal }, + ); + + return response.data; + }; + + getOrganization = async ( + organizationId: string, + ): Promise<TypesGen.Organization> => { + const response = await this.axios.get<TypesGen.Organization>( + `/api/v2/organizations/${organizationId}`, + ); + + return response.data; + }; + + getOrganizations = async (): Promise<TypesGen.Organization[]> => { + const response = await this.axios.get<TypesGen.Organization[]>( + "/api/v2/users/me/organizations", + ); + return response.data; + }; + + getTemplate = async (templateId: string): Promise<TypesGen.Template> => { + const response = await this.axios.get<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getTemplates = async ( + organizationId: string, + options?: TemplateOptions, + ): Promise<TypesGen.Template[]> => { + const params: Record<string, string> = {}; + 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); + } + + const response = await this.axios.get<TypesGen.Template[]>( + `/api/v2/organizations/${organizationId}/templates`, + { params }, + ); + + return response.data; + }; + + getTemplateByName = async ( + organizationId: string, + name: string, + ): Promise<TypesGen.Template> => { + const response = await this.axios.get<TypesGen.Template>( + `/api/v2/organizations/${organizationId}/templates/${name}`, + ); + + return response.data; + }; + + getTemplateVersion = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${versionId}`, + ); + + return response.data; + }; + + getTemplateVersionResources = async ( + versionId: string, + ): Promise<TypesGen.WorkspaceResource[]> => { + const response = await this.axios.get<TypesGen.WorkspaceResource[]>( + `/api/v2/templateversions/${versionId}/resources`, + ); + + return response.data; + }; + + getTemplateVersionVariables = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionVariable[]> => { + // Defined as separate variable to avoid wonky Prettier formatting because + // the type definition is so long + type VerArray = TypesGen.TemplateVersionVariable[]; + + const response = await axiosInstance.get<VerArray>( + `/api/v2/templateversions/${versionId}/variables`, + ); + + return response.data; + }; + + getTemplateVersions = async ( + templateId: string, + ): Promise<TypesGen.TemplateVersion[]> => { + const response = await this.axios.get<TypesGen.TemplateVersion[]>( + `/api/v2/templates/${templateId}/versions`, + ); + return response.data; + }; + + getTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, + ); + + return response.data; + }; + + getPreviousTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ) => { + try { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/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 + const is404 = + isAxiosError(error) && error.response && error.response.status === 404; + + if (is404) { + return undefined; + } + + throw error; + } + }; + + createTemplateVersion = async ( + organizationId: string, + data: TypesGen.CreateTemplateVersionRequest, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/organizations/${organizationId}/templateversions`, + data, + ); + + return response.data; + }; + + getTemplateVersionExternalAuth = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionExternalAuth[]> => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/external-auth`, + ); + + return response.data; + }; + + getTemplateVersionRichParameters = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionParameter[]> => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ); + return response.data; + }; + + createTemplate = async ( + organizationId: string, + data: TypesGen.CreateTemplateRequest, + ): Promise<TypesGen.Template> => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templates`, + data, + ); + + return response.data; + }; + + updateActiveTemplateVersion = async ( + templateId: string, + data: TypesGen.UpdateActiveTemplateVersion, + ) => { + const response = await this.axios.patch<TypesGen.Response>( + `/api/v2/templates/${templateId}/versions`, + data, + ); + return response.data; + }; + + patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, + ) => { + const response = await this.axios.patch<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}`, + data, ); return response.data; }; + + archiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}/archive`, + ); + + return response.data; + }; + + unarchiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}/unarchive`, + ); + return response.data; + }; + + updateTemplateMeta = async ( + templateId: string, + data: TypesGen.UpdateTemplateMeta, + ): Promise<TypesGen.Template | null> => { + const response = await this.axios.patch<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + data, + ); + + // On 304 response there is no data payload. + if (response.status === 304) { + return null; + } + + return response.data; + }; + + deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => { + const response = await this.axios.delete<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.get<TypesGen.Workspace>( + `/api/v2/workspaces/${workspaceId}`, + { params }, + ); + + return response.data; + }; + + getWorkspaces = async ( + options: TypesGen.WorkspacesRequest, + ): Promise<TypesGen.WorkspacesResponse> => { + const url = getURLWithSearchParams("/api/v2/workspaces", options); + const response = await this.axios.get<TypesGen.WorkspacesResponse>(url); + return response.data; + }; + + getWorkspaceByOwnerAndName = async ( + username = "me", + workspaceName: string, + params?: TypesGen.WorkspaceOptions, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.get<TypesGen.Workspace>( + `/api/v2/users/${username}/workspace/${workspaceName}`, + { params }, + ); + + return response.data; + }; + + getWorkspaceBuildByNumber = async ( + username = "me", + workspaceName: string, + buildNumber: number, + ): Promise<TypesGen.WorkspaceBuild> => { + const response = await this.axios.get<TypesGen.WorkspaceBuild>( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ); + + return response.data; + }; + + waitForBuild = (build: TypesGen.WorkspaceBuild) => { + return new Promise<TypesGen.ProvisionerJob | undefined>((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); + } + + return res(latestJobInfo); + })(); + }); + }; + + postWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.CreateWorkspaceBuildRequest, + ): Promise<TypesGen.WorkspaceBuild> => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ); + + 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, + }); + }; + + stopWorkspace = ( + workspaceId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: "stop", + log_level: logLevel, + }); + }; + + deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { + return postWorkspaceBuild(workspaceId, { + transition: "delete", + ...options, + }); + }; + + cancelWorkspaceBuild = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], + ): Promise<TypesGen.Response> => { + const response = await this.axios.patch( + `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + ); + + return response.data; + }; + + updateWorkspaceDormancy = async ( + workspaceId: string, + dormant: boolean, + ): Promise<TypesGen.Workspace> => { + const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/dormant`, + data, + ); + + return response.data; + }; + + updateWorkspaceAutomaticUpdates = async ( + workspaceId: string, + automaticUpdates: TypesGen.AutomaticUpdates, + ): Promise<void> => { + const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { + automatic_updates: automaticUpdates, + }; + + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/autoupdates`, + req, + ); + + return response.data; + }; + + restartWorkspace = async ({ + workspace, + buildParameters, + }: RestartWorkspaceParameters): Promise<void> => { + const stopBuild = await this.stopWorkspace(workspace.id); + const awaitedStopBuild = await this.waitForBuild(stopBuild); + + // If the restart is canceled halfway through, make sure we bail + if (awaitedStopBuild?.status === "canceled") { + return; + } + + const startBuild = await this.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + undefined, + buildParameters, + ); + + await waitForBuild(startBuild); + }; + + cancelTemplateVersionBuild = async ( + templateVersionId: TypesGen.TemplateVersion["id"], + ): Promise<TypesGen.Response> => { + const response = await axiosInstance.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ); + + return response.data; + }; + + createUser = async ( + user: TypesGen.CreateUserRequest, + ): Promise<TypesGen.User> => { + const response = await this.axios.post<TypesGen.User>( + "/api/v2/users", + user, + ); + + return response.data; + }; + + createWorkspace = async ( + organizationId: string, + userId = "me", + workspace: TypesGen.CreateWorkspaceRequest, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.post<TypesGen.Workspace>( + `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + workspace, + ); + + return response.data; + }; + + patchWorkspace = async ( + workspaceId: string, + data: TypesGen.UpdateWorkspaceRequest, + ): Promise<void> => { + await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + }; + + getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => { + const response = await this.axios.get("/api/v2/buildinfo"); + return response.data; + }; + + getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => { + const response = await axiosInstance.get("/api/v2/updatecheck"); + return response.data; + }; + + putWorkspaceAutostart = async ( + workspaceID: string, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, + ): Promise<void> => { + const payload = JSON.stringify(autostart); + await this.axios.put( + `/api/v2/workspaces/${workspaceID}/autostart`, + payload, + { + headers: { ...BASE_CONTENT_TYPE_JSON }, + }, + ); + }; } //////////////////////////////////////////////////////////////////////////////// @@ -230,7 +873,7 @@ export const login = async ( "/api/v2/users/login", payload, { - headers: { ...CONTENT_TYPE_JSON }, + headers: { ...BASE_CONTENT_TYPE_JSON }, }, ); @@ -364,10 +1007,6 @@ export const getTemplate = async ( return response.data; }; -export interface TemplateOptions { - readonly deprecated?: boolean; -} - export const getTemplates = async ( organizationId: string, options?: TemplateOptions, @@ -446,10 +1085,6 @@ export const getTemplateVersionByName = async ( return response.data; }; -export type GetPreviousTemplateVersionByNameResponse = - | TypesGen.TemplateVersion - | undefined; - export const getPreviousTemplateVersionByName = async ( organizationId: string, templateName: string, @@ -589,42 +1224,6 @@ export const getWorkspace = async ( 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 }, - ); -}; - -interface SearchParamOptions extends TypesGen.Pagination { - q?: string; -} - -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; - } -}; - export const getWorkspaces = async ( options: TypesGen.WorkspacesRequest, ): Promise<TypesGen.WorkspacesResponse> => { @@ -708,11 +1307,6 @@ export const stopWorkspace = ( log_level: logLevel, }); -export type DeleteWorkspaceOptions = Pick< - TypesGen.CreateWorkspaceBuildRequest, - "log_level" & "orphan" ->; - export const deleteWorkspace = ( workspaceId: string, options?: DeleteWorkspaceOptions, @@ -843,7 +1437,7 @@ export const putWorkspaceAutostart = async ( `/api/v2/workspaces/${workspaceID}/autostart`, payload, { - headers: { ...CONTENT_TYPE_JSON }, + headers: { ...BASE_CONTENT_TYPE_JSON }, }, ); }; @@ -854,7 +1448,7 @@ export const putWorkspaceAutostop = async ( ): Promise<void> => { const payload = JSON.stringify(ttl); await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { - headers: { ...CONTENT_TYPE_JSON }, + headers: { ...BASE_CONTENT_TYPE_JSON }, }); }; From 84b74061133b458e149e57d4c7a7076200225705 Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Wed, 1 May 2024 16:42:00 +0000 Subject: [PATCH 3/7] wip: finish initial version of class implementation --- site/src/api/api.ts | 2145 ++++++++++++++++++++++++++++++------------- 1 file changed, 1512 insertions(+), 633 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 873d0addf4bdd..976a0ba984895 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -29,6 +29,92 @@ import * as TypesGen from "./typesGenerated"; // START OF API FILE //////////////////////////////////////////////////////////////////////////////// +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 }, + ); +}; + /** * @returns {EventSource} An EventSource that emits workspace event objects * (ServerSentEvent) @@ -81,6 +167,135 @@ export const withDefaultFeatures = ( return fs as TypesGen.Entitlements["features"]; }; +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; +}; + +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" + : ""; + + 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(); + }); + + return socket; +}; + +type WatchWorkspaceAgentLogsOptions = { + after: number; + onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; + onDone?: () => void; + onError: (error: Error) => void; +}; + +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; +}; + // 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 @@ -106,615 +321,1529 @@ export type DeleteWorkspaceOptions = Pick< "log_level" & "orphan" >; -export class Api { - constructor(private readonly axios: AxiosInstance) {} +export type DeploymentConfig = Readonly<{ + config: TypesGen.DeploymentValues; + options: TypesGen.SerpentOption[]; +}>; - login = async ( - email: string, - password: string, - ): Promise<TypesGen.LoginWithPasswordResponse> => { - const payload = JSON.stringify({ email, password }); - const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>( - "/api/v2/users/login", - payload, - { headers: { ...BASE_CONTENT_TYPE_JSON } }, - ); +type Claims = { + license_expires: number; + account_type?: string; + account_id?: string; + trial: boolean; + all_features: boolean; + version: number; + features: Record<string, number>; + require_telemetry?: boolean; +}; - return response.data; - }; +export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & { + claims: Claims; + expires_at: string; +}; - convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { - const response = await this.axios.post<TypesGen.OAuthConversionResponse>( - "/api/v2/users/me/convert-login", - request, - ); +export type InsightsParams = { + start_time: string; + end_time: string; + template_ids: string; +}; - return response.data; - }; +export type InsightsTemplateParams = InsightsParams & { + interval: "day" | "week"; +}; + +export type GetJFrogXRayScanParams = { + workspaceId: string; + agentId: string; +}; + +export class MissingBuildParameters extends Error { + parameters: TypesGen.TemplateVersionParameter[] = []; + versionId: string; + + constructor( + parameters: TypesGen.TemplateVersionParameter[], + versionId: string, + ) { + super("Missing build parameters."); + this.parameters = parameters; + this.versionId = versionId; + } +} + +export class Api { + constructor(private readonly axios: AxiosInstance) {} + + login = async ( + email: string, + password: string, + ): Promise<TypesGen.LoginWithPasswordResponse> => { + const payload = JSON.stringify({ email, password }); + const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>( + "/api/v2/users/login", + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + + return response.data; + }; + + convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { + const response = await this.axios.post<TypesGen.OAuthConversionResponse>( + "/api/v2/users/me/convert-login", + request, + ); + + return response.data; + }; logout = async (): Promise<void> => { return this.axios.post("/api/v2/users/logout"); }; - getAuthenticatedUser = async () => { - const response = await this.axios.get<TypesGen.User>("/api/v2/users/me"); + getAuthenticatedUser = async () => { + const response = await this.axios.get<TypesGen.User>("/api/v2/users/me"); + return response.data; + }; + + getUserParameters = async (templateID: string) => { + const response = await this.axios.get<TypesGen.UserParameter[]>( + `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + ); + + return response.data; + }; + + getAuthMethods = async (): Promise<TypesGen.AuthMethods> => { + const response = await this.axios.get<TypesGen.AuthMethods>( + "/api/v2/users/authmethods", + ); + + return response.data; + }; + + getUserLoginType = async (): Promise<TypesGen.UserLoginType> => { + const response = await this.axios.get<TypesGen.UserLoginType>( + "/api/v2/users/me/login-type", + ); + + return response.data; + }; + + checkAuthorization = async ( + params: TypesGen.AuthorizationRequest, + ): Promise<TypesGen.AuthorizationResponse> => { + const response = await this.axios.post<TypesGen.AuthorizationResponse>( + `/api/v2/authcheck`, + params, + ); + + return response.data; + }; + + getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => { + const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>( + "/api/v2/users/me/keys", + ); + + return response.data; + }; + + getTokens = async ( + params: TypesGen.TokensFilter, + ): Promise<TypesGen.APIKeyWithOwner[]> => { + const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>( + `/api/v2/users/me/keys/tokens`, + { params }, + ); + + return response.data; + }; + + deleteToken = async (keyId: string): Promise<void> => { + await this.axios.delete("/api/v2/users/me/keys/" + keyId); + }; + + createToken = async ( + params: TypesGen.CreateTokenRequest, + ): Promise<TypesGen.GenerateAPIKeyResponse> => { + const response = await this.axios.post( + `/api/v2/users/me/keys/tokens`, + params, + ); + + return response.data; + }; + + getTokenConfig = async (): Promise<TypesGen.TokenConfig> => { + const response = await this.axios.get( + "/api/v2/users/me/keys/tokens/tokenconfig", + ); + + return response.data; + }; + + getUsers = async ( + options: TypesGen.UsersRequest, + signal?: AbortSignal, + ): Promise<TypesGen.GetUsersResponse> => { + const url = getURLWithSearchParams("/api/v2/users", options); + const response = await this.axios.get<TypesGen.GetUsersResponse>( + url.toString(), + { signal }, + ); + + return response.data; + }; + + getOrganization = async ( + organizationId: string, + ): Promise<TypesGen.Organization> => { + const response = await this.axios.get<TypesGen.Organization>( + `/api/v2/organizations/${organizationId}`, + ); + + return response.data; + }; + + getOrganizations = async (): Promise<TypesGen.Organization[]> => { + const response = await this.axios.get<TypesGen.Organization[]>( + "/api/v2/users/me/organizations", + ); + return response.data; + }; + + getTemplate = async (templateId: string): Promise<TypesGen.Template> => { + const response = await this.axios.get<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getTemplates = async ( + organizationId: string, + options?: TemplateOptions, + ): Promise<TypesGen.Template[]> => { + const params: Record<string, string> = {}; + 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); + } + + const response = await this.axios.get<TypesGen.Template[]>( + `/api/v2/organizations/${organizationId}/templates`, + { params }, + ); + + return response.data; + }; + + getTemplateByName = async ( + organizationId: string, + name: string, + ): Promise<TypesGen.Template> => { + const response = await this.axios.get<TypesGen.Template>( + `/api/v2/organizations/${organizationId}/templates/${name}`, + ); + + return response.data; + }; + + getTemplateVersion = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${versionId}`, + ); + + return response.data; + }; + + getTemplateVersionResources = async ( + versionId: string, + ): Promise<TypesGen.WorkspaceResource[]> => { + const response = await this.axios.get<TypesGen.WorkspaceResource[]>( + `/api/v2/templateversions/${versionId}/resources`, + ); + + return response.data; + }; + + getTemplateVersionVariables = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionVariable[]> => { + // Defined as separate variable to avoid wonky Prettier formatting because + // the type definition is so long + type VerArray = TypesGen.TemplateVersionVariable[]; + + const response = await this.axios.get<VerArray>( + `/api/v2/templateversions/${versionId}/variables`, + ); + + return response.data; + }; + + getTemplateVersions = async ( + templateId: string, + ): Promise<TypesGen.TemplateVersion[]> => { + const response = await this.axios.get<TypesGen.TemplateVersion[]>( + `/api/v2/templates/${templateId}/versions`, + ); + return response.data; + }; + + getTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, + ); + + return response.data; + }; + + getPreviousTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ) => { + try { + const response = await this.axios.get<TypesGen.TemplateVersion>( + `/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 + const is404 = + isAxiosError(error) && error.response && error.response.status === 404; + + if (is404) { + return undefined; + } + + throw error; + } + }; + + createTemplateVersion = async ( + organizationId: string, + data: TypesGen.CreateTemplateVersionRequest, + ): Promise<TypesGen.TemplateVersion> => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/organizations/${organizationId}/templateversions`, + data, + ); + + return response.data; + }; + + getTemplateVersionExternalAuth = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionExternalAuth[]> => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/external-auth`, + ); + + return response.data; + }; + + getTemplateVersionRichParameters = async ( + versionId: string, + ): Promise<TypesGen.TemplateVersionParameter[]> => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ); + return response.data; + }; + + createTemplate = async ( + organizationId: string, + data: TypesGen.CreateTemplateRequest, + ): Promise<TypesGen.Template> => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templates`, + data, + ); + + return response.data; + }; + + updateActiveTemplateVersion = async ( + templateId: string, + data: TypesGen.UpdateActiveTemplateVersion, + ) => { + const response = await this.axios.patch<TypesGen.Response>( + `/api/v2/templates/${templateId}/versions`, + data, + ); + return response.data; + }; + + patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, + ) => { + const response = await this.axios.patch<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}`, + data, + ); + + return response.data; + }; + + archiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}/archive`, + ); + + return response.data; + }; + + unarchiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post<TypesGen.TemplateVersion>( + `/api/v2/templateversions/${templateVersionId}/unarchive`, + ); + return response.data; + }; + + updateTemplateMeta = async ( + templateId: string, + data: TypesGen.UpdateTemplateMeta, + ): Promise<TypesGen.Template | null> => { + const response = await this.axios.patch<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + data, + ); + + // On 304 response there is no data payload. + if (response.status === 304) { + return null; + } + + return response.data; + }; + + deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => { + const response = await this.axios.delete<TypesGen.Template>( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.get<TypesGen.Workspace>( + `/api/v2/workspaces/${workspaceId}`, + { params }, + ); + + return response.data; + }; + + getWorkspaces = async ( + options: TypesGen.WorkspacesRequest, + ): Promise<TypesGen.WorkspacesResponse> => { + const url = getURLWithSearchParams("/api/v2/workspaces", options); + const response = await this.axios.get<TypesGen.WorkspacesResponse>(url); + return response.data; + }; + + getWorkspaceByOwnerAndName = async ( + username = "me", + workspaceName: string, + params?: TypesGen.WorkspaceOptions, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.get<TypesGen.Workspace>( + `/api/v2/users/${username}/workspace/${workspaceName}`, + { params }, + ); + + return response.data; + }; + + getWorkspaceBuildByNumber = async ( + username = "me", + workspaceName: string, + buildNumber: number, + ): Promise<TypesGen.WorkspaceBuild> => { + const response = await this.axios.get<TypesGen.WorkspaceBuild>( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ); + + return response.data; + }; + + waitForBuild = (build: TypesGen.WorkspaceBuild) => { + return new Promise<TypesGen.ProvisionerJob | undefined>((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); + } + + return res(latestJobInfo); + })(); + }); + }; + + postWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.CreateWorkspaceBuildRequest, + ): Promise<TypesGen.WorkspaceBuild> => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ); + + 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, + }); + }; + + stopWorkspace = ( + workspaceId: string, + logLevel?: TypesGen.ProvisionerLogLevel, + ) => { + return this.postWorkspaceBuild(workspaceId, { + transition: "stop", + log_level: logLevel, + }); + }; + + deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { + return postWorkspaceBuild(workspaceId, { + transition: "delete", + ...options, + }); + }; + + cancelWorkspaceBuild = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], + ): Promise<TypesGen.Response> => { + const response = await this.axios.patch( + `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + ); + + return response.data; + }; + + updateWorkspaceDormancy = async ( + workspaceId: string, + dormant: boolean, + ): Promise<TypesGen.Workspace> => { + const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/dormant`, + data, + ); + + return response.data; + }; + + updateWorkspaceAutomaticUpdates = async ( + workspaceId: string, + automaticUpdates: TypesGen.AutomaticUpdates, + ): Promise<void> => { + const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { + automatic_updates: automaticUpdates, + }; + + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/autoupdates`, + req, + ); + + return response.data; + }; + + restartWorkspace = async ({ + workspace, + buildParameters, + }: RestartWorkspaceParameters): Promise<void> => { + const stopBuild = await this.stopWorkspace(workspace.id); + const awaitedStopBuild = await this.waitForBuild(stopBuild); + + // If the restart is canceled halfway through, make sure we bail + if (awaitedStopBuild?.status === "canceled") { + return; + } + + const startBuild = await this.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + undefined, + buildParameters, + ); + + await waitForBuild(startBuild); + }; + + cancelTemplateVersionBuild = async ( + templateVersionId: TypesGen.TemplateVersion["id"], + ): Promise<TypesGen.Response> => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ); + + return response.data; + }; + + createUser = async ( + user: TypesGen.CreateUserRequest, + ): Promise<TypesGen.User> => { + const response = await this.axios.post<TypesGen.User>( + "/api/v2/users", + user, + ); + + return response.data; + }; + + createWorkspace = async ( + organizationId: string, + userId = "me", + workspace: TypesGen.CreateWorkspaceRequest, + ): Promise<TypesGen.Workspace> => { + const response = await this.axios.post<TypesGen.Workspace>( + `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + workspace, + ); + + return response.data; + }; + + patchWorkspace = async ( + workspaceId: string, + data: TypesGen.UpdateWorkspaceRequest, + ): Promise<void> => { + await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + }; + + getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => { + const response = await this.axios.get("/api/v2/buildinfo"); + return response.data; + }; + + getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => { + const response = await this.axios.get("/api/v2/updatecheck"); + return response.data; + }; + + putWorkspaceAutostart = async ( + workspaceID: string, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, + ): Promise<void> => { + const payload = JSON.stringify(autostart); + await this.axios.put( + `/api/v2/workspaces/${workspaceID}/autostart`, + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + }; + + putWorkspaceAutostop = async ( + workspaceID: string, + ttl: TypesGen.UpdateWorkspaceTTLRequest, + ): Promise<void> => { + const payload = JSON.stringify(ttl); + await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { + headers: { ...BASE_CONTENT_TYPE_JSON }, + }); + }; + + updateProfile = async ( + userId: string, + data: TypesGen.UpdateUserProfileRequest, + ): Promise<TypesGen.User> => { + const response = await this.axios.put( + `/api/v2/users/${userId}/profile`, + data, + ); + return response.data; + }; + + updateAppearanceSettings = async ( + userId: string, + data: TypesGen.UpdateUserAppearanceSettingsRequest, + ): Promise<TypesGen.User> => { + const response = await this.axios.put( + `/api/v2/users/${userId}/appearance`, + data, + ); + return response.data; + }; + + getUserQuietHoursSchedule = async ( + userId: TypesGen.User["id"], + ): Promise<TypesGen.UserQuietHoursScheduleResponse> => { + const response = await this.axios.get( + `/api/v2/users/${userId}/quiet-hours`, + ); + return response.data; + }; + + updateUserQuietHoursSchedule = async ( + userId: TypesGen.User["id"], + data: TypesGen.UpdateUserQuietHoursScheduleRequest, + ): Promise<TypesGen.UserQuietHoursScheduleResponse> => { + const response = await this.axios.put( + `/api/v2/users/${userId}/quiet-hours`, + data, + ); + + return response.data; + }; + + activateUser = async ( + userId: TypesGen.User["id"], + ): Promise<TypesGen.User> => { + const response = await this.axios.put<TypesGen.User>( + `/api/v2/users/${userId}/status/activate`, + ); + return response.data; + }; + + suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => { + const response = await this.axios.put<TypesGen.User>( + `/api/v2/users/${userId}/status/suspend`, + ); + + return response.data; + }; + + deleteUser = async (userId: TypesGen.User["id"]): Promise<void> => { + await this.axios.delete(`/api/v2/users/${userId}`); + }; + + // API definition: + // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 + hasFirstUser = async (): Promise<boolean> => { + 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; + } + + throw error; + } + }; + + createFirstUser = async ( + req: TypesGen.CreateFirstUserRequest, + ): Promise<TypesGen.CreateFirstUserResponse> => { + const response = await this.axios.post(`/api/v2/users/first`, req); + return response.data; + }; + + updateUserPassword = async ( + userId: TypesGen.User["id"], + updatePassword: TypesGen.UpdateUserPasswordRequest, + ): Promise<undefined> => { + await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); + }; + + getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => { + const response = + await this.axios.get<TypesGen.AssignableRoles[]>(`/api/v2/users/roles`); + return response.data; }; - getUserParameters = async (templateID: string) => { - const response = await this.axios.get<TypesGen.UserParameter[]>( - `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + updateUserRoles = async ( + roles: TypesGen.Role["name"][], + userId: TypesGen.User["id"], + ): Promise<TypesGen.User> => { + const response = await this.axios.put<TypesGen.User>( + `/api/v2/users/${userId}/roles`, + { roles }, ); return response.data; }; - getAuthMethods = async (): Promise<TypesGen.AuthMethods> => { - const response = await this.axios.get<TypesGen.AuthMethods>( - "/api/v2/users/authmethods", + getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => { + const response = await this.axios.get<TypesGen.GitSSHKey>( + `/api/v2/users/${userId}/gitsshkey`, ); return response.data; }; - getUserLoginType = async (): Promise<TypesGen.UserLoginType> => { - const response = await this.axios.get<TypesGen.UserLoginType>( - "/api/v2/users/me/login-type", + regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => { + const response = await this.axios.put<TypesGen.GitSSHKey>( + `/api/v2/users/${userId}/gitsshkey`, ); return response.data; }; - checkAuthorization = async ( - params: TypesGen.AuthorizationRequest, - ): Promise<TypesGen.AuthorizationResponse> => { - const response = await this.axios.post<TypesGen.AuthorizationResponse>( - `/api/v2/authcheck`, - params, + getWorkspaceBuilds = async ( + workspaceId: string, + req?: TypesGen.WorkspaceBuildsRequest, + ) => { + const response = await this.axios.get<TypesGen.WorkspaceBuild[]>( + getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), ); return response.data; }; - getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => { - const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>( - "/api/v2/users/me/keys", + getWorkspaceBuildLogs = async ( + buildId: string, + before: Date, + ): Promise<TypesGen.ProvisionerJobLog[]> => { + const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>( + `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, ); return response.data; }; - getTokens = async ( - params: TypesGen.TokensFilter, - ): Promise<TypesGen.APIKeyWithOwner[]> => { - const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>( - `/api/v2/users/me/keys/tokens`, - { params }, + getWorkspaceAgentLogs = async ( + agentID: string, + ): Promise<TypesGen.WorkspaceAgentLog[]> => { + const response = await this.axios.get<TypesGen.WorkspaceAgentLog[]>( + `/api/v2/workspaceagents/${agentID}/logs`, ); return response.data; }; - deleteToken = async (keyId: string): Promise<void> => { - await this.axios.delete("/api/v2/users/me/keys/" + keyId); + putWorkspaceExtension = async ( + workspaceId: string, + newDeadline: dayjs.Dayjs, + ): Promise<void> => { + await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { + deadline: newDeadline, + }); }; - createToken = async ( - params: TypesGen.CreateTokenRequest, - ): Promise<TypesGen.GenerateAPIKeyResponse> => { - const response = await this.axios.post( - `/api/v2/users/me/keys/tokens`, - params, - ); + refreshEntitlements = async (): Promise<void> => { + await this.axios.post("/api/v2/licenses/refresh-entitlements"); + }; - return response.data; + getEntitlements = async (): Promise<TypesGen.Entitlements> => { + try { + const response = await this.axios.get<TypesGen.Entitlements>( + "/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; + } }; - getTokenConfig = async (): Promise<TypesGen.TokenConfig> => { - const response = await this.axios.get( - "/api/v2/users/me/keys/tokens/tokenconfig", - ); + getExperiments = async (): Promise<TypesGen.Experiment[]> => { + try { + const response = await this.axios.get<TypesGen.Experiment[]>( + "/api/v2/experiments", + ); - return response.data; + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + + throw error; + } }; - getUsers = async ( - options: TypesGen.UsersRequest, - signal?: AbortSignal, - ): Promise<TypesGen.GetUsersResponse> => { - const url = getURLWithSearchParams("/api/v2/users", options); - const response = await this.axios.get<TypesGen.GetUsersResponse>( - url.toString(), - { signal }, - ); + getAvailableExperiments = + async (): Promise<TypesGen.AvailableExperiments> => { + try { + const response = await this.axios.get("/api/v2/experiments/available"); - return response.data; + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return { safe: [] }; + } + throw error; + } + }; + + getExternalAuthProvider = async ( + provider: string, + ): Promise<TypesGen.ExternalAuth> => { + const res = await this.axios.get(`/api/v2/external-auth/${provider}`); + return res.data; }; - getOrganization = async ( - organizationId: string, - ): Promise<TypesGen.Organization> => { - const response = await this.axios.get<TypesGen.Organization>( - `/api/v2/organizations/${organizationId}`, + getExternalAuthDevice = async ( + provider: string, + ): Promise<TypesGen.ExternalAuthDevice> => { + const resp = await this.axios.get( + `/api/v2/external-auth/${provider}/device`, ); + return resp.data; + }; - return response.data; + exchangeExternalAuthDevice = async ( + provider: string, + req: TypesGen.ExternalAuthDeviceExchange, + ): Promise<void> => { + const resp = await this.axios.post( + `/api/v2/external-auth/${provider}/device`, + req, + ); + + return resp.data; }; - getOrganizations = async (): Promise<TypesGen.Organization[]> => { - const response = await this.axios.get<TypesGen.Organization[]>( - "/api/v2/users/me/organizations", + getUserExternalAuthProviders = + async (): Promise<TypesGen.ListUserExternalAuthResponse> => { + const resp = await this.axios.get(`/api/v2/external-auth`); + return resp.data; + }; + + unlinkExternalAuthProvider = async (provider: string): Promise<string> => { + const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`); + return resp.data; + }; + + getOAuth2ProviderApps = async ( + filter?: TypesGen.OAuth2ProviderAppFilter, + ): Promise<TypesGen.OAuth2ProviderApp[]> => { + 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<TypesGen.OAuth2ProviderApp> => { + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`); + return resp.data; + }; + + postOAuth2ProviderApp = async ( + data: TypesGen.PostOAuth2ProviderAppRequest, + ): Promise<TypesGen.OAuth2ProviderApp> => { + const response = await this.axios.post( + `/api/v2/oauth2-provider/apps`, + data, ); return response.data; }; - getTemplate = async (templateId: string): Promise<TypesGen.Template> => { - const response = await this.axios.get<TypesGen.Template>( - `/api/v2/templates/${templateId}`, + putOAuth2ProviderApp = async ( + id: string, + data: TypesGen.PutOAuth2ProviderAppRequest, + ): Promise<TypesGen.OAuth2ProviderApp> => { + const response = await this.axios.put( + `/api/v2/oauth2-provider/apps/${id}`, + data, ); - return response.data; }; - getTemplates = async ( - organizationId: string, - options?: TemplateOptions, - ): Promise<TypesGen.Template[]> => { - const params: Record<string, string> = {}; - 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); - } + deleteOAuth2ProviderApp = async (id: string): Promise<void> => { + await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`); + }; - const response = await this.axios.get<TypesGen.Template[]>( - `/api/v2/organizations/${organizationId}/templates`, - { params }, + getOAuth2ProviderAppSecrets = async ( + id: string, + ): Promise<TypesGen.OAuth2ProviderAppSecret[]> => { + const resp = await this.axios.get( + `/api/v2/oauth2-provider/apps/${id}/secrets`, ); + return resp.data; + }; - return response.data; + postOAuth2ProviderAppSecret = async ( + id: string, + ): Promise<TypesGen.OAuth2ProviderAppSecretFull> => { + const resp = await this.axios.post( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; }; - getTemplateByName = async ( - organizationId: string, - name: string, - ): Promise<TypesGen.Template> => { - const response = await this.axios.get<TypesGen.Template>( - `/api/v2/organizations/${organizationId}/templates/${name}`, + deleteOAuth2ProviderAppSecret = async ( + appId: string, + secretId: string, + ): Promise<void> => { + await this.axios.delete( + `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, ); + }; + revokeOAuth2ProviderApp = async (appId: string): Promise<void> => { + await this.axios.delete(`/oauth2/tokens?client_id=${appId}`); + }; + + getAuditLogs = async ( + options: TypesGen.AuditLogsRequest, + ): Promise<TypesGen.AuditLogResponse> => { + const url = getURLWithSearchParams("/api/v2/audit", options); + const response = await this.axios.get(url); return response.data; }; - getTemplateVersion = async ( - versionId: string, - ): Promise<TypesGen.TemplateVersion> => { - const response = await this.axios.get<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${versionId}`, + getTemplateDAUs = async ( + templateId: string, + ): Promise<TypesGen.DAUsResponse> => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/daus`, ); return response.data; }; - getTemplateVersionResources = async ( - versionId: string, - ): Promise<TypesGen.WorkspaceResource[]> => { - const response = await this.axios.get<TypesGen.WorkspaceResource[]>( - `/api/v2/templateversions/${versionId}/resources`, + 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<TypesGen.DAUsResponse> => { + const response = await this.axios.get( + `/api/v2/insights/daus?tz_offset=${offset}`, ); return response.data; }; - getTemplateVersionVariables = async ( - versionId: string, - ): Promise<TypesGen.TemplateVersionVariable[]> => { - // Defined as separate variable to avoid wonky Prettier formatting because - // the type definition is so long - type VerArray = TypesGen.TemplateVersionVariable[]; - - const response = await axiosInstance.get<VerArray>( - `/api/v2/templateversions/${versionId}/variables`, - ); + getTemplateACLAvailable = async ( + templateId: string, + options: TypesGen.UsersRequest, + ): Promise<TypesGen.ACLAvailable> => { + const url = getURLWithSearchParams( + `/api/v2/templates/${templateId}/acl/available`, + options, + ).toString(); + const response = await this.axios.get(url); return response.data; }; - getTemplateVersions = async ( + getTemplateACL = async ( templateId: string, - ): Promise<TypesGen.TemplateVersion[]> => { - const response = await this.axios.get<TypesGen.TemplateVersion[]>( - `/api/v2/templates/${templateId}/versions`, + ): Promise<TypesGen.TemplateACL> => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/acl`, ); + return response.data; }; - getTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, - ): Promise<TypesGen.TemplateVersion> => { - const response = await this.axios.get<TypesGen.TemplateVersion>( - `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, + 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; }; - getPreviousTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, - ) => { - try { - const response = await this.axios.get<TypesGen.TemplateVersion>( - `/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 - const is404 = - isAxiosError(error) && error.response && error.response.status === 404; + getApplicationsHost = async (): Promise<TypesGen.AppHostResponse> => { + const response = await this.axios.get(`/api/v2/applications/host`); + return response.data; + }; - if (is404) { - return undefined; - } + getGroups = async (organizationId: string): Promise<TypesGen.Group[]> => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/groups`, + ); - throw error; - } + return response.data; }; - createTemplateVersion = async ( + createGroup = async ( organizationId: string, - data: TypesGen.CreateTemplateVersionRequest, - ): Promise<TypesGen.TemplateVersion> => { - const response = await this.axios.post<TypesGen.TemplateVersion>( - `/api/v2/organizations/${organizationId}/templateversions`, + data: TypesGen.CreateGroupRequest, + ): Promise<TypesGen.Group> => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/groups`, data, ); + return response.data; + }; + getGroup = async (groupId: string): Promise<TypesGen.Group> => { + const response = await this.axios.get(`/api/v2/groups/${groupId}`); return response.data; }; - getTemplateVersionExternalAuth = async ( - versionId: string, - ): Promise<TypesGen.TemplateVersionExternalAuth[]> => { + patchGroup = async ( + groupId: string, + data: TypesGen.PatchGroupRequest, + ): Promise<TypesGen.Group> => { + const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data); + return response.data; + }; + + addMember = async (groupId: string, userId: string) => { + return patchGroup(groupId, { + name: "", + add_users: [userId], + remove_users: [], + }); + }; + + removeMember = async (groupId: string, userId: string) => { + return patchGroup(groupId, { + name: "", + display_name: "", + add_users: [], + remove_users: [userId], + }); + }; + + deleteGroup = async (groupId: string): Promise<void> => { + await this.axios.delete(`/api/v2/groups/${groupId}`); + }; + + getWorkspaceQuota = async ( + username: string, + ): Promise<TypesGen.WorkspaceQuota> => { const response = await this.axios.get( - `/api/v2/templateversions/${versionId}/external-auth`, + `/api/v2/workspace-quota/${encodeURIComponent(username)}`, ); + return response.data; + }; + getAgentListeningPorts = async ( + agentID: string, + ): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/listening-ports`, + ); return response.data; }; - getTemplateVersionRichParameters = async ( - versionId: string, - ): Promise<TypesGen.TemplateVersionParameter[]> => { + getWorkspaceAgentSharedPorts = async ( + workspaceID: string, + ): Promise<TypesGen.WorkspaceAgentPortShares> => { const response = await this.axios.get( - `/api/v2/templateversions/${versionId}/rich-parameters`, + `/api/v2/workspaces/${workspaceID}/port-share`, ); return response.data; }; - createTemplate = async ( - organizationId: string, - data: TypesGen.CreateTemplateRequest, - ): Promise<TypesGen.Template> => { + upsertWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.UpsertWorkspaceAgentPortShareRequest, + ): Promise<TypesGen.WorkspaceAgentPortShares> => { const response = await this.axios.post( - `/api/v2/organizations/${organizationId}/templates`, - data, + `/api/v2/workspaces/${workspaceID}/port-share`, + req, + ); + return response.data; + }; + + deleteWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.DeleteWorkspaceAgentPortShareRequest, + ): Promise<TypesGen.WorkspaceAgentPortShares> => { + const response = await this.axios.delete( + `/api/v2/workspaces/${workspaceID}/port-share`, + { data: req }, ); return response.data; }; - updateActiveTemplateVersion = async ( - templateId: string, - data: TypesGen.UpdateActiveTemplateVersion, - ) => { - const response = await this.axios.patch<TypesGen.Response>( - `/api/v2/templates/${templateId}/versions`, - data, - ); + // getDeploymentSSHConfig is used by the VSCode-Extension. + getDeploymentSSHConfig = async (): Promise<TypesGen.SSHConfigResponse> => { + const response = await this.axios.get(`/api/v2/deployment/ssh`); return response.data; }; - patchTemplateVersion = async ( - templateVersionId: string, - data: TypesGen.PatchTemplateVersionRequest, - ) => { - const response = await this.axios.patch<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}`, - data, - ); + getDeploymentConfig = async (): Promise<DeploymentConfig> => { + const response = await this.axios.get(`/api/v2/deployment/config`); + return response.data; + }; + getDeploymentStats = async (): Promise<TypesGen.DeploymentStats> => { + const response = await this.axios.get(`/api/v2/deployment/stats`); return response.data; }; - archiveTemplateVersion = async (templateVersionId: string) => { - const response = await this.axios.post<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}/archive`, + getReplicas = async (): Promise<TypesGen.Replica[]> => { + const response = await this.axios.get(`/api/v2/replicas`); + return response.data; + }; + + getFile = async (fileId: string): Promise<ArrayBuffer> => { + const response = await this.axios.get<ArrayBuffer>( + `/api/v2/files/${fileId}`, + { responseType: "arraybuffer" }, ); return response.data; }; - unarchiveTemplateVersion = async (templateVersionId: string) => { - const response = await this.axios.post<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}/unarchive`, - ); + getWorkspaceProxyRegions = async (): Promise< + TypesGen.RegionsResponse<TypesGen.Region> + > => { + const response = + await this.axios.get<TypesGen.RegionsResponse<TypesGen.Region>>( + `/api/v2/regions`, + ); + return response.data; }; - updateTemplateMeta = async ( - templateId: string, - data: TypesGen.UpdateTemplateMeta, - ): Promise<TypesGen.Template | null> => { - const response = await this.axios.patch<TypesGen.Template>( - `/api/v2/templates/${templateId}`, - data, - ); + getWorkspaceProxies = async (): Promise< + TypesGen.RegionsResponse<TypesGen.WorkspaceProxy> + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse<TypesGen.WorkspaceProxy> + >(`/api/v2/workspaceproxies`); - // On 304 response there is no data payload. - if (response.status === 304) { - return null; - } + return response.data; + }; + createWorkspaceProxy = async ( + b: TypesGen.CreateWorkspaceProxyRequest, + ): Promise<TypesGen.UpdateWorkspaceProxyResponse> => { + const response = await this.axios.post(`/api/v2/workspaceproxies`, b); return response.data; }; - deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => { - const response = await this.axios.delete<TypesGen.Template>( - `/api/v2/templates/${templateId}`, - ); + getAppearance = async (): Promise<TypesGen.AppearanceConfig> => { + 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: "", + service_banner: { + enabled: false, + }, + }; + } + + throw ex; + } + }; + updateAppearance = async ( + b: TypesGen.AppearanceConfig, + ): Promise<TypesGen.AppearanceConfig> => { + const response = await this.axios.put(`/api/v2/appearance`, b); return response.data; }; - getWorkspace = async ( - workspaceId: string, - params?: TypesGen.WorkspaceOptions, - ): Promise<TypesGen.Workspace> => { - const response = await this.axios.get<TypesGen.Workspace>( - `/api/v2/workspaces/${workspaceId}`, - { params }, + getTemplateExamples = async ( + organizationId: string, + ): Promise<TypesGen.TemplateExample[]> => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/examples`, ); return response.data; }; - getWorkspaces = async ( - options: TypesGen.WorkspacesRequest, - ): Promise<TypesGen.WorkspacesResponse> => { - const url = getURLWithSearchParams("/api/v2/workspaces", options); - const response = await this.axios.get<TypesGen.WorkspacesResponse>(url); + uploadFile = async (file: File): Promise<TypesGen.UploadResponse> => { + const response = await this.axios.post("/api/v2/files", file, { + headers: { "Content-Type": "application/x-tar" }, + }); + return response.data; }; - getWorkspaceByOwnerAndName = async ( - username = "me", - workspaceName: string, - params?: TypesGen.WorkspaceOptions, - ): Promise<TypesGen.Workspace> => { - const response = await this.axios.get<TypesGen.Workspace>( - `/api/v2/users/${username}/workspace/${workspaceName}`, - { params }, + getTemplateVersionLogs = async ( + versionId: string, + ): Promise<TypesGen.ProvisionerJobLog[]> => { + const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>( + `/api/v2/templateversions/${versionId}/logs`, ); - return response.data; }; - getWorkspaceBuildByNumber = async ( - username = "me", - workspaceName: string, - buildNumber: number, + updateWorkspaceVersion = async ( + workspace: TypesGen.Workspace, ): Promise<TypesGen.WorkspaceBuild> => { - const response = await this.axios.get<TypesGen.WorkspaceBuild>( - `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + const template = await getTemplate(workspace.template_id); + return startWorkspace(workspace.id, template.active_version_id); + }; + + getWorkspaceBuildParameters = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], + ): Promise<TypesGen.WorkspaceBuildParameter[]> => { + const response = await this.axios.get<TypesGen.WorkspaceBuildParameter[]>( + `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, ); return response.data; }; - waitForBuild = (build: TypesGen.WorkspaceBuild) => { - return new Promise<TypesGen.ProvisionerJob | undefined>((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); - } + getLicenses = async (): Promise<GetLicensesResponse[]> => { + const response = await this.axios.get(`/api/v2/licenses`); + return response.data; + }; - await delay(1000); - } + createLicense = async ( + data: TypesGen.AddLicenseRequest, + ): Promise<TypesGen.AddLicenseRequest> => { + const response = await this.axios.post(`/api/v2/licenses`, data); + return response.data; + }; - return res(latestJobInfo); - })(); - }); + removeLicense = async (licenseId: number): Promise<void> => { + await this.axios.delete(`/api/v2/licenses/${licenseId}`); }; - postWorkspaceBuild = async ( - workspaceId: string, - data: TypesGen.CreateWorkspaceBuildRequest, + /** 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<TypesGen.WorkspaceBuild> => { - const response = await this.axios.post( - `/api/v2/workspaces/${workspaceId}/builds`, - data, + const [currentBuildParameters, templateParameters] = await Promise.all([ + this.getWorkspaceBuildParameters(workspace.latest_build.id), + this.getTemplateVersionRichParameters(templateVersionId), + ]); + + const missingParameters = getMissingParameters( + currentBuildParameters, + newBuildParameters, + templateParameters, ); - return response.data; - }; + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, templateVersionId); + } - startWorkspace = ( - workspaceId: string, - templateVersionId: string, - logLevel?: TypesGen.ProvisionerLogLevel, - buildParameters?: TypesGen.WorkspaceBuildParameter[], - ) => { - return this.postWorkspaceBuild(workspaceId, { + return this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: templateVersionId, - log_level: logLevel, - rich_parameter_values: buildParameters, + rich_parameter_values: newBuildParameters, }); }; - stopWorkspace = ( - workspaceId: string, - logLevel?: TypesGen.ProvisionerLogLevel, - ) => { - return this.postWorkspaceBuild(workspaceId, { - transition: "stop", - log_level: logLevel, - }); - }; + /** 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<TypesGen.WorkspaceBuild> => { + 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 getTemplateVersionRichParameters(activeVersionId); + + const missingParameters = getMissingParameters( + oldBuildParameters, + newBuildParameters, + templateParameters, + ); - deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { - return postWorkspaceBuild(workspaceId, { - transition: "delete", - ...options, + if (missingParameters.length > 0) { + throw new MissingBuildParameters(missingParameters, activeVersionId); + } + + return postWorkspaceBuild(workspace.id, { + transition: "start", + template_version_id: activeVersionId, + rich_parameter_values: newBuildParameters, }); }; - cancelWorkspaceBuild = async ( - workspaceBuildId: TypesGen.WorkspaceBuild["id"], - ): Promise<TypesGen.Response> => { - const response = await this.axios.patch( - `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + getWorkspaceResolveAutostart = async ( + workspaceId: string, + ): Promise<TypesGen.ResolveAutostartResponse> => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}/resolve-autostart`, ); - return response.data; }; - updateWorkspaceDormancy = async ( - workspaceId: string, - dormant: boolean, - ): Promise<TypesGen.Workspace> => { - const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; - const response = await this.axios.put( - `/api/v2/workspaces/${workspaceId}/dormant`, - data, + issueReconnectingPTYSignedToken = async ( + params: TypesGen.IssueReconnectingPTYSignedTokenRequest, + ): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => { + const response = await this.axios.post( + "/api/v2/applications/reconnecting-pty-signed-token", + params, ); return response.data; }; - updateWorkspaceAutomaticUpdates = async ( - workspaceId: string, - automaticUpdates: TypesGen.AutomaticUpdates, - ): Promise<void> => { - const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { - automatic_updates: automaticUpdates, + 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), + ]); + + return { + templateVersionRichParameters, + buildParameters, }; + }; - const response = await this.axios.put( - `/api/v2/workspaces/${workspaceId}/autoupdates`, - req, + getInsightsUserLatency = async ( + filters: InsightsParams, + ): Promise<TypesGen.UserLatencyInsightsResponse> => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-latency?${params}`, ); return response.data; }; - restartWorkspace = async ({ - workspace, - buildParameters, - }: RestartWorkspaceParameters): Promise<void> => { - const stopBuild = await this.stopWorkspace(workspace.id); - const awaitedStopBuild = await this.waitForBuild(stopBuild); + getInsightsUserActivity = async ( + filters: InsightsParams, + ): Promise<TypesGen.UserActivityInsightsResponse> => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-activity?${params}`, + ); - // If the restart is canceled halfway through, make sure we bail - if (awaitedStopBuild?.status === "canceled") { - return; - } + return response.data; + }; - const startBuild = await this.startWorkspace( - workspace.id, - workspace.latest_build.template_version_id, - undefined, - buildParameters, + getInsightsTemplate = async ( + params: InsightsTemplateParams, + ): Promise<TypesGen.TemplateInsightsResponse> => { + const searchParams = new URLSearchParams(params); + const response = await this.axios.get( + `/api/v2/insights/templates?${searchParams}`, ); - await waitForBuild(startBuild); + return response.data; }; - cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"], - ): Promise<TypesGen.Response> => { - const response = await axiosInstance.patch( - `/api/v2/templateversions/${templateVersionId}/cancel`, + getHealth = async (force: boolean = false) => { + const params = new URLSearchParams({ force: force.toString() }); + const response = await this.axios.get<TypesGen.HealthcheckReport>( + `/api/v2/debug/health?${params}`, ); - return response.data; }; - createUser = async ( - user: TypesGen.CreateUserRequest, - ): Promise<TypesGen.User> => { - const response = await this.axios.post<TypesGen.User>( - "/api/v2/users", - user, + getHealthSettings = async (): Promise<TypesGen.HealthSettings> => { + const res = await this.axios.get<TypesGen.HealthSettings>( + `/api/v2/debug/health/settings`, ); - return response.data; + return res.data; }; - createWorkspace = async ( - organizationId: string, - userId = "me", - workspace: TypesGen.CreateWorkspaceRequest, - ): Promise<TypesGen.Workspace> => { - const response = await this.axios.post<TypesGen.Workspace>( - `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, - workspace, + updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => { + const response = await this.axios.put<TypesGen.HealthSettings>( + `/api/v2/debug/health/settings`, + data, ); return response.data; }; - patchWorkspace = async ( - workspaceId: string, - data: TypesGen.UpdateWorkspaceRequest, - ): Promise<void> => { - await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + putFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); }; - getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => { - const response = await this.axios.get("/api/v2/buildinfo"); - return response.data; + deleteFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); }; - getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => { - const response = await axiosInstance.get("/api/v2/updatecheck"); - return response.data; - }; + getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { + const searchParams = new URLSearchParams({ + workspace_id: options.workspaceId, + agent_id: options.agentId, + }); - putWorkspaceAutostart = async ( - workspaceID: string, - autostart: TypesGen.UpdateWorkspaceAutostartRequest, - ): Promise<void> => { - const payload = JSON.stringify(autostart); - await this.axios.put( - `/api/v2/workspaces/${workspaceID}/autostart`, - payload, - { - headers: { ...BASE_CONTENT_TYPE_JSON }, - }, - ); + try { + const res = await this.axios.get<TypesGen.JFrogXrayScan>( + `/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; + } }; } @@ -1977,11 +3106,6 @@ export const getDeploymentSSHConfig = return response.data; }; -export type DeploymentConfig = { - readonly config: TypesGen.DeploymentValues; - readonly options: TypesGen.SerpentOption[]; -}; - export const getDeploymentConfig = async (): Promise<DeploymentConfig> => { const response = await axiosInstance.get(`/api/v2/deployment/config`); return response.data; @@ -2103,21 +3227,6 @@ export const getWorkspaceBuildParameters = async ( ); return response.data; }; -type Claims = { - license_expires: number; - account_type?: string; - account_id?: string; - trial: boolean; - all_features: boolean; - version: number; - features: Record<string, number>; - require_telemetry?: boolean; -}; - -export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & { - claims: Claims; - expires_at: string; -}; export const getLicenses = async (): Promise<GetLicensesResponse[]> => { const response = await axiosInstance.get(`/api/v2/licenses`); @@ -2135,20 +3244,6 @@ export const removeLicense = async (licenseId: number): Promise<void> => { await axiosInstance.delete(`/api/v2/licenses/${licenseId}`); }; -export class MissingBuildParameters extends Error { - parameters: TypesGen.TemplateVersionParameter[] = []; - versionId: string; - - constructor( - parameters: TypesGen.TemplateVersionParameter[], - versionId: string, - ) { - super("Missing build parameters."); - this.parameters = parameters; - this.versionId = versionId; - } -} - /** Steps to change the workspace version * - Get the latest template to access the latest active version * - Get the current build parameters @@ -2230,207 +3325,6 @@ export const getWorkspaceResolveAutostart = async ( return response.data; }; -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 }, - ); -}; - -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; -}; - -type WatchWorkspaceAgentLogsOptions = { - after: number; - onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; - onDone?: () => void; - onError: (error: Error) => void; -}; - -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" - : ""; - - 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(); - }); - - return socket; -}; - -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; -}; - export const issueReconnectingPTYSignedToken = async ( params: TypesGen.IssueReconnectingPTYSignedTokenRequest, ): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => { @@ -2453,12 +3347,6 @@ export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { }; }; -export type InsightsParams = { - start_time: string; - end_time: string; - template_ids: string; -}; - export const getInsightsUserLatency = async ( filters: InsightsParams, ): Promise<TypesGen.UserLatencyInsightsResponse> => { @@ -2479,10 +3367,6 @@ export const getInsightsUserActivity = async ( return response.data; }; -export type InsightsTemplateParams = InsightsParams & { - interval: "day" | "week"; -}; - export const getInsightsTemplate = async ( params: InsightsTemplateParams, ): Promise<TypesGen.TemplateInsightsResponse> => { @@ -2527,11 +3411,6 @@ export const deleteFavoriteWorkspace = async (workspaceID: string) => { await axiosInstance.delete(`/api/v2/workspaces/${workspaceID}/favorite`); }; -export type GetJFrogXRayScanParams = { - workspaceId: string; - agentId: string; -}; - export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { const searchParams = new URLSearchParams({ workspace_id: options.workspaceId, From f60627eb3f65adb3f889de1547eca5ff238bc798 Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Wed, 1 May 2024 21:45:12 +0000 Subject: [PATCH 4/7] chore: update all import paths to go through client instance --- site/e2e/api.ts | 27 +- site/e2e/global.setup.ts | 4 +- site/e2e/helpers.ts | 3 +- site/e2e/reporter.ts | 3 +- site/e2e/tests/deployment/general.spec.ts | 4 +- site/e2e/tests/deployment/network.spec.ts | 4 +- .../tests/deployment/observability.spec.ts | 4 +- site/e2e/tests/deployment/security.spec.ts | 4 +- site/e2e/tests/deployment/userAuth.spec.ts | 4 +- .../tests/deployment/workspaceProxies.spec.ts | 4 +- site/e2e/tests/groups/removeMember.spec.ts | 4 +- .../templates/updateTemplateSchedule.spec.ts | 8 +- site/src/api/api.test.ts | 79 +- site/src/api/api.ts | 1520 +---------------- site/src/api/queries/appearance.ts | 6 +- site/src/api/queries/audits.ts | 4 +- site/src/api/queries/authCheck.ts | 4 +- site/src/api/queries/buildInfo.ts | 4 +- site/src/api/queries/debug.ts | 10 +- site/src/api/queries/deployment.ts | 10 +- site/src/api/queries/entitlements.ts | 6 +- site/src/api/queries/experiments.ts | 6 +- site/src/api/queries/externalAuth.ts | 14 +- site/src/api/queries/files.ts | 6 +- site/src/api/queries/groups.ts | 19 +- site/src/api/queries/insights.ts | 18 +- site/src/api/queries/integrations.ts | 4 +- site/src/api/queries/oauth2.ts | 20 +- site/src/api/queries/roles.ts | 4 +- site/src/api/queries/settings.ts | 6 +- site/src/api/queries/sshKeys.ts | 6 +- site/src/api/queries/templates.ts | 56 +- site/src/api/queries/updateCheck.ts | 4 +- site/src/api/queries/users.ts | 40 +- site/src/api/queries/workspaceBuilds.ts | 12 +- site/src/api/queries/workspaceQuota.ts | 6 +- site/src/api/queries/workspaceportsharing.ts | 12 +- site/src/api/queries/workspaces.ts | 41 +- site/src/components/Filter/UserFilter.tsx | 6 +- site/src/contexts/ProxyContext.tsx | 6 +- site/src/contexts/auth/RequireAuth.tsx | 3 +- site/src/contexts/useProxyLatency.ts | 3 +- .../src/modules/resources/AppLink/AppLink.tsx | 4 +- .../modules/resources/PortForwardButton.tsx | 4 +- .../VSCodeDesktopButton.tsx | 8 +- site/src/pages/AuditPage/AuditPage.test.tsx | 16 +- .../CreateTemplatePage.test.tsx | 59 +- .../CreateTokenPage/CreateTokenPage.test.tsx | 4 +- .../pages/CreateTokenPage/CreateTokenPage.tsx | 6 +- .../CreateWorkspacePage.test.tsx | 72 +- .../CreateWorkspacePage.tsx | 4 +- .../AddNewLicensePage.tsx | 4 +- .../LicensesSettingsPage.tsx | 6 +- .../TemplateEmbedPage.test.tsx | 4 +- .../TemplateEmbedPage/TemplateEmbedPage.tsx | 5 +- .../src/pages/TemplatePage/TemplateLayout.tsx | 16 +- .../TemplateSummaryPage.tsx | 4 +- .../TemplateVersionsPage.tsx | 12 +- .../useDeletionDialogState.test.ts | 6 +- .../TemplatePage/useDeletionDialogState.ts | 4 +- .../TemplateSettingsPage.test.tsx | 22 +- .../TemplateSettingsPage.tsx | 6 +- .../TemplateSchedulePage.test.tsx | 20 +- .../TemplateSchedulePage.tsx | 5 +- .../TemplateVariablesPage.test.tsx | 28 +- .../TemplateVersionEditorPage.test.tsx | 18 +- .../TemplateVersionEditorPage.tsx | 6 +- .../pages/TerminalPage/TerminalPage.test.tsx | 6 +- .../AccountPage/AccountPage.test.tsx | 50 +- .../AppearancePage/AppearancePage.test.tsx | 18 +- .../SSHKeysPage/SSHKeysPage.test.tsx | 10 +- .../SecurityPage/SecurityPage.test.tsx | 46 +- .../SecurityPage/SecurityPage.tsx | 4 +- .../SecurityPage/SingleSignOnSection.tsx | 4 +- .../UserSettingsPage/TokensPage/hooks.ts | 6 +- site/src/pages/UsersPage/UsersPage.test.tsx | 16 +- .../WorkspaceBuildPage.test.tsx | 4 +- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 4 +- .../BuildParametersPopover.tsx | 4 +- .../WorkspacePage/WorkspacePage.test.tsx | 58 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 4 +- .../WorkspaceScheduleControls.test.tsx | 10 +- .../WorkspaceParametersPage.test.tsx | 22 +- .../WorkspaceParametersPage.tsx | 10 +- .../WorkspaceScheduleForm.test.tsx | 16 +- .../WorkspaceSchedulePage.tsx | 15 +- .../WorkspaceSettingsPage.test.tsx | 8 +- .../WorkspaceSettingsPage.tsx | 6 +- .../BatchUpdateConfirmation.tsx | 4 +- .../WorkspacesPage/WorkspacesPage.test.tsx | 30 +- .../src/pages/WorkspacesPage/batchActions.tsx | 23 +- site/src/pages/WorkspacesPage/data.ts | 6 +- site/src/pages/WorkspacesPage/filter/menus.ts | 6 +- site/src/utils/terminal.ts | 4 +- 94 files changed, 668 insertions(+), 2077 deletions(-) diff --git a/site/e2e/api.ts b/site/e2e/api.ts index 65a4aaa40a937..fb442ca7f5bd7 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, client } from "api/api"; import type { SerpentOption } from "api/typesGenerated"; import { coderPort } from "./constants"; import { findSessionToken, randomName } from "./helpers"; @@ -11,25 +11,26 @@ let currentOrgId: string; export const setupApiCalls = async (page: Page) => { try { const token = await findSessionToken(page); - API.setSessionToken(token); + client.setSessionToken(token); } catch { // If this fails, we have an unauthenticated client. } - API.setHost(`http://127.0.0.1:${coderPort}`); + + client.setHost(`http://127.0.0.1:${coderPort}`); }; export const getCurrentOrgId = async (): Promise<string> => { if (currentOrgId) { return currentOrgId; } - const currentUser = await API.getAuthenticatedUser(); + const currentUser = await client.api.getAuthenticatedUser(); currentOrgId = currentUser.organization_ids[0]; return currentOrgId; }; export const createUser = async (orgId: string) => { const name = randomName(); - const user = await API.createUser({ + const user = await client.api.createUser({ email: `${name}@coder.com`, username: name, password: "s3cure&password!", @@ -42,7 +43,7 @@ export const createUser = async (orgId: string) => { export const createGroup = async (orgId: string) => { const name = randomName(); - const group = await API.createGroup(orgId, { + const group = await client.api.createGroup(orgId, { name, display_name: `Display ${name}`, avatar_url: "/emojis/1f60d.png", @@ -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..e577e048ddb27 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 { client } 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 client.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 1c5349fbf5e5b..474b378904371 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 { client } from "api/api"; import type { WorkspaceBuildParameter, UpdateTemplateMeta, @@ -396,6 +396,7 @@ export const waitUntilUrlIsNotResponding = async (url: string) => { const retryIntervalMs = 1000; let retries = 0; + const axiosInstance = client.getAxiosInstance(); while (retries < maxRetries) { try { await axiosInstance.get(url); diff --git a/site/e2e/reporter.ts b/site/e2e/reporter.ts index f981dede78e66..edbe9b12bc72e 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 { client } from "api/api"; import { coderdPProfPort, enterpriseLicense } from "./constants"; class CoderReporter implements Reporter { @@ -136,6 +136,7 @@ class CoderReporter implements Reporter { const logLines = (chunk: string): string[] => chunk.trimEnd().split("\n"); const exportDebugPprof = async (outputFile: string) => { + const axiosInstance = client.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..c52ec09b641f0 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 { client } from "api/api"; import { setupApiCalls } from "../../api"; import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants"; @@ -7,7 +7,7 @@ test("experiments", async ({ page }) => { await setupApiCalls(page); // Load experiments from backend API - const availableExperiments = await API.getAvailableExperiments(); + const availableExperiments = await client.api.getAvailableExperiments(); // Verify if the site lists the same experiments await page.goto("/deployment/general", { waitUntil: "networkidle" }); diff --git a/site/e2e/tests/deployment/network.spec.ts b/site/e2e/tests/deployment/network.spec.ts index c979bb8e1022f..36ea46d7f45ac 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 { client } 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 client.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..0268c8baa3ab1 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 { client } 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 client.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..e8a79776a8ebc 100644 --- a/site/e2e/tests/deployment/security.spec.ts +++ b/site/e2e/tests/deployment/security.spec.ts @@ -1,7 +1,7 @@ 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 { client } from "api/api"; import { findConfigOption, setupApiCalls, @@ -12,7 +12,7 @@ import { test("enabled security settings", async ({ page }) => { await setupApiCalls(page); - const config = await getDeploymentConfig(); + const config = await client.api.getDeploymentConfig(); await page.goto("/deployment/security", { waitUntil: "domcontentloaded" }); diff --git a/site/e2e/tests/deployment/userAuth.spec.ts b/site/e2e/tests/deployment/userAuth.spec.ts index cf656c99fae3f..8083e4f66537d 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 { client } 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 client.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..ff02853f01b83 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 { client } 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 client.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..3d2fb6534e846 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 { client } from "api/api"; import { createGroup, createUser, @@ -19,7 +19,7 @@ test("remove member", async ({ page, baseURL }) => { createGroup(orgId), createUser(orgId), ]); - await API.addMember(group.id, member.id); + await client.api.addMember(group.id, member.id); await page.goto(`${baseURL}/groups/${group.id}`, { waitUntil: "domcontentloaded", diff --git a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts index 1eb272a665edb..768d41518d7a3 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 { client } 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 client.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 client.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 client.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..f2547a22cd80c 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 { client, getURLWithSearchParams, MissingBuildParameters } from "./api"; import type * as TypesGen from "./typesGenerated"; +const axiosInstance = client.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 client.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 client.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 client.api.logout(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -80,7 +81,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await api.logout(); + await client.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 client.api.getApiKey(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -121,7 +122,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await api.getApiKey(); + await client.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,25 +173,30 @@ describe("api.ts", () => { describe("update", () => { it("creates a build with start and the latest template", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(client.api, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - 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: [], - }); + jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); + await client.api.updateWorkspace(MockWorkspace); + expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith( + MockWorkspace.id, + { + transition: "start", + template_version_id: MockTemplate.active_version_id, + rich_parameter_values: [], + }, + ); }); it("fails when having missing parameters", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(client.api, "postWorkspaceBuild") .mockResolvedValue(MockWorkspaceBuild); - jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate); - jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]); + jest.spyOn(client.api, "getTemplate").mockResolvedValue(MockTemplate); jest - .spyOn(api, "getTemplateVersionRichParameters") + .spyOn(client.api, "getWorkspaceBuildParameters") + .mockResolvedValue([]); + jest + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValue([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, @@ -198,14 +204,14 @@ describe("api.ts", () => { let error = new Error(); try { - await api.updateWorkspace(MockWorkspace); + await client.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,23 +219,26 @@ describe("api.ts", () => { it("creates a build with the no parameters if it is already filled", async () => { jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(client.api, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); jest - .spyOn(api, "getWorkspaceBuildParameters") + .spyOn(client.api, "getWorkspaceBuildParameters") .mockResolvedValue([MockWorkspaceBuildParameter1]); jest - .spyOn(api, "getTemplateVersionRichParameters") + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValue([ { ...MockTemplateVersionParameter1, required: true, mutable: false }, ]); - await api.updateWorkspace(MockWorkspace); - expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { - transition: "start", - template_version_id: MockTemplate.active_version_id, - rich_parameter_values: [], - }); + await client.api.updateWorkspace(MockWorkspace); + expect(client.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 976a0ba984895..5a8e4bfb1374f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -850,7 +850,7 @@ export class Api { }; deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { - return postWorkspaceBuild(workspaceId, { + return this.postWorkspaceBuild(workspaceId, { transition: "delete", ...options, }); @@ -914,7 +914,7 @@ export class Api { buildParameters, ); - await waitForBuild(startBuild); + await this.waitForBuild(startBuild); }; cancelTemplateVersionBuild = async ( @@ -1435,7 +1435,7 @@ export class Api { }; addMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { + return this.patchGroup(groupId, { name: "", add_users: [userId], remove_users: [], @@ -1443,7 +1443,7 @@ export class Api { }; removeMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { + return this.patchGroup(groupId, { name: "", display_name: "", add_users: [], @@ -1619,8 +1619,8 @@ export class Api { updateWorkspaceVersion = async ( workspace: TypesGen.Workspace, ): Promise<TypesGen.WorkspaceBuild> => { - const template = await getTemplate(workspace.template_id); - return startWorkspace(workspace.id, template.active_version_id); + const template = await this.getTemplate(workspace.template_id); + return this.startWorkspace(workspace.id, template.active_version_id); }; getWorkspaceBuildParameters = async ( @@ -1705,7 +1705,7 @@ export class Api { const activeVersionId = template.active_version_id; const templateParameters = - await getTemplateVersionRichParameters(activeVersionId); + await this.getTemplateVersionRichParameters(activeVersionId); const missingParameters = getMissingParameters( oldBuildParameters, @@ -1717,7 +1717,7 @@ export class Api { throw new MissingBuildParameters(missingParameters, activeVersionId); } - return postWorkspaceBuild(workspace.id, { + return this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: activeVersionId, rich_parameter_values: newBuildParameters, @@ -1871,6 +1871,7 @@ interface ClientApi { getCsrfToken: () => string; setSessionToken: (token: string) => void; setHost: (host: string | undefined) => void; + getAxiosInstance: () => AxiosInstance; } export class Client implements ClientApi { @@ -1927,1505 +1928,10 @@ export class Client implements ClientApi { setHost = (host: string | undefined): void => { this.axios.defaults.baseURL = host; }; -} - -export const client = new Client(); - -//////////////////////////////////////////////////////////////////////////////// -// START OF OLD CODE -//////////////////////////////////////////////////////////////////////////////// - -export const axiosInstance = globalAxios.create(); - -// 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; -}; - -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; -}; - -// 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; -}; - -export const login = async ( - email: string, - password: string, -): Promise<TypesGen.LoginWithPasswordResponse> => { - const payload = JSON.stringify({ - email, - password, - }); - - const response = await axiosInstance.post<TypesGen.LoginWithPasswordResponse>( - "/api/v2/users/login", - payload, - { - headers: { ...BASE_CONTENT_TYPE_JSON }, - }, - ); - - return response.data; -}; - -export const convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { - const response = await axiosInstance.post<TypesGen.OAuthConversionResponse>( - "/api/v2/users/me/convert-login", - request, - ); - return response.data; -}; - -export const logout = async (): Promise<void> => { - await axiosInstance.post("/api/v2/users/logout"); -}; - -export const getAuthenticatedUser = async () => { - const response = await axiosInstance.get<TypesGen.User>("/api/v2/users/me"); - return response.data; -}; - -export const getUserParameters = async (templateID: string) => { - const response = await axiosInstance.get<TypesGen.UserParameter[]>( - "/api/v2/users/me/autofill-parameters?template_id=" + templateID, - ); - return response.data; -}; - -export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => { - const response = await axiosInstance.get<TypesGen.AuthMethods>( - "/api/v2/users/authmethods", - ); - return response.data; -}; - -export const getUserLoginType = async (): Promise<TypesGen.UserLoginType> => { - const response = await axiosInstance.get<TypesGen.UserLoginType>( - "/api/v2/users/me/login-type", - ); - return response.data; -}; - -export const checkAuthorization = async ( - params: TypesGen.AuthorizationRequest, -): Promise<TypesGen.AuthorizationResponse> => { - const response = await axiosInstance.post<TypesGen.AuthorizationResponse>( - `/api/v2/authcheck`, - params, - ); - return response.data; -}; - -export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => { - const response = await axiosInstance.post<TypesGen.GenerateAPIKeyResponse>( - "/api/v2/users/me/keys", - ); - return response.data; -}; - -export const getTokens = async ( - params: TypesGen.TokensFilter, -): Promise<TypesGen.APIKeyWithOwner[]> => { - const response = await axiosInstance.get<TypesGen.APIKeyWithOwner[]>( - `/api/v2/users/me/keys/tokens`, - { - params, - }, - ); - return response.data; -}; - -export const deleteToken = async (keyId: string): Promise<void> => { - await axiosInstance.delete("/api/v2/users/me/keys/" + keyId); -}; - -export const createToken = async ( - params: TypesGen.CreateTokenRequest, -): Promise<TypesGen.GenerateAPIKeyResponse> => { - const response = await axiosInstance.post( - `/api/v2/users/me/keys/tokens`, - params, - ); - return response.data; -}; - -export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => { - const response = await axiosInstance.get( - "/api/v2/users/me/keys/tokens/tokenconfig", - ); - return response.data; -}; - -export const getUsers = async ( - options: TypesGen.UsersRequest, - signal?: AbortSignal, -): Promise<TypesGen.GetUsersResponse> => { - const url = getURLWithSearchParams("/api/v2/users", options); - const response = await axiosInstance.get<TypesGen.GetUsersResponse>( - url.toString(), - { - signal, - }, - ); - return response.data; -}; - -export const getOrganization = async ( - organizationId: string, -): Promise<TypesGen.Organization> => { - const response = await axiosInstance.get<TypesGen.Organization>( - `/api/v2/organizations/${organizationId}`, - ); - return response.data; -}; - -export const getOrganizations = async (): Promise<TypesGen.Organization[]> => { - const response = await axiosInstance.get<TypesGen.Organization[]>( - "/api/v2/users/me/organizations", - ); - return response.data; -}; - -export const getTemplate = async ( - templateId: string, -): Promise<TypesGen.Template> => { - const response = await axiosInstance.get<TypesGen.Template>( - `/api/v2/templates/${templateId}`, - ); - return response.data; -}; - -export const getTemplates = async ( - organizationId: string, - options?: TemplateOptions, -): Promise<TypesGen.Template[]> => { - const params = {} as Record<string, string>; - 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); - } - - const response = await axiosInstance.get<TypesGen.Template[]>( - `/api/v2/organizations/${organizationId}/templates`, - { - params, - }, - ); - return response.data; -}; - -export const getTemplateByName = async ( - organizationId: string, - name: string, -): Promise<TypesGen.Template> => { - const response = await axiosInstance.get<TypesGen.Template>( - `/api/v2/organizations/${organizationId}/templates/${name}`, - ); - return response.data; -}; - -export const getTemplateVersion = async ( - versionId: string, -): Promise<TypesGen.TemplateVersion> => { - const response = await axiosInstance.get<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${versionId}`, - ); - return response.data; -}; - -export const getTemplateVersionResources = async ( - versionId: string, -): Promise<TypesGen.WorkspaceResource[]> => { - const response = await axiosInstance.get<TypesGen.WorkspaceResource[]>( - `/api/v2/templateversions/${versionId}/resources`, - ); - return response.data; -}; - -export const getTemplateVersionVariables = async ( - versionId: string, -): Promise<TypesGen.TemplateVersionVariable[]> => { - const response = await axiosInstance.get<TypesGen.TemplateVersionVariable[]>( - `/api/v2/templateversions/${versionId}/variables`, - ); - return response.data; -}; - -export const getTemplateVersions = async ( - templateId: string, -): Promise<TypesGen.TemplateVersion[]> => { - const response = await axiosInstance.get<TypesGen.TemplateVersion[]>( - `/api/v2/templates/${templateId}/versions`, - ); - return response.data; -}; - -export const getTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, -): Promise<TypesGen.TemplateVersion> => { - const response = await axiosInstance.get<TypesGen.TemplateVersion>( - `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, - ); - return response.data; -}; - -export const getPreviousTemplateVersionByName = async ( - organizationId: string, - templateName: string, - versionName: string, -) => { - try { - const response = await axiosInstance.get<TypesGen.TemplateVersion>( - `/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 const createTemplateVersion = async ( - organizationId: string, - data: TypesGen.CreateTemplateVersionRequest, -): Promise<TypesGen.TemplateVersion> => { - const response = await axiosInstance.post<TypesGen.TemplateVersion>( - `/api/v2/organizations/${organizationId}/templateversions`, - data, - ); - return response.data; -}; - -export const getTemplateVersionExternalAuth = async ( - versionId: string, -): Promise<TypesGen.TemplateVersionExternalAuth[]> => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/external-auth`, - ); - return response.data; -}; - -export const getTemplateVersionRichParameters = async ( - versionId: string, -): Promise<TypesGen.TemplateVersionParameter[]> => { - const response = await axiosInstance.get( - `/api/v2/templateversions/${versionId}/rich-parameters`, - ); - return response.data; -}; -export const createTemplate = async ( - organizationId: string, - data: TypesGen.CreateTemplateRequest, -): Promise<TypesGen.Template> => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/templates`, - data, - ); - return response.data; -}; - -export const updateActiveTemplateVersion = async ( - templateId: string, - data: TypesGen.UpdateActiveTemplateVersion, -) => { - const response = await axiosInstance.patch<TypesGen.Response>( - `/api/v2/templates/${templateId}/versions`, - data, - ); - return response.data; -}; - -export const patchTemplateVersion = async ( - templateVersionId: string, - data: TypesGen.PatchTemplateVersionRequest, -) => { - const response = await axiosInstance.patch<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}`, - data, - ); - return response.data; -}; - -export const archiveTemplateVersion = async (templateVersionId: string) => { - const response = await axiosInstance.post<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}/archive`, - ); - return response.data; -}; - -export const unarchiveTemplateVersion = async (templateVersionId: string) => { - const response = await axiosInstance.post<TypesGen.TemplateVersion>( - `/api/v2/templateversions/${templateVersionId}/unarchive`, - ); - return response.data; -}; - -export const updateTemplateMeta = async ( - templateId: string, - data: TypesGen.UpdateTemplateMeta, -): Promise<TypesGen.Template | null> => { - const response = await axiosInstance.patch<TypesGen.Template>( - `/api/v2/templates/${templateId}`, - data, - ); - // On 304 response there is no data payload. - if (response.status === 304) { - return null; - } - - return response.data; -}; - -export const deleteTemplate = async ( - templateId: string, -): Promise<TypesGen.Template> => { - const response = await axiosInstance.delete<TypesGen.Template>( - `/api/v2/templates/${templateId}`, - ); - return response.data; -}; - -export const getWorkspace = async ( - workspaceId: string, - params?: TypesGen.WorkspaceOptions, -): Promise<TypesGen.Workspace> => { - const response = await axiosInstance.get<TypesGen.Workspace>( - `/api/v2/workspaces/${workspaceId}`, - { - params, - }, - ); - return response.data; -}; - -export const getWorkspaces = async ( - options: TypesGen.WorkspacesRequest, -): Promise<TypesGen.WorkspacesResponse> => { - const url = getURLWithSearchParams("/api/v2/workspaces", options); - const response = await axiosInstance.get<TypesGen.WorkspacesResponse>(url); - return response.data; -}; - -export const getWorkspaceByOwnerAndName = async ( - username = "me", - workspaceName: string, - params?: TypesGen.WorkspaceOptions, -): Promise<TypesGen.Workspace> => { - const response = await axiosInstance.get<TypesGen.Workspace>( - `/api/v2/users/${username}/workspace/${workspaceName}`, - { - params, - }, - ); - return response.data; -}; - -export function waitForBuild(build: TypesGen.WorkspaceBuild) { - return new Promise<TypesGen.ProvisionerJob | undefined>((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); - } - - await delay(1000); - } - - return res(latestJobInfo); - })(); - }); -} - -export const postWorkspaceBuild = async ( - workspaceId: string, - data: TypesGen.CreateWorkspaceBuildRequest, -): Promise<TypesGen.WorkspaceBuild> => { - const response = await axiosInstance.post( - `/api/v2/workspaces/${workspaceId}/builds`, - data, - ); - return response.data; -}; - -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, - }); - -export const deleteWorkspace = ( - workspaceId: string, - options?: DeleteWorkspaceOptions, -) => - postWorkspaceBuild(workspaceId, { - transition: "delete", - ...options, - }); - -export const cancelWorkspaceBuild = async ( - workspaceBuildId: TypesGen.WorkspaceBuild["id"], -): Promise<TypesGen.Response> => { - const response = await axiosInstance.patch( - `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, - ); - return response.data; -}; - -export const updateWorkspaceDormancy = async ( - workspaceId: string, - dormant: boolean, -): Promise<TypesGen.Workspace> => { - const data: TypesGen.UpdateWorkspaceDormancy = { - dormant: dormant, + getAxiosInstance = (): AxiosInstance => { + return this.axios; }; +} - const response = await axiosInstance.put( - `/api/v2/workspaces/${workspaceId}/dormant`, - data, - ); - return response.data; -}; - -export const updateWorkspaceAutomaticUpdates = async ( - workspaceId: string, - automaticUpdates: TypesGen.AutomaticUpdates, -): Promise<void> => { - const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { - automatic_updates: automaticUpdates, - }; - - const response = await axiosInstance.put( - `/api/v2/workspaces/${workspaceId}/autoupdates`, - req, - ); - return response.data; -}; - -export const restartWorkspace = async ({ - workspace, - buildParameters, -}: { - workspace: TypesGen.Workspace; - buildParameters?: TypesGen.WorkspaceBuildParameter[]; -}) => { - const stopBuild = await stopWorkspace(workspace.id); - const awaitedStopBuild = await waitForBuild(stopBuild); - - // If the restart is canceled halfway through, make sure we bail - if (awaitedStopBuild?.status === "canceled") { - return; - } - - const startBuild = await startWorkspace( - workspace.id, - workspace.latest_build.template_version_id, - undefined, - buildParameters, - ); - await waitForBuild(startBuild); -}; - -export const cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"], -): Promise<TypesGen.Response> => { - const response = await axiosInstance.patch( - `/api/v2/templateversions/${templateVersionId}/cancel`, - ); - return response.data; -}; - -export const createUser = async ( - user: TypesGen.CreateUserRequest, -): Promise<TypesGen.User> => { - const response = await axiosInstance.post<TypesGen.User>( - "/api/v2/users", - user, - ); - return response.data; -}; - -export const createWorkspace = async ( - organizationId: string, - userId = "me", - workspace: TypesGen.CreateWorkspaceRequest, -): Promise<TypesGen.Workspace> => { - const response = await axiosInstance.post<TypesGen.Workspace>( - `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, - workspace, - ); - return response.data; -}; - -export const patchWorkspace = async ( - workspaceId: string, - data: TypesGen.UpdateWorkspaceRequest, -) => { - await axiosInstance.patch(`/api/v2/workspaces/${workspaceId}`, data); -}; - -export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => { - const response = await axiosInstance.get("/api/v2/buildinfo"); - return response.data; -}; - -export const getUpdateCheck = - async (): Promise<TypesGen.UpdateCheckResponse> => { - const response = await axiosInstance.get("/api/v2/updatecheck"); - return response.data; - }; - -export const putWorkspaceAutostart = async ( - workspaceID: string, - autostart: TypesGen.UpdateWorkspaceAutostartRequest, -): Promise<void> => { - const payload = JSON.stringify(autostart); - await axiosInstance.put( - `/api/v2/workspaces/${workspaceID}/autostart`, - payload, - { - headers: { ...BASE_CONTENT_TYPE_JSON }, - }, - ); -}; - -export const putWorkspaceAutostop = async ( - workspaceID: string, - ttl: TypesGen.UpdateWorkspaceTTLRequest, -): Promise<void> => { - const payload = JSON.stringify(ttl); - await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { - headers: { ...BASE_CONTENT_TYPE_JSON }, - }); -}; - -export const updateProfile = async ( - userId: string, - data: TypesGen.UpdateUserProfileRequest, -): Promise<TypesGen.User> => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/profile`, - data, - ); - return response.data; -}; - -export const updateAppearanceSettings = async ( - userId: string, - data: TypesGen.UpdateUserAppearanceSettingsRequest, -): Promise<TypesGen.User> => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/appearance`, - data, - ); - return response.data; -}; - -export const getUserQuietHoursSchedule = async ( - userId: TypesGen.User["id"], -): Promise<TypesGen.UserQuietHoursScheduleResponse> => { - const response = await axiosInstance.get( - `/api/v2/users/${userId}/quiet-hours`, - ); - return response.data; -}; - -export const updateUserQuietHoursSchedule = async ( - userId: TypesGen.User["id"], - data: TypesGen.UpdateUserQuietHoursScheduleRequest, -): Promise<TypesGen.UserQuietHoursScheduleResponse> => { - const response = await axiosInstance.put( - `/api/v2/users/${userId}/quiet-hours`, - data, - ); - return response.data; -}; - -export const activateUser = async ( - userId: TypesGen.User["id"], -): Promise<TypesGen.User> => { - const response = await axiosInstance.put<TypesGen.User>( - `/api/v2/users/${userId}/status/activate`, - ); - return response.data; -}; - -export const suspendUser = async ( - userId: TypesGen.User["id"], -): Promise<TypesGen.User> => { - const response = await axiosInstance.put<TypesGen.User>( - `/api/v2/users/${userId}/status/suspend`, - ); - return response.data; -}; - -export const deleteUser = async ( - userId: TypesGen.User["id"], -): Promise<undefined> => { - return await axiosInstance.delete(`/api/v2/users/${userId}`); -}; - -// API definition: -// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 -export const hasFirstUser = async (): Promise<boolean> => { - 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; - } - - throw error; - } -}; - -export const createFirstUser = async ( - req: TypesGen.CreateFirstUserRequest, -): Promise<TypesGen.CreateFirstUserResponse> => { - const response = await axiosInstance.post(`/api/v2/users/first`, req); - return response.data; -}; - -export const updateUserPassword = async ( - userId: TypesGen.User["id"], - updatePassword: TypesGen.UpdateUserPasswordRequest, -): Promise<undefined> => - axiosInstance.put(`/api/v2/users/${userId}/password`, updatePassword); - -export const getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => { - const response = - await axiosInstance.get<Array<TypesGen.AssignableRoles>>( - `/api/v2/users/roles`, - ); - return response.data; -}; - -export const updateUserRoles = async ( - roles: TypesGen.Role["name"][], - userId: TypesGen.User["id"], -): Promise<TypesGen.User> => { - const response = await axiosInstance.put<TypesGen.User>( - `/api/v2/users/${userId}/roles`, - { roles }, - ); - return response.data; -}; - -export const getUserSSHKey = async ( - userId = "me", -): Promise<TypesGen.GitSSHKey> => { - const response = await axiosInstance.get<TypesGen.GitSSHKey>( - `/api/v2/users/${userId}/gitsshkey`, - ); - return response.data; -}; - -export const regenerateUserSSHKey = async ( - userId = "me", -): Promise<TypesGen.GitSSHKey> => { - const response = await axiosInstance.put<TypesGen.GitSSHKey>( - `/api/v2/users/${userId}/gitsshkey`, - ); - return response.data; -}; - -export const getWorkspaceBuilds = async ( - workspaceId: string, - req?: TypesGen.WorkspaceBuildsRequest, -) => { - const response = await axiosInstance.get<TypesGen.WorkspaceBuild[]>( - getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), - ); - return response.data; -}; - -export const getWorkspaceBuildByNumber = async ( - username = "me", - workspaceName: string, - buildNumber: number, -): Promise<TypesGen.WorkspaceBuild> => { - const response = await axiosInstance.get<TypesGen.WorkspaceBuild>( - `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, - ); - return response.data; -}; - -export const getWorkspaceBuildLogs = async ( - buildId: string, - before: Date, -): Promise<TypesGen.ProvisionerJobLog[]> => { - const response = await axiosInstance.get<TypesGen.ProvisionerJobLog[]>( - `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, - ); - return response.data; -}; - -export const getWorkspaceAgentLogs = async ( - agentID: string, -): Promise<TypesGen.WorkspaceAgentLog[]> => { - const response = await axiosInstance.get<TypesGen.WorkspaceAgentLog[]>( - `/api/v2/workspaceagents/${agentID}/logs`, - ); - return response.data; -}; - -export const putWorkspaceExtension = async ( - workspaceId: string, - newDeadline: dayjs.Dayjs, -): Promise<void> => { - await axiosInstance.put(`/api/v2/workspaces/${workspaceId}/extend`, { - deadline: newDeadline, - }); -}; - -export const refreshEntitlements = async (): Promise<void> => { - await axiosInstance.post("/api/v2/licenses/refresh-entitlements"); -}; - -export const getEntitlements = async (): Promise<TypesGen.Entitlements> => { - 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; - } -}; - -export const getExperiments = async (): Promise<TypesGen.Experiment[]> => { - 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<TypesGen.AvailableExperiments> => { - 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; - } - }; - -export const getExternalAuthProvider = async ( - provider: string, -): Promise<TypesGen.ExternalAuth> => { - const resp = await axiosInstance.get(`/api/v2/external-auth/${provider}`); - return resp.data; -}; - -export const getExternalAuthDevice = async ( - provider: string, -): Promise<TypesGen.ExternalAuthDevice> => { - const resp = await axiosInstance.get( - `/api/v2/external-auth/${provider}/device`, - ); - return resp.data; -}; - -export const exchangeExternalAuthDevice = async ( - provider: string, - req: TypesGen.ExternalAuthDeviceExchange, -): Promise<void> => { - const resp = await axiosInstance.post( - `/api/v2/external-auth/${provider}/device`, - req, - ); - return resp.data; -}; - -export const getUserExternalAuthProviders = - async (): Promise<TypesGen.ListUserExternalAuthResponse> => { - const resp = await axiosInstance.get(`/api/v2/external-auth`); - return resp.data; - }; - -export const unlinkExternalAuthProvider = async ( - provider: string, -): Promise<string> => { - const resp = await axiosInstance.delete(`/api/v2/external-auth/${provider}`); - return resp.data; -}; - -export const getOAuth2ProviderApps = async ( - filter?: TypesGen.OAuth2ProviderAppFilter, -): Promise<TypesGen.OAuth2ProviderApp[]> => { - 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; -}; - -export const getOAuth2ProviderApp = async ( - id: string, -): Promise<TypesGen.OAuth2ProviderApp> => { - const resp = await axiosInstance.get(`/api/v2/oauth2-provider/apps/${id}`); - return resp.data; -}; - -export const postOAuth2ProviderApp = async ( - data: TypesGen.PostOAuth2ProviderAppRequest, -): Promise<TypesGen.OAuth2ProviderApp> => { - const response = await axiosInstance.post( - `/api/v2/oauth2-provider/apps`, - data, - ); - return response.data; -}; - -export const putOAuth2ProviderApp = async ( - id: string, - data: TypesGen.PutOAuth2ProviderAppRequest, -): Promise<TypesGen.OAuth2ProviderApp> => { - const response = await axiosInstance.put( - `/api/v2/oauth2-provider/apps/${id}`, - data, - ); - return response.data; -}; - -export const deleteOAuth2ProviderApp = async (id: string): Promise<void> => { - await axiosInstance.delete(`/api/v2/oauth2-provider/apps/${id}`); -}; - -export const getOAuth2ProviderAppSecrets = async ( - id: string, -): Promise<TypesGen.OAuth2ProviderAppSecret[]> => { - const resp = await axiosInstance.get( - `/api/v2/oauth2-provider/apps/${id}/secrets`, - ); - return resp.data; -}; - -export const postOAuth2ProviderAppSecret = async ( - id: string, -): Promise<TypesGen.OAuth2ProviderAppSecretFull> => { - const resp = await axiosInstance.post( - `/api/v2/oauth2-provider/apps/${id}/secrets`, - ); - return resp.data; -}; - -export const deleteOAuth2ProviderAppSecret = async ( - appId: string, - secretId: string, -): Promise<void> => { - await axiosInstance.delete( - `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, - ); -}; - -export const revokeOAuth2ProviderApp = async (appId: string): Promise<void> => { - await axiosInstance.delete(`/oauth2/tokens?client_id=${appId}`); -}; - -export const getAuditLogs = async ( - options: TypesGen.AuditLogsRequest, -): Promise<TypesGen.AuditLogResponse> => { - const url = getURLWithSearchParams("/api/v2/audit", options); - const response = await axiosInstance.get(url); - return response.data; -}; - -export const getTemplateDAUs = async ( - templateId: string, -): Promise<TypesGen.DAUsResponse> => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}/daus`, - ); - return response.data; -}; - -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<TypesGen.DAUsResponse> => { - const response = await axiosInstance.get( - `/api/v2/insights/daus?tz_offset=${offset}`, - ); - return response.data; -}; - -export const getTemplateACLAvailable = async ( - templateId: string, - options: TypesGen.UsersRequest, -): Promise<TypesGen.ACLAvailable> => { - const url = getURLWithSearchParams( - `/api/v2/templates/${templateId}/acl/available`, - options, - ); - const response = await axiosInstance.get(url.toString()); - return response.data; -}; - -export const getTemplateACL = async ( - templateId: string, -): Promise<TypesGen.TemplateACL> => { - const response = await axiosInstance.get( - `/api/v2/templates/${templateId}/acl`, - ); - return response.data; -}; - -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; -}; - -export const getApplicationsHost = - async (): Promise<TypesGen.AppHostResponse> => { - const response = await axiosInstance.get(`/api/v2/applications/host`); - return response.data; - }; - -export const getGroups = async ( - organizationId: string, -): Promise<TypesGen.Group[]> => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/groups`, - ); - return response.data; -}; - -export const createGroup = async ( - organizationId: string, - data: TypesGen.CreateGroupRequest, -): Promise<TypesGen.Group> => { - const response = await axiosInstance.post( - `/api/v2/organizations/${organizationId}/groups`, - data, - ); - return response.data; -}; - -export const getGroup = async (groupId: string): Promise<TypesGen.Group> => { - const response = await axiosInstance.get(`/api/v2/groups/${groupId}`); - return response.data; -}; - -export const patchGroup = async ( - groupId: string, - data: TypesGen.PatchGroupRequest, -): Promise<TypesGen.Group> => { - const response = await axiosInstance.patch(`/api/v2/groups/${groupId}`, data); - return response.data; -}; - -export const addMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { - name: "", - add_users: [userId], - remove_users: [], - }); -}; - -export const removeMember = async (groupId: string, userId: string) => { - return patchGroup(groupId, { - name: "", - display_name: "", - add_users: [], - remove_users: [userId], - }); -}; - -export const deleteGroup = async (groupId: string): Promise<void> => { - await axiosInstance.delete(`/api/v2/groups/${groupId}`); -}; - -export const getWorkspaceQuota = async ( - username: string, -): Promise<TypesGen.WorkspaceQuota> => { - const response = await axiosInstance.get( - `/api/v2/workspace-quota/${encodeURIComponent(username)}`, - ); - return response.data; -}; - -export const getAgentListeningPorts = async ( - agentID: string, -): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => { - const response = await axiosInstance.get( - `/api/v2/workspaceagents/${agentID}/listening-ports`, - ); - return response.data; -}; - -export const getWorkspaceAgentSharedPorts = async ( - workspaceID: string, -): Promise<TypesGen.WorkspaceAgentPortShares> => { - const response = await axiosInstance.get( - `/api/v2/workspaces/${workspaceID}/port-share`, - ); - return response.data; -}; - -export const upsertWorkspaceAgentSharedPort = async ( - workspaceID: string, - req: TypesGen.UpsertWorkspaceAgentPortShareRequest, -): Promise<TypesGen.WorkspaceAgentPortShares> => { - const response = await axiosInstance.post( - `/api/v2/workspaces/${workspaceID}/port-share`, - req, - ); - return response.data; -}; - -export const deleteWorkspaceAgentSharedPort = async ( - workspaceID: string, - req: TypesGen.DeleteWorkspaceAgentPortShareRequest, -): Promise<TypesGen.WorkspaceAgentPortShares> => { - const response = await axiosInstance.delete( - `/api/v2/workspaces/${workspaceID}/port-share`, - { - data: req, - }, - ); - return response.data; -}; - -// getDeploymentSSHConfig is used by the VSCode-Extension. -export const getDeploymentSSHConfig = - async (): Promise<TypesGen.SSHConfigResponse> => { - const response = await axiosInstance.get(`/api/v2/deployment/ssh`); - return response.data; - }; - -export const getDeploymentConfig = async (): Promise<DeploymentConfig> => { - const response = await axiosInstance.get(`/api/v2/deployment/config`); - return response.data; -}; - -export const getDeploymentStats = - async (): Promise<TypesGen.DeploymentStats> => { - const response = await axiosInstance.get(`/api/v2/deployment/stats`); - return response.data; - }; - -export const getReplicas = async (): Promise<TypesGen.Replica[]> => { - const response = await axiosInstance.get(`/api/v2/replicas`); - return response.data; -}; - -export const getFile = async (fileId: string): Promise<ArrayBuffer> => { - const response = await axiosInstance.get<ArrayBuffer>( - `/api/v2/files/${fileId}`, - { - responseType: "arraybuffer", - }, - ); - return response.data; -}; - -export const getWorkspaceProxyRegions = async (): Promise< - TypesGen.RegionsResponse<TypesGen.Region> -> => { - const response = - await axiosInstance.get<TypesGen.RegionsResponse<TypesGen.Region>>( - `/api/v2/regions`, - ); - return response.data; -}; - -export const getWorkspaceProxies = async (): Promise< - TypesGen.RegionsResponse<TypesGen.WorkspaceProxy> -> => { - const response = await axiosInstance.get< - TypesGen.RegionsResponse<TypesGen.WorkspaceProxy> - >(`/api/v2/workspaceproxies`); - return response.data; -}; - -export const createWorkspaceProxy = async ( - b: TypesGen.CreateWorkspaceProxyRequest, -): Promise<TypesGen.UpdateWorkspaceProxyResponse> => { - const response = await axiosInstance.post(`/api/v2/workspaceproxies`, b); - return response.data; -}; - -export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => { - 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, - }, - }; - } - throw ex; - } -}; - -export const updateAppearance = async ( - b: TypesGen.AppearanceConfig, -): Promise<TypesGen.AppearanceConfig> => { - const response = await axiosInstance.put(`/api/v2/appearance`, b); - return response.data; -}; - -export const getTemplateExamples = async ( - organizationId: string, -): Promise<TypesGen.TemplateExample[]> => { - const response = await axiosInstance.get( - `/api/v2/organizations/${organizationId}/templates/examples`, - ); - return response.data; -}; - -export const uploadFile = async ( - file: File, -): Promise<TypesGen.UploadResponse> => { - const response = await axiosInstance.post("/api/v2/files", file, { - headers: { - "Content-Type": "application/x-tar", - }, - }); - return response.data; -}; - -export const getTemplateVersionLogs = async ( - versionId: string, -): Promise<TypesGen.ProvisionerJobLog[]> => { - const response = await axiosInstance.get<TypesGen.ProvisionerJobLog[]>( - `/api/v2/templateversions/${versionId}/logs`, - ); - return response.data; -}; - -export const updateWorkspaceVersion = async ( - workspace: TypesGen.Workspace, -): Promise<TypesGen.WorkspaceBuild> => { - const template = await getTemplate(workspace.template_id); - return startWorkspace(workspace.id, template.active_version_id); -}; - -export const getWorkspaceBuildParameters = async ( - workspaceBuildId: TypesGen.WorkspaceBuild["id"], -): Promise<TypesGen.WorkspaceBuildParameter[]> => { - const response = await axiosInstance.get<TypesGen.WorkspaceBuildParameter[]>( - `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, - ); - return response.data; -}; - -export const getLicenses = async (): Promise<GetLicensesResponse[]> => { - const response = await axiosInstance.get(`/api/v2/licenses`); - return response.data; -}; - -export const createLicense = async ( - data: TypesGen.AddLicenseRequest, -): Promise<TypesGen.AddLicenseRequest> => { - const response = await axiosInstance.post(`/api/v2/licenses`, data); - return response.data; -}; - -export const removeLicense = async (licenseId: number): Promise<void> => { - await axiosInstance.delete(`/api/v2/licenses/${licenseId}`); -}; - -/** 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<TypesGen.WorkspaceBuild> => { - const [currentBuildParameters, templateParameters] = await Promise.all([ - getWorkspaceBuildParameters(workspace.latest_build.id), - getTemplateVersionRichParameters(templateVersionId), - ]); - - const missingParameters = getMissingParameters( - currentBuildParameters, - newBuildParameters, - templateParameters, - ); - - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, templateVersionId); - } - - return postWorkspaceBuild(workspace.id, { - transition: "start", - template_version_id: templateVersionId, - rich_parameter_values: newBuildParameters, - }); -}; - -/** 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<TypesGen.WorkspaceBuild> => { - 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, - ); - - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, activeVersionId); - } - - return postWorkspaceBuild(workspace.id, { - transition: "start", - template_version_id: activeVersionId, - rich_parameter_values: newBuildParameters, - }); -}; - -export const getWorkspaceResolveAutostart = async ( - workspaceId: string, -): Promise<TypesGen.ResolveAutostartResponse> => { - const response = await axiosInstance.get( - `/api/v2/workspaces/${workspaceId}/resolve-autostart`, - ); - return response.data; -}; - -export const issueReconnectingPTYSignedToken = async ( - params: TypesGen.IssueReconnectingPTYSignedTokenRequest, -): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => { - const response = await axiosInstance.post( - "/api/v2/applications/reconnecting-pty-signed-token", - params, - ); - 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, - }; -}; - -export const getInsightsUserLatency = async ( - filters: InsightsParams, -): Promise<TypesGen.UserLatencyInsightsResponse> => { - const params = new URLSearchParams(filters); - const response = await axiosInstance.get( - `/api/v2/insights/user-latency?${params}`, - ); - return response.data; -}; - -export const getInsightsUserActivity = async ( - filters: InsightsParams, -): Promise<TypesGen.UserActivityInsightsResponse> => { - const params = new URLSearchParams(filters); - const response = await axiosInstance.get( - `/api/v2/insights/user-activity?${params}`, - ); - return response.data; -}; - -export const getInsightsTemplate = async ( - params: InsightsTemplateParams, -): Promise<TypesGen.TemplateInsightsResponse> => { - const searchParams = new URLSearchParams(params); - const response = await axiosInstance.get( - `/api/v2/insights/templates?${searchParams}`, - ); - return response.data; -}; - -export const getHealth = async (force: boolean = false) => { - const params = new URLSearchParams({ force: force.toString() }); - const response = await axiosInstance.get<TypesGen.HealthcheckReport>( - `/api/v2/debug/health?${params}`, - ); - return response.data; -}; - -export const getHealthSettings = async () => { - return ( - await axiosInstance.get<TypesGen.HealthSettings>( - `/api/v2/debug/health/settings`, - ) - ).data; -}; - -export const updateHealthSettings = async ( - data: TypesGen.UpdateHealthSettings, -) => { - const response = await axiosInstance.put<TypesGen.HealthSettings>( - `/api/v2/debug/health/settings`, - data, - ); - return response.data; -}; - -export const putFavoriteWorkspace = async (workspaceID: string) => { - await axiosInstance.put(`/api/v2/workspaces/${workspaceID}/favorite`); -}; - -export const deleteFavoriteWorkspace = async (workspaceID: string) => { - await axiosInstance.delete(`/api/v2/workspaces/${workspaceID}/favorite`); -}; - -export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { - const searchParams = new URLSearchParams({ - workspace_id: options.workspaceId, - agent_id: options.agentId, - }); - - try { - const res = await axiosInstance.get<TypesGen.JFrogXrayScan>( - `/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; - } - } -}; +export const client = new Client(); diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index 8178ed602abb7..a60ebd32f0c17 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -1,5 +1,5 @@ import type { QueryClient, UseQueryOptions } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { AppearanceConfig } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; import { cachedQuery } from "./util"; @@ -12,13 +12,13 @@ export const appearance = (): UseQueryOptions<AppearanceConfig> => { return cachedQuery({ initialData: initialAppearanceData, queryKey: ["appearance"], - queryFn: () => API.getAppearance(), + queryFn: () => client.api.getAppearance(), }); }; export const updateAppearance = (queryClient: QueryClient) => { return { - mutationFn: API.updateAppearance, + mutationFn: client.api.updateAppearance, onSuccess: (newConfig: AppearanceConfig) => { queryClient.setQueryData(appearanceConfigKey, newConfig); }, diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts index 6430767480714..22326c1c8d4a8 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 { client } 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 client.api.getAuditLogs({ offset, limit, q: payload, diff --git a/site/src/api/queries/authCheck.ts b/site/src/api/queries/authCheck.ts index be9e726ae074d..470685f354789 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 { client } from "api/api"; import type { AuthorizationRequest } from "api/typesGenerated"; export const AUTHORIZATION_KEY = "authorization"; @@ -9,6 +9,6 @@ export const getAuthorizationKey = (req: AuthorizationRequest) => export const checkAuthorization = (req: AuthorizationRequest) => { return { queryKey: getAuthorizationKey(req), - queryFn: () => API.checkAuthorization(req), + queryFn: () => client.api.checkAuthorization(req), }; }; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index 504b59bd1d341..b3f43928c6224 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -1,5 +1,5 @@ import type { UseQueryOptions } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { BuildInfoResponse } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; import { cachedQuery } from "./util"; @@ -12,6 +12,6 @@ export const buildInfo = (): UseQueryOptions<BuildInfoResponse> => { return cachedQuery({ initialData: initialBuildInfoData, queryKey: buildInfoKey, - queryFn: () => API.getBuildInfo(), + queryFn: () => client.api.getBuildInfo(), }); }; diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts index 1fba00c172c51..5cd4f40c7b635 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 { client } from "api/api"; import type { HealthSettings, UpdateHealthSettings } from "api/typesGenerated"; export const HEALTH_QUERY_KEY = ["health"]; @@ -7,14 +7,14 @@ export const HEALTH_QUERY_SETTINGS_KEY = ["health", "settings"]; export const health = () => ({ queryKey: HEALTH_QUERY_KEY, - queryFn: async () => API.getHealth(), + queryFn: async () => client.api.getHealth(), }); export const refreshHealth = (queryClient: QueryClient) => { return { mutationFn: async () => { await queryClient.cancelQueries(HEALTH_QUERY_KEY); - const newHealthData = await API.getHealth(true); + const newHealthData = await client.api.getHealth(true); queryClient.setQueryData(HEALTH_QUERY_KEY, newHealthData); }, }; @@ -23,7 +23,7 @@ export const refreshHealth = (queryClient: QueryClient) => { export const healthSettings = () => { return { queryKey: HEALTH_QUERY_SETTINGS_KEY, - queryFn: API.getHealthSettings, + queryFn: client.api.getHealthSettings, }; }; @@ -36,7 +36,7 @@ export const updateHealthSettings = ( unknown > => { return { - mutationFn: API.updateHealthSettings, + mutationFn: client.api.updateHealthSettings, onSuccess: async (_, newSettings) => { await queryClient.invalidateQueries(HEALTH_QUERY_KEY); queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, newSettings); diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 540c76ebd79e2..e4ef76c623b34 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -1,29 +1,29 @@ -import * as API from "api/api"; +import { client } from "api/api"; export const deploymentConfig = () => { return { queryKey: ["deployment", "config"], - queryFn: API.getDeploymentConfig, + queryFn: client.api.getDeploymentConfig, }; }; export const deploymentDAUs = () => { return { queryKey: ["deployment", "daus"], - queryFn: () => API.getDeploymentDAUs(), + queryFn: () => client.api.getDeploymentDAUs(), }; }; export const deploymentStats = () => { return { queryKey: ["deployment", "stats"], - queryFn: API.getDeploymentStats, + queryFn: client.api.getDeploymentStats, }; }; export const deploymentSSHConfig = () => { return { queryKey: ["deployment", "sshConfig"], - queryFn: API.getDeploymentSSHConfig, + queryFn: client.api.getDeploymentSSHConfig, }; }; diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts index 46b41133bd476..bdb483073c5d3 100644 --- a/site/src/api/queries/entitlements.ts +++ b/site/src/api/queries/entitlements.ts @@ -1,5 +1,5 @@ import type { QueryClient, UseQueryOptions } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { Entitlements } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; import { cachedQuery } from "./util"; @@ -11,13 +11,13 @@ export const entitlements = (): UseQueryOptions<Entitlements> => { return cachedQuery({ initialData: initialEntitlementsData, queryKey: entitlementsQueryKey, - queryFn: () => API.getEntitlements(), + queryFn: () => client.api.getEntitlements(), }); }; export const refreshEntitlements = (queryClient: QueryClient) => { return { - mutationFn: API.refreshEntitlements, + mutationFn: client.api.refreshEntitlements, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: entitlementsQueryKey, diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index 934e44b863437..38c7ed4142b9f 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,5 +1,5 @@ import type { UseQueryOptions } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { Experiments } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; import { cachedQuery } from "./util"; @@ -11,13 +11,13 @@ export const experiments = (): UseQueryOptions<Experiments> => { return cachedQuery({ initialData: initialExperimentsData, queryKey: experimentsKey, - queryFn: () => API.getExperiments(), + queryFn: () => client.api.getExperiments(), }); }; export const availableExperiments = () => { return { queryKey: ["availableExperiments"], - queryFn: async () => API.getAvailableExperiments(), + queryFn: async () => client.api.getAvailableExperiments(), }; }; diff --git a/site/src/api/queries/externalAuth.ts b/site/src/api/queries/externalAuth.ts index 18cc95a8839ff..d3d905d9844d3 100644 --- a/site/src/api/queries/externalAuth.ts +++ b/site/src/api/queries/externalAuth.ts @@ -1,25 +1,25 @@ import type { QueryClient, UseMutationOptions } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { ExternalAuth } from "api/typesGenerated"; // Returns all configured external auths for a given user. export const externalAuths = () => { return { queryKey: ["external-auth"], - queryFn: () => API.getUserExternalAuthProviders(), + queryFn: () => client.api.getUserExternalAuthProviders(), }; }; export const externalAuthProvider = (providerId: string) => { return { queryKey: ["external-auth", providerId], - queryFn: () => API.getExternalAuthProvider(providerId), + queryFn: () => client.api.getExternalAuthProvider(providerId), }; }; export const externalAuthDevice = (providerId: string) => { return { - queryFn: () => API.getExternalAuthDevice(providerId), + queryFn: () => client.api.getExternalAuthDevice(providerId), queryKey: ["external-auth", providerId, "device"], }; }; @@ -31,7 +31,7 @@ export const exchangeExternalAuthDevice = ( ) => { return { queryFn: () => - API.exchangeExternalAuthDevice(providerId, { + client.api.exchangeExternalAuthDevice(providerId, { device_code: deviceCode, }), queryKey: ["external-auth", providerId, "device", deviceCode], @@ -46,7 +46,7 @@ export const validateExternalAuth = ( queryClient: QueryClient, ): UseMutationOptions<ExternalAuth, unknown, string> => { return { - mutationFn: API.getExternalAuthProvider, + mutationFn: client.api.getExternalAuthProvider, onSuccess: (data, providerId) => { queryClient.setQueryData(["external-auth", providerId], data); }, @@ -55,7 +55,7 @@ export const validateExternalAuth = ( export const unlinkExternalAuths = (queryClient: QueryClient) => { return { - mutationFn: API.unlinkExternalAuthProvider, + mutationFn: client.api.unlinkExternalAuthProvider, onSuccess: async () => { await queryClient.invalidateQueries(["external-auth"]); }, diff --git a/site/src/api/queries/files.ts b/site/src/api/queries/files.ts index cc840b52eb63f..c8e30e3b9bd0d 100644 --- a/site/src/api/queries/files.ts +++ b/site/src/api/queries/files.ts @@ -1,14 +1,14 @@ -import * as API from "api/api"; +import { client } from "api/api"; export const uploadFile = () => { return { - mutationFn: API.uploadFile, + mutationFn: client.api.uploadFile, }; }; export const file = (fileId: string) => { return { queryKey: ["files", fileId], - queryFn: () => API.getFile(fileId), + queryFn: () => client.api.getFile(fileId), }; }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 71cba33354b9b..1bd4d1554ca1b 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 { client } from "api/api"; import type { CreateGroupRequest, Group, @@ -15,14 +14,14 @@ const getGroupQueryKey = (groupId: string) => ["group", groupId]; export const groups = (organizationId: string) => { return { queryKey: GROUPS_QUERY_KEY, - queryFn: () => API.getGroups(organizationId), + queryFn: () => client.api.getGroups(organizationId), } satisfies UseQueryOptions<Group[]>; }; export const group = (groupId: string) => { return { queryKey: getGroupQueryKey(groupId), - queryFn: () => API.getGroup(groupId), + queryFn: () => client.api.getGroup(groupId), }; }; @@ -72,7 +71,7 @@ export const groupPermissions = (groupId: string) => { return { queryKey: [...getGroupQueryKey(groupId), "permissions"], queryFn: () => - checkAuthorization({ + client.api.checkAuthorization({ checks: { canUpdateGroup: { object: { @@ -92,7 +91,7 @@ export const createGroup = (queryClient: QueryClient) => { organizationId, ...request }: CreateGroupRequest & { organizationId: string }) => - API.createGroup(organizationId, request), + client.api.createGroup(organizationId, request), onSuccess: async () => { await queryClient.invalidateQueries(GROUPS_QUERY_KEY); }, @@ -105,7 +104,7 @@ export const patchGroup = (queryClient: QueryClient) => { groupId, ...request }: PatchGroupRequest & { groupId: string }) => - API.patchGroup(groupId, request), + client.api.patchGroup(groupId, request), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; @@ -113,7 +112,7 @@ export const patchGroup = (queryClient: QueryClient) => { export const deleteGroup = (queryClient: QueryClient) => { return { - mutationFn: API.deleteGroup, + mutationFn: client.api.deleteGroup, onSuccess: async (_: void, groupId: string) => invalidateGroup(queryClient, groupId), }; @@ -122,7 +121,7 @@ export const deleteGroup = (queryClient: QueryClient) => { export const addMember = (queryClient: QueryClient) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => - API.addMember(groupId, userId), + client.api.addMember(groupId, userId), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; @@ -131,7 +130,7 @@ export const addMember = (queryClient: QueryClient) => { export const removeMember = (queryClient: QueryClient) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => - API.removeMember(groupId, userId), + client.api.removeMember(groupId, userId), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; diff --git a/site/src/api/queries/insights.ts b/site/src/api/queries/insights.ts index 7d60565e83bb0..7e2f5bd5b9c0a 100644 --- a/site/src/api/queries/insights.ts +++ b/site/src/api/queries/insights.ts @@ -1,22 +1,26 @@ -import * as API from "api/api"; +import { + type InsightsParams, + type InsightsTemplateParams, + client, +} 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), + queryFn: () => client.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), + queryFn: () => client.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), + queryFn: () => client.api.getInsightsUserActivity(params), }; }; diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts index de43a4c8f4cac..0e26194b80b22 100644 --- a/site/src/api/queries/integrations.ts +++ b/site/src/api/queries/integrations.ts @@ -1,9 +1,9 @@ import type { GetJFrogXRayScanParams } from "api/api"; -import * as API from "api/api"; +import { client } from "api/api"; export const xrayScan = (params: GetJFrogXRayScanParams) => { return { queryKey: ["xray", params], - queryFn: () => API.getJFrogXRayScan(params), + queryFn: () => client.api.getJFrogXRayScan(params), }; }; diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts index 78b31762b2aa5..06f8f9d9b4097 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 { client } from "api/api"; import type * as TypesGen from "api/typesGenerated"; const appsKey = ["oauth2-provider", "apps"]; @@ -10,20 +10,20 @@ const appSecretsKey = (appId: string) => appKey(appId).concat("secrets"); export const getApps = (userId?: string) => { return { queryKey: userId ? appsKey.concat(userId) : appsKey, - queryFn: () => API.getOAuth2ProviderApps({ user_id: userId }), + queryFn: () => client.api.getOAuth2ProviderApps({ user_id: userId }), }; }; export const getApp = (id: string) => { return { queryKey: appKey(id), - queryFn: () => API.getOAuth2ProviderApp(id), + queryFn: () => client.api.getOAuth2ProviderApp(id), }; }; export const postApp = (queryClient: QueryClient) => { return { - mutationFn: API.postOAuth2ProviderApp, + mutationFn: client.api.postOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: appsKey, @@ -40,7 +40,7 @@ export const putApp = (queryClient: QueryClient) => { }: { id: string; req: TypesGen.PutOAuth2ProviderAppRequest; - }) => API.putOAuth2ProviderApp(id, req), + }) => client.api.putOAuth2ProviderApp(id, req), onSuccess: async (app: TypesGen.OAuth2ProviderApp) => { await queryClient.invalidateQueries({ queryKey: appKey(app.id), @@ -51,7 +51,7 @@ export const putApp = (queryClient: QueryClient) => { export const deleteApp = (queryClient: QueryClient) => { return { - mutationFn: API.deleteOAuth2ProviderApp, + mutationFn: client.api.deleteOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: appsKey, @@ -63,13 +63,13 @@ export const deleteApp = (queryClient: QueryClient) => { export const getAppSecrets = (id: string) => { return { queryKey: appSecretsKey(id), - queryFn: () => API.getOAuth2ProviderAppSecrets(id), + queryFn: () => client.api.getOAuth2ProviderAppSecrets(id), }; }; export const postAppSecret = (queryClient: QueryClient) => { return { - mutationFn: API.postOAuth2ProviderAppSecret, + mutationFn: client.api.postOAuth2ProviderAppSecret, onSuccess: async ( _: TypesGen.OAuth2ProviderAppSecretFull, appId: string, @@ -84,7 +84,7 @@ export const postAppSecret = (queryClient: QueryClient) => { export const deleteAppSecret = (queryClient: QueryClient) => { return { mutationFn: ({ appId, secretId }: { appId: string; secretId: string }) => - API.deleteOAuth2ProviderAppSecret(appId, secretId), + client.api.deleteOAuth2ProviderAppSecret(appId, secretId), onSuccess: async (_: void, { appId }: { appId: string }) => { await queryClient.invalidateQueries({ queryKey: appSecretsKey(appId), @@ -95,7 +95,7 @@ export const deleteAppSecret = (queryClient: QueryClient) => { export const revokeApp = (queryClient: QueryClient, userId: string) => { return { - mutationFn: API.revokeOAuth2ProviderApp, + mutationFn: client.api.revokeOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: userAppsKey(userId), diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 37b2af49f3e74..7bfe89162316b 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -1,8 +1,8 @@ -import * as API from "api/api"; +import { client } from "api/api"; export const roles = () => { return { queryKey: ["roles"], - queryFn: API.getRoles, + queryFn: client.api.getRoles, }; }; diff --git a/site/src/api/queries/settings.ts b/site/src/api/queries/settings.ts index 4a086cf18532c..bd4ef7f944dce 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 { client } from "api/api"; import type { UpdateUserQuietHoursScheduleRequest, UserQuietHoursScheduleResponse, @@ -16,7 +16,7 @@ export const userQuietHoursSchedule = ( ): QueryOptions<UserQuietHoursScheduleResponse> => { return { queryKey: userQuietHoursScheduleKey(userId), - queryFn: () => API.getUserQuietHoursSchedule(userId), + queryFn: () => client.api.getUserQuietHoursSchedule(userId), }; }; @@ -26,7 +26,7 @@ export const updateUserQuietHoursSchedule = ( ) => { return { mutationFn: (request: UpdateUserQuietHoursScheduleRequest) => - API.updateUserQuietHoursSchedule(userId, request), + client.api.updateUserQuietHoursSchedule(userId, request), onSuccess: async () => { await queryClient.invalidateQueries(userQuietHoursScheduleKey(userId)); }, diff --git a/site/src/api/queries/sshKeys.ts b/site/src/api/queries/sshKeys.ts index 6fc3593c318c7..878a80523863e 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 { client } from "api/api"; import type { GitSSHKey } from "api/typesGenerated"; const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"]; @@ -7,7 +7,7 @@ const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"]; export const userSSHKey = (userId: string) => { return { queryKey: getUserSSHKeyQueryKey(userId), - queryFn: () => API.getUserSSHKey(userId), + queryFn: () => client.api.getUserSSHKey(userId), }; }; @@ -16,7 +16,7 @@ export const regenerateUserSSHKey = ( queryClient: QueryClient, ) => { return { - mutationFn: () => API.regenerateUserSSHKey(userId), + mutationFn: () => client.api.regenerateUserSSHKey(userId), onSuccess: (newKey: GitSSHKey) => { queryClient.setQueryData(getUserSSHKeyQueryKey(userId), newKey); }, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 83879415bacf6..7e485a0e039b4 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 { client } from "api/api"; import type { CreateTemplateRequest, CreateTemplateVersionRequest, @@ -26,7 +26,7 @@ export const templateByName = ( ): QueryOptions<Template> => { return { queryKey: templateByNameKey(organizationId, name), - queryFn: async () => API.getTemplateByName(organizationId, name), + queryFn: async () => client.api.getTemplateByName(organizationId, name), }; }; @@ -39,27 +39,27 @@ const getTemplatesQueryKey = (organizationId: string, deprecated?: boolean) => [ export const templates = (organizationId: string, deprecated?: boolean) => { return { queryKey: getTemplatesQueryKey(organizationId, deprecated), - queryFn: () => API.getTemplates(organizationId, { deprecated }), + queryFn: () => client.api.getTemplates(organizationId, { deprecated }), }; }; export const templateACL = (templateId: string) => { return { queryKey: ["templateAcl", templateId], - queryFn: () => API.getTemplateACL(templateId), + queryFn: () => client.api.getTemplateACL(templateId), }; }; export const setUserRole = ( queryClient: QueryClient, ): MutationOptions< - Awaited<ReturnType<typeof API.updateTemplateACL>>, + Awaited<ReturnType<typeof client.api.updateTemplateACL>>, unknown, { templateId: string; userId: string; role: TemplateRole } > => { return { mutationFn: ({ templateId, userId, role }) => - API.updateTemplateACL(templateId, { + client.api.updateTemplateACL(templateId, { user_perms: { [userId]: role, }, @@ -73,13 +73,13 @@ export const setUserRole = ( export const setGroupRole = ( queryClient: QueryClient, ): MutationOptions< - Awaited<ReturnType<typeof API.updateTemplateACL>>, + Awaited<ReturnType<typeof client.api.updateTemplateACL>>, unknown, { templateId: string; groupId: string; role: TemplateRole } > => { return { mutationFn: ({ templateId, groupId, role }) => - API.updateTemplateACL(templateId, { + client.api.updateTemplateACL(templateId, { group_perms: { [groupId]: role, }, @@ -93,14 +93,14 @@ export const setGroupRole = ( export const templateExamples = (organizationId: string) => { return { queryKey: [...getTemplatesQueryKey(organizationId), "examples"], - queryFn: () => API.getTemplateExamples(organizationId), + queryFn: () => client.api.getTemplateExamples(organizationId), }; }; export const templateVersion = (versionId: string) => { return { queryKey: ["templateVersion", versionId], - queryFn: () => API.getTemplateVersion(versionId), + queryFn: () => client.api.getTemplateVersion(versionId), }; }; @@ -112,14 +112,18 @@ export const templateVersionByName = ( return { queryKey: ["templateVersion", organizationId, templateName, versionName], queryFn: () => - API.getTemplateVersionByName(organizationId, templateName, versionName), + client.api.getTemplateVersionByName( + organizationId, + templateName, + versionName, + ), }; }; export const templateVersions = (templateId: string) => { return { queryKey: ["templateVersions", templateId], - queryFn: () => API.getTemplateVersions(templateId), + queryFn: () => client.api.getTemplateVersions(templateId), }; }; @@ -132,14 +136,14 @@ export const templateVersionVariablesKey = (versionId: string) => [ export const templateVersionVariables = (versionId: string) => { return { queryKey: templateVersionVariablesKey(versionId), - queryFn: () => API.getTemplateVersionVariables(versionId), + queryFn: () => client.api.getTemplateVersionVariables(versionId), }; }; export const createTemplateVersion = (organizationId: string) => { return { mutationFn: async (request: CreateTemplateVersionRequest) => { - const newVersion = await API.createTemplateVersion( + const newVersion = await client.api.createTemplateVersion( organizationId, request, ); @@ -151,7 +155,7 @@ export const createTemplateVersion = (organizationId: string) => { export const createAndBuildTemplateVersion = (organizationId: string) => { return { mutationFn: async (request: CreateTemplateVersionRequest) => { - const newVersion = await API.createTemplateVersion( + const newVersion = await client.api.createTemplateVersion( organizationId, request, ); @@ -167,7 +171,7 @@ export const updateActiveTemplateVersion = ( ) => { return { mutationFn: (versionId: string) => - API.updateActiveTemplateVersion(template.id, { + client.api.updateActiveTemplateVersion(template.id, { id: versionId, }), onSuccess: async () => { @@ -185,7 +189,7 @@ export const templaceACLAvailable = ( ) => { return { queryKey: ["template", templateId, "aclAvailable", options], - queryFn: () => API.getTemplateACLAvailable(templateId, options), + queryFn: () => client.api.getTemplateACLAvailable(templateId, options), }; }; @@ -198,7 +202,7 @@ export const templateVersionExternalAuthKey = (versionId: string) => [ export const templateVersionExternalAuth = (versionId: string) => { return { queryKey: templateVersionExternalAuthKey(versionId), - queryFn: () => API.getTemplateVersionExternalAuth(versionId), + queryFn: () => client.api.getTemplateVersionExternalAuth(versionId), }; }; @@ -217,13 +221,13 @@ export type CreateTemplateOptions = { }; const createTemplateFn = async (options: CreateTemplateOptions) => { - const version = await API.createTemplateVersion( + const version = await client.api.createTemplateVersion( options.organizationId, options.version, ); options.onCreateVersion?.(version); await waitBuildToBeFinished(version, options.onTemplateVersionChanges); - return API.createTemplate(options.organizationId, { + return client.api.createTemplate(options.organizationId, { ...options.template, template_version_id: version.id, }); @@ -232,21 +236,21 @@ const createTemplateFn = async (options: CreateTemplateOptions) => { export const templateVersionLogs = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "logs"], - queryFn: () => API.getTemplateVersionLogs(versionId), + queryFn: () => client.api.getTemplateVersionLogs(versionId), }; }; export const richParameters = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "richParameters"], - queryFn: () => API.getTemplateVersionRichParameters(versionId), + queryFn: () => client.api.getTemplateVersionRichParameters(versionId), }; }; export const resources = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "resources"], - queryFn: () => API.getTemplateVersionResources(versionId), + queryFn: () => client.api.getTemplateVersionResources(versionId), }; }; @@ -254,7 +258,7 @@ export const templateFiles = (fileId: string) => { return { queryKey: ["templateFiles", fileId], queryFn: async () => { - const tarFile = await API.getFile(fileId); + const tarFile = await client.api.getFile(fileId); return getTemplateVersionFiles(tarFile); }, }; @@ -274,7 +278,7 @@ export const previousTemplateVersion = ( "previous", ], queryFn: async () => { - const result = await API.getPreviousTemplateVersionByName( + const result = await client.api.getPreviousTemplateVersionByName( organizationId, templateName, versionName, @@ -294,7 +298,7 @@ const waitBuildToBeFinished = async ( do { // When pending we want to poll more frequently await delay(jobStatus === "pending" ? 250 : 1000); - data = await API.getTemplateVersion(version.id); + data = await client.api.getTemplateVersion(version.id); onRequest?.(data); jobStatus = data.job.status; diff --git a/site/src/api/queries/updateCheck.ts b/site/src/api/queries/updateCheck.ts index 40fcc6a3cfdde..6c7a2add42ef4 100644 --- a/site/src/api/queries/updateCheck.ts +++ b/site/src/api/queries/updateCheck.ts @@ -1,8 +1,8 @@ -import * as API from "api/api"; +import { client } from "api/api"; export const updateCheck = () => { return { queryKey: ["updateCheck"], - queryFn: () => API.getUpdateCheck(), + queryFn: () => client.api.getUpdateCheck(), }; }; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 3a1806276146b..bee3cae979aeb 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -4,7 +4,7 @@ import type { UseMutationOptions, UseQueryOptions, } from "react-query"; -import * as API from "api/api"; +import { client } from "api/api"; import type { AuthorizationRequest, GetUsersResponse, @@ -39,14 +39,14 @@ export function paginatedUsers( }, queryKey: ({ payload }) => usersKey(payload), - queryFn: ({ payload, signal }) => API.getUsers(payload, signal), + queryFn: ({ payload, signal }) => client.api.getUsers(payload, signal), }; } export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => { return { queryKey: usersKey(req), - queryFn: ({ signal }) => API.getUsers(req, signal), + queryFn: ({ signal }) => client.api.getUsers(req, signal), cacheTime: 5 * 1000 * 60, }; }; @@ -57,13 +57,13 @@ export const updatePassword = () => { userId, ...request }: UpdateUserPasswordRequest & { userId: string }) => - API.updateUserPassword(userId, request), + client.api.updateUserPassword(userId, request), }; }; export const createUser = (queryClient: QueryClient) => { return { - mutationFn: API.createUser, + mutationFn: client.api.createUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -72,13 +72,13 @@ export const createUser = (queryClient: QueryClient) => { export const createFirstUser = () => { return { - mutationFn: API.createFirstUser, + mutationFn: client.api.createFirstUser, }; }; export const suspendUser = (queryClient: QueryClient) => { return { - mutationFn: API.suspendUser, + mutationFn: client.api.suspendUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -87,7 +87,7 @@ export const suspendUser = (queryClient: QueryClient) => { export const activateUser = (queryClient: QueryClient) => { return { - mutationFn: API.activateUser, + mutationFn: client.api.activateUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -96,7 +96,7 @@ export const activateUser = (queryClient: QueryClient) => { export const deleteUser = (queryClient: QueryClient) => { return { - mutationFn: API.deleteUser, + mutationFn: client.api.deleteUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -106,7 +106,7 @@ export const deleteUser = (queryClient: QueryClient) => { export const updateRoles = (queryClient: QueryClient) => { return { mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) => - API.updateUserRoles(roles, userId), + client.api.updateUserRoles(roles, userId), onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -120,7 +120,7 @@ export const authMethods = () => { // Even the endpoint being /users/authmethods we don't want to revalidate it // when users change so its better add a unique query key queryKey: ["authMethods"], - queryFn: API.getAuthMethods, + queryFn: client.api.getAuthMethods, }; }; @@ -132,14 +132,14 @@ export const me = (): UseQueryOptions<User> & { return cachedQuery({ initialData: initialUserData, queryKey: meKey, - queryFn: API.getAuthenticatedUser, + queryFn: client.api.getAuthenticatedUser, }); }; export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> { return { queryKey: [...meKey, "apiKey"], - queryFn: () => API.getApiKey(), + queryFn: () => client.api.getApiKey(), }; } @@ -148,7 +148,7 @@ export const hasFirstUser = (): UseQueryOptions<boolean> => { // This cannot be false otherwise it will not fetch! initialData: Boolean(initialUserData) || undefined, queryKey: ["hasFirstUser"], - queryFn: API.hasFirstUser, + queryFn: client.api.hasFirstUser, }); }; @@ -178,10 +178,10 @@ const loginFn = async ({ password: string; authorization: AuthorizationRequest; }) => { - await API.login(email, password); + await client.api.login(email, password); const [user, permissions] = await Promise.all([ - API.getAuthenticatedUser(), - API.checkAuthorization(authorization), + client.api.getAuthenticatedUser(), + client.api.checkAuthorization(authorization), ]); return { user, @@ -191,7 +191,7 @@ const loginFn = async ({ export const logout = (queryClient: QueryClient) => { return { - mutationFn: API.logout, + mutationFn: client.api.logout, onSuccess: () => { queryClient.removeQueries(); }, @@ -201,7 +201,7 @@ export const logout = (queryClient: QueryClient) => { export const updateProfile = (userId: string) => { return { mutationFn: (req: UpdateUserProfileRequest) => - API.updateProfile(userId, req), + client.api.updateProfile(userId, req), }; }; @@ -215,7 +215,7 @@ export const updateAppearanceSettings = ( unknown > => { return { - mutationFn: (req) => API.updateAppearanceSettings(userId, req), + mutationFn: (req) => client.api.updateAppearanceSettings(userId, req), onMutate: async (patch) => { // Mutate the `queryClient` optimistically to make the theme switcher // more responsive. diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 8960068b6169c..d4cc127c6814e 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 { client } from "api/api"; import type { WorkspaceBuild, WorkspaceBuildParameter, @@ -13,7 +13,7 @@ export function workspaceBuildParametersKey(workspaceBuildId: string) { export function workspaceBuildParameters(workspaceBuildId: string) { return { queryKey: workspaceBuildParametersKey(workspaceBuildId), - queryFn: () => API.getWorkspaceBuildParameters(workspaceBuildId), + queryFn: () => client.api.getWorkspaceBuildParameters(workspaceBuildId), } as const satisfies QueryOptions<WorkspaceBuildParameter[]>; } @@ -25,7 +25,11 @@ export const workspaceBuildByNumber = ( return { queryKey: ["workspaceBuild", username, workspaceName, buildNumber], queryFn: () => - API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber), + client.api.getWorkspaceBuildByNumber( + username, + workspaceName, + buildNumber, + ), }; }; @@ -49,7 +53,7 @@ export const infiniteWorkspaceBuilds = ( return pages.length + 1; }, queryFn: ({ pageParam = 0 }) => { - return API.getWorkspaceBuilds(workspaceId, { + return client.api.getWorkspaceBuilds(workspaceId, { limit, offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit, }); diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index f43adf616688e..ec27415b7ce2e 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 { client } from "api/api"; export const getWorkspaceQuotaQueryKey = (username: string) => [ username, @@ -8,7 +8,7 @@ export const getWorkspaceQuotaQueryKey = (username: string) => [ export const workspaceQuota = (username: string) => { return { queryKey: getWorkspaceQuotaQueryKey(username), - queryFn: () => API.getWorkspaceQuota(username), + queryFn: () => client.api.getWorkspaceQuota(username), }; }; @@ -20,6 +20,6 @@ export const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [ export const workspaceResolveAutostart = (workspaceId: string) => { return { queryKey: getWorkspaceResolveAutostartQueryKey(workspaceId), - queryFn: () => API.getWorkspaceResolveAutostart(workspaceId), + queryFn: () => client.api.getWorkspaceResolveAutostart(workspaceId), }; }; diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts index 9e341d551a4f3..fd37af90fa926 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 { client } 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: () => client.api.getWorkspaceAgentSharedPorts(workspaceId), }; }; export const upsertWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { - await upsertWorkspaceAgentSharedPort(workspaceId, options); + await client.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 client.api.deleteWorkspaceAgentSharedPort(workspaceId, options); }, }; }; diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 816cc5613e99d..da68598cc6358 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, client } 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 }), + client.api.getWorkspaceByOwnerAndName(owner, name, { + include_deleted: true, + }), }; }; @@ -49,7 +50,7 @@ export const createWorkspace = (queryClient: QueryClient) => { return { mutationFn: async (variables: CreateWorkspaceMutationVariables) => { const { userId, organizationId, ...req } = variables; - return API.createWorkspace(organizationId, userId, req); + return client.api.createWorkspace(organizationId, userId, req); }, onSuccess: async () => { await queryClient.invalidateQueries(["workspaces"]); @@ -71,14 +72,14 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => { if (versionId) { templateVersionParameters = { template_version_id: versionId }; } else { - const template = await API.getTemplateByName( + const template = await client.api.getTemplateByName( organizationId, templateName, ); templateVersionParameters = { template_id: template.id }; } - return API.createWorkspace(organizationId, "me", { + return client.api.createWorkspace(organizationId, "me", { ...templateVersionParameters, name: defaultName, rich_parameter_values: defaultBuildParameters, @@ -102,7 +103,7 @@ export function workspaces(config: WorkspacesRequest = {}) { return { queryKey: workspacesKey(config), - queryFn: () => API.getWorkspaces({ q, limit }), + queryFn: () => client.api.getWorkspaces({ q, limit }), } as const satisfies QueryOptions<WorkspacesResponse>; } @@ -111,7 +112,7 @@ export const updateDeadline = ( ): UseMutationOptions<void, unknown, Dayjs> => { return { mutationFn: (deadline: Dayjs) => { - return putWorkspaceExtension(workspace.id, deadline); + return client.api.putWorkspaceExtension(workspace.id, deadline); }, }; }; @@ -128,7 +129,11 @@ export const changeVersion = ( versionId: string; buildParameters?: WorkspaceBuildParameter[]; }) => { - return API.changeWorkspaceVersion(workspace, versionId, buildParameters); + return client.api.changeWorkspaceVersion( + workspace, + versionId, + buildParameters, + ); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -142,7 +147,7 @@ export const updateWorkspace = ( ) => { return { mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { - return API.updateWorkspace(workspace, buildParameters); + return client.api.updateWorkspace(workspace, buildParameters); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -155,8 +160,8 @@ export const deleteWorkspace = ( queryClient: QueryClient, ) => { return { - mutationFn: (options: API.DeleteWorkspaceOptions) => { - return API.deleteWorkspace(workspace.id, options); + mutationFn: (options: DeleteWorkspaceOptions) => { + return client.api.deleteWorkspace(workspace.id, options); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -170,7 +175,7 @@ export const stopWorkspace = ( ) => { return { mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { - return API.stopWorkspace(workspace.id, logLevel); + return client.api.stopWorkspace(workspace.id, logLevel); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -190,7 +195,7 @@ export const startWorkspace = ( buildParameters?: WorkspaceBuildParameter[]; logLevel?: ProvisionerLogLevel; }) => { - return API.startWorkspace( + return client.api.startWorkspace( workspace.id, workspace.latest_build.template_version_id, logLevel, @@ -206,7 +211,7 @@ export const startWorkspace = ( export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { - return API.cancelWorkspaceBuild(workspace.latest_build.id); + return client.api.cancelWorkspaceBuild(workspace.latest_build.id); }, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -219,7 +224,7 @@ export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { export const activate = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { - return API.updateWorkspaceDormancy(workspace.id, false); + return client.api.updateWorkspaceDormancy(workspace.id, false); }, onSuccess: (updatedWorkspace: Workspace) => { queryClient.setQueryData( @@ -263,9 +268,9 @@ export const toggleFavorite = ( return { mutationFn: () => { if (workspace.favorite) { - return API.deleteFavoriteWorkspace(workspace.id); + return client.api.deleteFavoriteWorkspace(workspace.id); } else { - return API.putFavoriteWorkspace(workspace.id); + return client.api.putFavoriteWorkspace(workspace.id); } }, onSuccess: async () => { diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 74bbf91376a12..fe44940880380 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 { client } 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 client.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 client.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 07e1486dd12e6..fb1d3256e9fd2 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 { client } from "api/api"; import { cachedQuery } from "api/queries/util"; import type { Region, WorkspaceProxy } from "api/typesGenerated"; import { useAuthenticated } from "contexts/auth/RequireAuth"; @@ -120,8 +120,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => { const { permissions } = useAuthenticated(); const query = async (): Promise<readonly Region[]> => { const endpoint = permissions.editWorkspaceProxies - ? getWorkspaceProxies - : getWorkspaceProxyRegions; + ? client.api.getWorkspaceProxies + : client.api.getWorkspaceProxyRegions; const resp = await endpoint(); return resp.regions; }; diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx index d8db9071cc940..d820b267f55b5 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 { client } from "api/api"; import { isApiError } from "api/errors"; import { Loader } from "components/Loader/Loader"; import { ProxyProvider } from "contexts/ProxyContext"; @@ -22,6 +22,7 @@ export const RequireAuth: FC = () => { return; } + const axiosInstance = client.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..fb3d03d0269e4 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 { client } 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 = client.getAxiosInstance(); const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => { return axiosInstance.get(latencyURL, { withCredentials: false, diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 7243d2b1af5b6..bfbc03f4a9a91 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 { client } 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<AppLinkProps> = ({ app, workspace, agent }) => { let url = href; if (hasMagicToken !== -1) { setFetchingSessionToken(true); - const key = await getApiKey(); + const key = await client.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 1e4e2c2f59b82..8439a5c431c8b 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 { client } from "api/api"; import { deleteWorkspacePortShare, upsertWorkspacePortShare, @@ -70,7 +70,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => { const portsQuery = useQuery({ queryKey: ["portForward", agent.id], - queryFn: () => getAgentListeningPorts(agent.id), + queryFn: () => client.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..36b6f5822f804 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 { client } from "api/api"; import type { DisplayApp } from "api/typesGenerated"; import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; @@ -119,7 +119,8 @@ const VSCodeButton: FC<VSCodeDesktopButtonProps> = ({ disabled={loading} onClick={() => { setLoading(true); - getApiKey() + client.api + .getApiKey() .then(({ key }) => { const query = new URLSearchParams({ owner: userName, @@ -163,7 +164,8 @@ const VSCodeInsidersButton: FC<VSCodeDesktopButtonProps> = ({ disabled={loading} onClick={() => { setLoading(true); - getApiKey() + client.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..ea5e8b438606f 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 { client } from "api/api"; import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; import { MockAuditLog, @@ -61,10 +61,12 @@ describe("AuditPage", () => { it("renders page 5", async () => { // Given const page = 5; - const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs").mockResolvedValue({ - audit_logs: [MockAuditLog, MockAuditLog2], - count: 2, - }); + const getAuditLogsSpy = jest + .spyOn(client.api, "getAuditLogs") + .mockResolvedValue({ + audit_logs: [MockAuditLog, MockAuditLog2], + count: 2, + }); // When await renderPage({ page: page }); @@ -82,7 +84,7 @@ describe("AuditPage", () => { describe("Filtering", () => { it("filters by URL", async () => { const getAuditLogsSpy = jest - .spyOn(API, "getAuditLogs") + .spyOn(client.api, "getAuditLogs") .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 }); const query = "resource_type:workspace action:create"; @@ -98,7 +100,7 @@ describe("AuditPage", () => { it("resets page to 1 when filter is changed", async () => { await renderPage({ page: 2 }); - const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs"); + const getAuditLogsSpy = jest.spyOn(client.api, "getAuditLogs"); getAuditLogsSpy.mockClear(); const filterField = screen.getByLabelText("Filter"); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx index 38eacc38fd30e..1c28b4a1e3afb 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 { client } from "api/api"; import { MockTemplateExample, MockTemplateVersion, @@ -35,14 +35,14 @@ test("Create template from starter template", async () => { const { router, container } = await renderPage(searchParams); const form = container.querySelector("form") as HTMLFormElement; - jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({ + jest.spyOn(client.api, "createTemplateVersion").mockResolvedValueOnce({ ...MockTemplateVersion, job: { ...MockTemplateVersion.job, status: "pending", }, }); - jest.spyOn(API, "getTemplateVersion").mockResolvedValue({ + jest.spyOn(client.api, "getTemplateVersion").mockResolvedValue({ ...MockTemplateVersion, job: { ...MockTemplateVersion.job, @@ -51,7 +51,7 @@ test("Create template from starter template", async () => { }, }); jest - .spyOn(API, "getTemplateVersionVariables") + .spyOn(client.api, "getTemplateVersionVariables") .mockResolvedValue([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, @@ -85,35 +85,42 @@ test("Create template from starter template", async () => { // Setup the mock for the second template version creation before submit the form jest.clearAllMocks(); jest - .spyOn(API, "createTemplateVersion") + .spyOn(client.api, "createTemplateVersion") .mockResolvedValue(MockTemplateVersion); - jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); - jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); + jest + .spyOn(client.api, "getTemplateVersion") + .mockResolvedValue(MockTemplateVersion); + jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( within(form).getByRole("button", { name: /create template/i }), ); - await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1)); + await waitFor(() => expect(client.api.createTemplate).toBeCalledTimes(1)); expect(router.state.location.pathname).toEqual( `/templates/${MockTemplate.name}/files`, ); - expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, { - example_id: "aws-windows", - provisioner: "terraform", - storage_method: "file", - tags: {}, - user_variable_values: [ - { name: "first_variable", value: "First value" }, - { name: "second_variable", value: "2" }, - { name: "third_variable", value: "true" }, - ], - }); + expect(client.api.createTemplateVersion).toHaveBeenCalledWith( + MockOrganization.id, + { + example_id: "aws-windows", + provisioner: "terraform", + storage_method: "file", + tags: {}, + user_variable_values: [ + { name: "first_variable", value: "First value" }, + { name: "second_variable", value: "2" }, + { name: "third_variable", value: "true" }, + ], + }, + ); }); test("Create template from duplicating a template", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); - jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); jest - .spyOn(API, "getTemplateVersionVariables") + .spyOn(client.api, "getTemplateVersion") + .mockResolvedValue(MockTemplateVersion); + jest + .spyOn(client.api, "getTemplateVersionVariables") .mockResolvedValue([MockTemplateVersionVariable1]); const searchParams = new URLSearchParams({ @@ -135,10 +142,12 @@ test("Create template from duplicating a template", async () => { ).toHaveValue(MockTemplateVersionVariable1.value); // Create template jest - .spyOn(API, "createTemplateVersion") + .spyOn(client.api, "createTemplateVersion") + .mockResolvedValue(MockTemplateVersion); + jest + .spyOn(client.api, "getTemplateVersion") .mockResolvedValue(MockTemplateVersion); - jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); - jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( screen.getByRole("button", { name: /create template/i }), ); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx index f58eed5d1e6b2..20f74d4c4dd79 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 { client } from "api/api"; import { renderWithAuth, waitForLoaderToBeRemoved, @@ -9,7 +9,7 @@ import { CreateTokenPage } from "./CreateTokenPage"; describe("TokenPage", () => { it("shows the success modal", async () => { - jest.spyOn(API, "createToken").mockResolvedValueOnce({ + jest.spyOn(client.api, "createToken").mockResolvedValueOnce({ key: "abcd", }); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx index 4ea1f98144671..a5883041b9299 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 { client } 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(client.api.createToken); const { data: tokenConfig, isLoading: fetchingTokenConfig, @@ -36,7 +36,7 @@ export const CreateTokenPage: FC = () => { error: tokenFetchError, } = useQuery({ queryKey: ["tokenconfig"], - queryFn: getTokenConfig, + queryFn: client.api.getTokenConfig, }); const [formError, setFormError] = useState<unknown>(undefined); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 85ffe8ea45896..47c29e4ba91b7 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 { client } from "api/api"; import { MockTemplate, MockUser, @@ -36,14 +36,16 @@ const renderCreateWorkspacePage = () => { describe("CreateWorkspacePage", () => { it("succeeds with default owner", async () => { jest - .spyOn(API, "getUsers") + .spyOn(client.api, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); jest - .spyOn(API, "getWorkspaceQuota") + .spyOn(client.api, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); - jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(API, "getTemplateVersionRichParameters") + .spyOn(client.api, "createWorkspace") + .mockResolvedValueOnce(MockWorkspace); + jest + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]); renderCreateWorkspacePage(); @@ -59,7 +61,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(API.createWorkspace).toBeCalledWith( + expect(client.api.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -73,7 +75,7 @@ describe("CreateWorkspacePage", () => { const param = "first_parameter"; const paramValue = "It works!"; jest - .spyOn(API, "getTemplateVersionRichParameters") + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]); renderWithAuth(<CreateWorkspacePage />, { @@ -89,7 +91,7 @@ describe("CreateWorkspacePage", () => { it("rich parameter: number validation fails", async () => { jest - .spyOn(API, "getTemplateVersionRichParameters") + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter2, @@ -124,7 +126,7 @@ describe("CreateWorkspacePage", () => { it("rich parameter: string validation fails", async () => { jest - .spyOn(API, "getTemplateVersionRichParameters") + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter3, @@ -157,14 +159,16 @@ describe("CreateWorkspacePage", () => { }); it("rich parameter: number validation fails with custom error", async () => { - jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ - MockTemplateVersionParameter1, - { - ...MockTemplateVersionParameter2, - validation_error: "These are values: {min}, {max}, and {value}.", - validation_monotonic: undefined, // only needs min-max rules - }, - ]); + jest + .spyOn(client.api, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + { + ...MockTemplateVersionParameter2, + validation_error: "These are values: {min}, {max}, and {value}.", + validation_monotonic: undefined, // only needs min-max rules + }, + ]); renderCreateWorkspacePage(); await waitForLoaderToBeRemoved(); @@ -187,14 +191,16 @@ describe("CreateWorkspacePage", () => { it("external auth authenticates and succeeds", async () => { jest - .spyOn(API, "getWorkspaceQuota") + .spyOn(client.api, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); jest - .spyOn(API, "getUsers") + .spyOn(client.api, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); - jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(API, "getTemplateVersionExternalAuth") + .spyOn(client.api, "createWorkspace") + .mockResolvedValueOnce(MockWorkspace); + jest + .spyOn(client.api, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); renderCreateWorkspacePage(); @@ -210,7 +216,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(githubButton); jest - .spyOn(API, "getTemplateVersionExternalAuth") + .spyOn(client.api, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithubAuthenticated]); await screen.findByText( @@ -223,7 +229,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(API.createWorkspace).toBeCalledWith( + expect(client.api.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -235,14 +241,16 @@ describe("CreateWorkspacePage", () => { it("optional external auth is optional", async () => { jest - .spyOn(API, "getWorkspaceQuota") + .spyOn(client.api, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); jest - .spyOn(API, "getUsers") + .spyOn(client.api, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); - jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(API, "getTemplateVersionExternalAuth") + .spyOn(client.api, "createWorkspace") + .mockResolvedValueOnce(MockWorkspace); + jest + .spyOn(client.api, "getTemplateVersionExternalAuth") .mockResolvedValue([ { ...MockTemplateVersionExternalAuthGithub, optional: true }, ]); @@ -263,7 +271,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(API.createWorkspace).toBeCalledWith( + expect(client.api.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -276,7 +284,7 @@ describe("CreateWorkspacePage", () => { it("auto create a workspace if uses mode=auto", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); renderWithAuth(<CreateWorkspacePage />, { route: @@ -307,10 +315,10 @@ describe("CreateWorkspacePage", () => { it("disables mode=auto if a required external auth provider is not connected", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); const externalAuthSpy = jest - .spyOn(API, "getTemplateVersionExternalAuth") + .spyOn(client.api, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); renderWithAuth(<CreateWorkspacePage />, { @@ -336,7 +344,7 @@ describe("CreateWorkspacePage", () => { it("auto create a workspace if uses mode=auto and version=version-id", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); renderWithAuth(<CreateWorkspacePage />, { route: diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index b885b11d32f6c..48dd90aa3232d 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 { client } 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: () => client.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..f470446e4655e 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 { client } 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(client.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 8f172cf1155a1..238d861511a41 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 { client } from "api/api"; import { getErrorMessage } from "api/errors"; import { entitlements, refreshEntitlements } from "api/queries/entitlements"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; @@ -32,7 +32,7 @@ const LicensesSettingsPage: FC = () => { }, [entitlementsQuery.error]); const { mutate: removeLicenseApi, isLoading: isRemovingLicense } = - useMutation(removeLicense, { + useMutation(client.api.removeLicense, { onSuccess: () => { displaySuccess("Successfully removed license"); void queryClient.invalidateQueries(["licenses"]); @@ -44,7 +44,7 @@ const LicensesSettingsPage: FC = () => { const { data: licenses, isLoading } = useQuery({ queryKey: ["licenses"], - queryFn: () => getLicenses(), + queryFn: () => client.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..e1afd45f64531 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 { client } from "api/api"; import { TemplateLayout } from "pages/TemplatePage/TemplateLayout"; import { MockTemplate, @@ -15,7 +15,7 @@ import TemplateEmbedPage from "./TemplateEmbedPage"; test("Users can fill the parameters and copy the open in coder url", async () => { jest - .spyOn(API, "getTemplateVersionRichParameters") + .spyOn(client.api, "getTemplateVersionRichParameters") .mockResolvedValue([parameter1, parameter2]); renderWithAuth( diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index 57716e0b91fe5..6263126eda7b7 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 { client } 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: () => + client.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..23c663e32ba1c 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 { client } from "api/api"; import type { AuthorizationRequest } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; @@ -39,10 +35,14 @@ const templatePermissions = ( }); const fetchTemplate = async (organizationId: string, templateName: string) => { - const template = await getTemplateByName(organizationId, templateName); + const template = await client.api.getTemplateByName( + organizationId, + templateName, + ); + const [activeVersion, permissions] = await Promise.all([ - getTemplateVersion(template.active_version_id), - checkAuthorization({ + client.api.getTemplateVersion(template.active_version_id), + client.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..fbd1ff993c802 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 { client } 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: () => client.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..d08de7669e505 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 { client } 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: () => client.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 client.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 client.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..5f8421f2d6584 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 { client } from "api/api"; import { MockTemplate } from "testHelpers/entities"; import { useDeletionDialogState } from "./useDeletionDialogState"; @@ -23,9 +23,9 @@ test("confirm template deletion", async () => { expect(result.current.isDeleteDialogOpen).toBeTruthy(); // Confirm delete - jest.spyOn(API, "deleteTemplate"); + jest.spyOn(client.api, "deleteTemplate"); await act(async () => result.current.confirmDelete()); - await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1)); + await waitFor(() => expect(client.api.deleteTemplate).toBeCalledTimes(1)); expect(onDeleteTemplate).toBeCalledTimes(1); }); diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.ts b/site/src/pages/TemplatePage/useDeletionDialogState.ts index 7b3b7bbfcac63..8185d128efa20 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 { client } 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 client.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..4b0120a29c546 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 { client, 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"; @@ -104,12 +104,14 @@ const fillAndSubmitForm = async ({ describe("TemplateSettingsPage", () => { it("succeeds", async () => { await renderTemplateSettingsPage(); - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); + await waitFor(() => + expect(client.api.updateTemplateMeta).toBeCalledTimes(1), + ); }); it("allows a description of 128 chars", () => { @@ -138,13 +140,16 @@ describe("TemplateSettingsPage", () => { http.get("/api/v2/entitlements", () => { return HttpResponse.json({ ...MockEntitlements, - features: API.withDefaultFeatures({ + features: withDefaultFeatures({ access_control: { enabled: true, entitlement: "entitled" }, }), }); }), ); - const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta"); + const updateTemplateMetaSpy = jest.spyOn( + client.api, + "updateTemplateMeta", + ); const deprecationMessage = "This template is deprecated"; await renderTemplateSettingsPage(); @@ -163,13 +168,16 @@ describe("TemplateSettingsPage", () => { http.get("/api/v2/entitlements", () => { return HttpResponse.json({ ...MockEntitlements, - features: API.withDefaultFeatures({ + features: withDefaultFeatures({ access_control: { enabled: false, entitlement: "not_entitled" }, }), }); }), ); - const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta"); + const updateTemplateMetaSpy = jest.spyOn( + client.api, + "updateTemplateMeta", + ); await renderTemplateSettingsPage(); await deprecateTemplate( diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 15e4c8fb21206..3ea10d8308b85 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 { client } from "api/api"; import { templateByNameKey } from "api/queries/templates"; import type { UpdateTemplateMeta } from "api/typesGenerated"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -31,7 +31,9 @@ export const TemplateSettingsPage: FC = () => { isLoading: isSubmitting, error: submitError, } = useMutation( - (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + (data: UpdateTemplateMeta) => { + return client.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..61e474be7ee84 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 { client } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockEntitlementsWithScheduling, @@ -127,38 +127,38 @@ function waitForWithCutoff(callback: () => void | Promise<void>) { describe("TemplateSchedulePage", () => { beforeEach(() => { jest - .spyOn(API, "getEntitlements") + .spyOn(client.api, "getEntitlements") .mockResolvedValue(MockEntitlementsWithScheduling); }); it("Calls the API when user fills in and submits a form", async () => { await renderTemplateSchedulePage(); - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(API.updateTemplateMeta).toBeCalledTimes(1), + expect(client.api.updateTemplateMeta).toBeCalledTimes(1), ); }); test("default is converted to and from hours", async () => { await renderTemplateSchedulePage(); - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(API.updateTemplateMeta).toBeCalledTimes(1), + expect(client.api.updateTemplateMeta).toBeCalledTimes(1), ); await waitForWithCutoff(() => { - expect(API.updateTemplateMeta).toBeCalledWith( + expect(client.api.updateTemplateMeta).toBeCalledWith( "test-template", expect.objectContaining({ default_ttl_ms: (validFormValues.default_ttl_ms || 0) * 3600000, @@ -170,18 +170,18 @@ describe("TemplateSchedulePage", () => { test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => { await renderTemplateSchedulePage(); - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(API.updateTemplateMeta).toBeCalledTimes(1), + expect(client.api.updateTemplateMeta).toBeCalledTimes(1), ); await waitForWithCutoff(() => { - expect(API.updateTemplateMeta).toBeCalledWith( + expect(client.api.updateTemplateMeta).toBeCalledWith( "test-template", expect.objectContaining({ failure_ttl_ms: (validFormValues.failure_ttl_ms || 0) * 86400000, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index de45cbd38652e..4c420bae5568e 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 { client } from "api/api"; import { templateByNameKey } from "api/queries/templates"; import type { UpdateTemplateMeta } from "api/typesGenerated"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -27,7 +27,8 @@ const TemplateSchedulePage: FC = () => { isLoading: isSubmitting, error: submitError, } = useMutation( - (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + (data: UpdateTemplateMeta) => + client.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..99047c540d352 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 { client } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockTemplate, @@ -32,12 +32,14 @@ const renderTemplateVariablesPage = async () => { describe("TemplateVariablesPage", () => { it("renders with variables", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest - .spyOn(API, "getTemplateVersion") + .spyOn(client.api, "getTemplateByName") + .mockResolvedValueOnce(MockTemplate); + jest + .spyOn(client.api, "getTemplateVersion") .mockResolvedValueOnce(MockTemplateVersion); jest - .spyOn(API, "getTemplateVersionVariables") + .spyOn(client.api, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, @@ -57,22 +59,26 @@ describe("TemplateVariablesPage", () => { }); it("user submits the form successfully", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest - .spyOn(API, "getTemplateVersion") + .spyOn(client.api, "getTemplateByName") + .mockResolvedValueOnce(MockTemplate); + jest + .spyOn(client.api, "getTemplateVersion") .mockResolvedValue(MockTemplateVersion); jest - .spyOn(API, "getTemplateVersionVariables") + .spyOn(client.api, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, ]); jest - .spyOn(API, "createTemplateVersion") + .spyOn(client.api, "createTemplateVersion") .mockResolvedValueOnce(MockTemplateVersion2); - jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ - message: "done", - }); + jest + .spyOn(client.api, "updateActiveTemplateVersion") + .mockResolvedValueOnce({ + message: "done", + }); await renderTemplateVariablesPage(); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 6f54255fbe23a..22b172c9cd9ed 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -26,6 +26,8 @@ import type { MonacoEditorProps } from "./MonacoEditor"; import { Language } from "./PublishTemplateVersionDialog"; import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; +const { client } = api; + // 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(client.api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); + jest.spyOn(client.api, "createTemplateVersion").mockResolvedValue({ ...templateVersion, job: { ...templateVersion.job, @@ -81,7 +83,7 @@ const buildTemplateVersion = async ( }, }); jest - .spyOn(api, "getTemplateVersionByName") + .spyOn(client.api, "getTemplateVersionByName") .mockResolvedValue(templateVersion); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") @@ -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(client.api, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(api, "updateActiveTemplateVersion") + .spyOn(client.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(client.api, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(api, "updateActiveTemplateVersion") + .spyOn(client.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(client.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..97e752c799459 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 { client } from "api/api"; import { file, uploadFile } from "api/queries/files"; import { createTemplateVersion, @@ -323,12 +323,12 @@ const publishVersion = async (options: { const publishActions: Promise<unknown>[] = []; if (haveChanges) { - publishActions.push(patchTemplateVersion(version.id, data)); + publishActions.push(client.api.patchTemplateVersion(version.id, data)); } if (isActiveVersion) { publishActions.push( - updateActiveTemplateVersion(version.template_id!, { + client.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..538087ca1b281 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 { client } from "api/api"; import { MockUser, MockWorkspace, @@ -56,7 +56,7 @@ describe("TerminalPage", () => { it("loads the right workspace data", async () => { jest - .spyOn(API, "getWorkspaceByOwnerAndName") + .spyOn(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValue(MockWorkspace); new WS( `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, @@ -65,7 +65,7 @@ describe("TerminalPage", () => { `/${MockUser.username}/${MockWorkspace.name}/terminal`, ); await waitFor(() => { - expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( + expect(client.api.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( MockUser.username, MockWorkspace.name, { include_deleted: true }, diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 4bce60a5fe465..52a53b8a0803e 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 { client } from "api/api"; import { mockApiError } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import * as AccountForm from "./AccountForm"; @@ -25,34 +25,36 @@ const fillAndSubmitForm = async () => { describe("AccountPage", () => { describe("when it is a success", () => { it("shows the success message", async () => { - jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) => - Promise.resolve({ - id: userId, - email: "user@coder.com", - created_at: new Date().toISOString(), - status: "active", - organization_ids: ["123"], - roles: [], - avatar_url: "", - last_seen_at: new Date().toISOString(), - login_type: "password", - theme_preference: "", - ...data, - }), - ); + jest + .spyOn(client.api, "updateProfile") + .mockImplementationOnce((userId, data) => + Promise.resolve({ + id: userId, + email: "user@coder.com", + created_at: new Date().toISOString(), + status: "active", + organization_ids: ["123"], + roles: [], + avatar_url: "", + last_seen_at: new Date().toISOString(), + login_type: "password", + theme_preference: "", + ...data, + }), + ); renderWithAuth(<AccountPage />); await fillAndSubmitForm(); const successMessage = await screen.findByText("Updated settings."); expect(successMessage).toBeDefined(); - expect(API.updateProfile).toBeCalledTimes(1); - expect(API.updateProfile).toBeCalledWith("me", newData); + expect(client.api.updateProfile).toBeCalledTimes(1); + expect(client.api.updateProfile).toBeCalledWith("me", newData); }); }); describe("when the username is already taken", () => { it("shows an error", async () => { - jest.spyOn(API, "updateProfile").mockRejectedValueOnce( + jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce( mockApiError({ message: "Invalid profile", validations: [ @@ -68,14 +70,14 @@ describe("AccountPage", () => { "Username is already in use", ); expect(errorMessage).toBeDefined(); - expect(API.updateProfile).toBeCalledTimes(1); - expect(API.updateProfile).toBeCalledWith("me", newData); + expect(client.api.updateProfile).toBeCalledTimes(1); + expect(client.api.updateProfile).toBeCalledWith("me", newData); }); }); describe("when it is an unknown error", () => { it("shows a generic error message", async () => { - jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ + jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce({ data: "unknown error", }); @@ -84,8 +86,8 @@ describe("AccountPage", () => { const errorMessage = await screen.findByText("Something went wrong."); expect(errorMessage).toBeDefined(); - expect(API.updateProfile).toBeCalledTimes(1); - expect(API.updateProfile).toBeCalledWith("me", newData); + expect(client.api.updateProfile).toBeCalledTimes(1); + expect(client.api.updateProfile).toBeCalledWith("me", newData); }); }); }); diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index 01c0ad3addfd0..8cca935805d8e 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 { client } from "api/api"; import { MockUser } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import { AppearancePage } from "./AppearancePage"; @@ -9,7 +9,7 @@ describe("appearance page", () => { it("does nothing when selecting current theme", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "dark", }); @@ -18,13 +18,13 @@ describe("appearance page", () => { await userEvent.click(dark); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(0); + expect(client.api.updateAppearanceSettings).toBeCalledTimes(0); }); it("changes theme to dark blue", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "darkBlue", }); @@ -33,8 +33,8 @@ describe("appearance page", () => { await userEvent.click(darkBlue); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(1); - expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", { + expect(client.api.updateAppearanceSettings).toBeCalledTimes(1); + expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", { theme_preference: "darkBlue", }); }); @@ -42,7 +42,7 @@ describe("appearance page", () => { it("changes theme to light", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "light", }); @@ -51,8 +51,8 @@ describe("appearance page", () => { await userEvent.click(light); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(1); - expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", { + expect(client.api.updateAppearanceSettings).toBeCalledTimes(1); + expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", { theme_preference: "light", }); }); diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index daa03d50ea839..e7c46bc466adf 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 { client } from "api/api"; import { MockGitSSHKey, mockApiError } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage"; @@ -28,7 +28,7 @@ describe("SSH keys Page", () => { const newUserSSHKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"; - jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({ + jest.spyOn(client.api, "regenerateUserSSHKey").mockResolvedValueOnce({ ...MockGitSSHKey, public_key: newUserSSHKey, }); @@ -43,7 +43,7 @@ describe("SSH keys Page", () => { await screen.findByText("SSH Key regenerated successfully."); // Check if the API was called correctly - expect(API.regenerateUserSSHKey).toBeCalledTimes(1); + expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1); // Check if the SSH key is updated await screen.findByText(newUserSSHKey); @@ -57,7 +57,7 @@ describe("SSH keys Page", () => { // Wait to the ssh be rendered on the screen await screen.findByText(MockGitSSHKey.public_key); - jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce( + jest.spyOn(client.api, "regenerateUserSSHKey").mockRejectedValueOnce( mockApiError({ message: SSHKeysPageLanguage.regenerationError, }), @@ -82,7 +82,7 @@ describe("SSH keys Page", () => { expect(alert).toHaveTextContent(SSHKeysPageLanguage.regenerationError); // Check if the API was called correctly - expect(API.regenerateUserSSHKey).toBeCalledTimes(1); + expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1); }); }); }); diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index f2f0c73c4d7c9..1665ece78b8b4 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 { client } from "api/api"; import type { OAuthConversionResponse } from "api/typesGenerated"; import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities"; import { @@ -37,29 +37,34 @@ const fillAndSubmitSecurityForm = () => { }; beforeEach(() => { - jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll); - jest.spyOn(API, "getUserLoginType").mockResolvedValue({ + jest + .spyOn(client.api, "getAuthMethods") + .mockResolvedValue(MockAuthMethodsAll); + jest.spyOn(client.api, "getUserLoginType").mockResolvedValue({ login_type: "password", }); }); test("update password successfully", async () => { jest - .spyOn(API, "updateUserPassword") + .spyOn(client.api, "updateUserPassword") .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined)); const { user } = await renderPage(); fillAndSubmitSecurityForm(); const successMessage = await screen.findByText("Updated password."); expect(successMessage).toBeDefined(); - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith( + user.id, + newSecurityFormValues, + ); await waitFor(() => expect(window.location).toBeAt("/")); }); test("update password with incorrect old password", async () => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( + jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce( mockApiError({ message: "Incorrect password.", validations: [{ detail: "Incorrect password.", field: "old_password" }], @@ -72,12 +77,15 @@ test("update password with incorrect old password", async () => { const errorMessage = await screen.findAllByText("Incorrect password."); expect(errorMessage).toBeDefined(); expect(errorMessage).toHaveLength(2); - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith( + user.id, + newSecurityFormValues, + ); }); test("update password with invalid password", async () => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( + jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce( mockApiError({ message: "Invalid password.", validations: [{ detail: "Invalid password.", field: "password" }], @@ -90,12 +98,15 @@ test("update password with invalid password", async () => { const errorMessage = await screen.findAllByText("Invalid password."); expect(errorMessage).toBeDefined(); expect(errorMessage).toHaveLength(2); - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith( + user.id, + newSecurityFormValues, + ); }); test("update password when submit returns an unknown error", async () => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ + jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce({ data: "unknown error", }); @@ -104,15 +115,18 @@ test("update password when submit returns an unknown error", async () => { const errorMessage = await screen.findByText("Something went wrong."); expect(errorMessage).toBeDefined(); - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith( + user.id, + newSecurityFormValues, + ); }); test("change login type to OIDC", async () => { const user = userEvent.setup(); const { user: userData } = await renderPage(); const convertToOAUTHSpy = jest - .spyOn(API, "convertToOAUTH") + .spyOn(client.api, "convertToOAUTH") .mockResolvedValue({ state_string: "some-state-string", expires_at: "2021-01-01T00:00:00Z", diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx index f0191310656c5..afd0a66498d9f 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 { client } 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: client.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..4b8bb9c9b11fd 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 { client } from "api/api"; import { getErrorMessage } from "api/errors"; import type { AuthMethods, @@ -52,7 +52,7 @@ export const useSingleSignOnSection = () => { const [loginTypeConfirmation, setLoginTypeConfirmation] = useState<LoginTypeConfirmation>({ open: false, selectedType: undefined }); - const mutation = useMutation(convertToOAUTH, { + const mutation = useMutation(client.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..3e923a89cd5bf 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 { client } from "api/api"; import type { TokensFilter } from "api/typesGenerated"; // Load all tokens @@ -13,7 +13,7 @@ export const useTokensData = ({ include_all }: TokensFilter) => { const result = useQuery({ queryKey, queryFn: () => - getTokens({ + client.api.getTokens({ include_all, }), }); @@ -29,7 +29,7 @@ export const useDeleteToken = (queryKey: QueryKey) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: deleteToken, + mutationFn: client.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..815b36a721b02 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 { client } from "api/api"; import type { Role } from "api/typesGenerated"; import { MockUser, @@ -261,7 +261,7 @@ describe("UsersPage", () => { await resetUserPassword(() => { jest - .spyOn(API, "updateUserPassword") + .spyOn(client.api, "updateUserPassword") .mockResolvedValueOnce(undefined); }); @@ -269,8 +269,8 @@ describe("UsersPage", () => { await screen.findByText("Successfully updated the user password."); // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", }); @@ -281,15 +281,17 @@ describe("UsersPage", () => { renderPage(); await resetUserPassword(() => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({}); + jest + .spyOn(client.api, "updateUserPassword") + .mockRejectedValueOnce({}); }); // Check if the error message is displayed await screen.findByText("Error on resetting the user password."); // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1); - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { + expect(client.api.updateUserPassword).toBeCalledTimes(1); + expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", }); diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index e358c50954d03..523ef6c808df6 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 { client } from "api/api"; import { MockWorkspace, MockWorkspaceAgent, @@ -18,7 +18,7 @@ afterEach(() => { describe("WorkspaceBuildPage", () => { test("gets the right workspace build", async () => { const getWorkspaceBuildSpy = jest - .spyOn(API, "getWorkspaceBuildByNumber") + .spyOn(client.api, "getWorkspaceBuildByNumber") .mockResolvedValue(MockWorkspaceBuild); renderWithAuth(<WorkspaceBuildPage />, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 13ec3028248bb..0345137b691c0 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 { client } 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 client.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..3299e25b1a522 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 { client } from "api/api"; import type { TemplateVersionParameter, Workspace, @@ -49,7 +49,7 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({ }) => { const { data: parameters } = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], - queryFn: () => getWorkspaceParameters(workspace), + queryFn: () => client.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..2ac588f5ce373 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 API from "api/api"; import type { TemplateVersionParameter, Workspace } from "api/typesGenerated"; import EventSourceMock from "eventsourcemock"; import { @@ -22,16 +22,22 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; +const { client } = API; + // 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, "getDeploymentConfig") + .spyOn(client.api, "getWorkspaceByOwnerAndName") + .mockResolvedValue(workspace); + jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest + .spyOn(client.api, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([]); + jest + .spyOn(client.api, "getDeploymentConfig") .mockResolvedValueOnce(MockDeploymentConfig); jest - .spyOn(api, "watchWorkspaceAgentLogs") + .spyOn(API, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { options.onDone?.(); return new WebSocket(""); @@ -87,7 +93,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(client.api, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await renderWorkspacePage(MockWorkspace); @@ -127,7 +133,7 @@ describe("WorkspacePage", () => { ); const deleteWorkspaceMock = jest - .spyOn(api, "deleteWorkspace") + .spyOn(client.api, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuildDelete); await renderWorkspacePage(MockFailedWorkspace); @@ -173,7 +179,7 @@ describe("WorkspacePage", () => { ); const startWorkspaceMock = jest - .spyOn(api, "startWorkspace") + .spyOn(client.api, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); @@ -181,7 +187,7 @@ describe("WorkspacePage", () => { it("requests a stop job when the user presses Stop", async () => { const stopWorkspaceMock = jest - .spyOn(api, "stopWorkspace") + .spyOn(client.api, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await testButton(MockWorkspace, "Stop", stopWorkspaceMock); @@ -189,7 +195,7 @@ describe("WorkspacePage", () => { it("requests a stop when the user presses Restart", async () => { const stopWorkspaceMock = jest - .spyOn(api, "stopWorkspace") + .spyOn(client.api, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -215,7 +221,7 @@ describe("WorkspacePage", () => { ); const cancelWorkspaceMock = jest - .spyOn(api, "cancelWorkspaceBuild") + .spyOn(client.api, "cancelWorkspaceBuild") .mockImplementation(() => Promise.resolve({ message: "job canceled" })); await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock); @@ -224,11 +230,11 @@ describe("WorkspacePage", () => { it("requests an update when the user presses Update", async () => { // Mocks jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceMock = jest - .spyOn(api, "updateWorkspace") + .spyOn(client.api, "updateWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -249,12 +255,12 @@ describe("WorkspacePage", () => { it("updates the parameters when they are missing during update", async () => { // Mocks jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceSpy = jest - .spyOn(api, "updateWorkspace") + .spyOn(client.api, "updateWorkspace") .mockRejectedValueOnce( - new api.MissingBuildParameters( + new API.MissingBuildParameters( [MockTemplateVersionParameter1, MockTemplateVersionParameter2], MockOutdatedWorkspace.template_active_version_id, ), @@ -271,7 +277,7 @@ describe("WorkspacePage", () => { // The update was called await waitFor(() => { - expect(api.updateWorkspace).toBeCalled(); + expect(client.api.updateWorkspace).toBeCalled(); updateWorkspaceSpy.mockClear(); }); @@ -294,7 +300,7 @@ describe("WorkspacePage", () => { // Check if the update was called using the values from the form await waitFor(() => { - expect(api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ + expect(client.api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ { name: MockTemplateVersionParameter1.name, value: "some-value", @@ -309,7 +315,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(client.api, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { ...MockTemplateVersionParameter1, @@ -321,7 +327,7 @@ describe("WorkspacePage", () => { ], buildParameters: [{ name: "rebuild", value: "false" }], }); - const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); + const restartWorkspaceSpy = jest.spyOn(client.api, "restartWorkspace"); const user = userEvent.setup(); await renderWorkspacePage(MockWorkspace); await user.click(screen.getByTestId("build-parameters-button")); @@ -351,7 +357,7 @@ describe("WorkspacePage", () => { const retryDebugButtonRe = /^Debug$/i; describe("Retries a failed 'Start' transition", () => { - const mockStart = jest.spyOn(api, "startWorkspace"); + const mockStart = jest.spyOn(client.api, "startWorkspace"); const failedStart: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -384,7 +390,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Stop' transition", () => { - const mockStop = jest.spyOn(api, "stopWorkspace"); + const mockStop = jest.spyOn(client.api, "stopWorkspace"); const failedStop: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -405,7 +411,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Delete' transition", () => { - const mockDelete = jest.spyOn(api, "deleteWorkspace"); + const mockDelete = jest.spyOn(client.api, "deleteWorkspace"); const failedDelete: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -450,7 +456,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(api, "startWorkspace"); + const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace"); await renderWorkspacePage(workspace); const retryWithBuildParametersButton = await screen.findByRole("button", { @@ -496,7 +502,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(api, "startWorkspace"); + const startWorkspaceSpy = jest.spyOn(client.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 5df3e18bc6fe6..13ed63059fbfc 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, client } from "api/api"; import { getErrorMessage } from "api/errors"; import { buildInfo } from "api/queries/buildInfo"; import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; @@ -80,7 +80,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({ }>({ open: false }); const { mutate: mutateRestartWorkspace, isLoading: isRestarting } = useMutation({ - mutationFn: restartWorkspace, + mutationFn: client.api.restartWorkspace, }); // SSH Prefix diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.test.tsx index 71e4174499305..19a0ef19fe34a 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 { client } from "api/api"; import { workspaceByOwnerAndName } from "api/queries/workspaces"; import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar"; import { ThemeProvider } from "contexts/ThemeProvider"; @@ -62,7 +62,7 @@ const renderScheduleControls = async () => { test("add 3 hours to deadline", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(API, "putWorkspaceExtension") + .spyOn(client.api, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); @@ -91,7 +91,7 @@ test("add 3 hours to deadline", async () => { test("remove 2 hours to deadline", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(API, "putWorkspaceExtension") + .spyOn(client.api, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); @@ -119,7 +119,7 @@ test("remove 2 hours to deadline", async () => { test("rollback to previous deadline on error", async () => { const user = userEvent.setup(); const initialScheduleMessage = "Stop in 3 hours"; - jest.spyOn(API, "putWorkspaceExtension").mockRejectedValue({}); + jest.spyOn(client.api, "putWorkspaceExtension").mockRejectedValue({}); await renderScheduleControls(); @@ -139,7 +139,7 @@ test("rollback to previous deadline on error", async () => { test("request is only sent once when clicking multiple times", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(API, "putWorkspaceExtension") + .spyOn(client.api, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 99b4f692d1db2..1879a28c01564 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 { client } from "api/api"; import { MockWorkspace, MockTemplateVersionParameter1, @@ -20,15 +20,17 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest - .spyOn(api, "getWorkspaceByOwnerAndName") + .spyOn(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockWorkspace); - jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([ - MockTemplateVersionParameter1, - MockTemplateVersionParameter2, - // Immutable parameters - MockTemplateVersionParameter4, - ]); - jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValueOnce([ + jest + .spyOn(client.api, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + // Immutable parameters + MockTemplateVersionParameter4, + ]); + jest.spyOn(client.api, "getWorkspaceBuildParameters").mockResolvedValueOnce([ MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, // Immutable value @@ -36,7 +38,7 @@ test("Submit the workspace settings page successfully", async () => { ]); // Mock the API calls that submit data const postWorkspaceBuildSpy = jest - .spyOn(api, "postWorkspaceBuild") + .spyOn(client.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..72dc73d672185 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 { client } 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: () => client.api.getWorkspaceParameters(workspace), }); const navigate = useNavigate(); const updateParameters = useMutation({ mutationFn: (buildParameters: WorkspaceBuildParameter[]) => - postWorkspaceBuild(workspace.id, { + client.api.postWorkspaceBuild(workspace.id, { transition: "start", rich_parameter_values: buildParameters, }), @@ -93,7 +93,9 @@ const WorkspaceParametersPage: FC = () => { export type WorkspaceParametersPageViewProps = { workspace: Workspace; canChangeVersions: boolean; - data: Awaited<ReturnType<typeof getWorkspaceParameters>> | undefined; + data: + | Awaited<ReturnType<typeof client.api.getWorkspaceParameters>> + | 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 22d6eaa4df141..920c9e4b2e352 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 { client } from "api/api"; import { defaultSchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; import { MockTemplate } from "testHelpers/entities"; import { render } from "testHelpers/renderHelpers"; @@ -271,7 +271,7 @@ const defaultFormProps: WorkspaceScheduleFormProps = { describe("templateInheritance", () => { it("disables the entire autostart feature appropriately", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); const props = { ...defaultFormProps, template: { @@ -299,7 +299,7 @@ describe("templateInheritance", () => { it("disables the autostart days of the week appropriately", async () => { const enabledDayLabels = ["Sat", "Sun"]; - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); const props = { ...defaultFormProps, template: { @@ -343,7 +343,7 @@ describe("templateInheritance", () => { allow_user_autostop: false, }, }; - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); render(<WorkspaceScheduleForm {...props} />); const autoStopToggle = await screen.findByLabelText("Enable Autostop"); @@ -355,7 +355,7 @@ describe("templateInheritance", () => { expect(ttlInput).toBeDisabled(); }); it("disables secondary autostart fields if main feature switch is toggled off", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -379,7 +379,7 @@ describe("templateInheritance", () => { }); }); it("disables secondary autostop fields if main feature switch is toggled off", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -398,7 +398,7 @@ describe("templateInheritance", () => { }); test("form should be enabled when both auto stop and auto start features are disabled, given that the template permits these actions", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -423,7 +423,7 @@ test("form should be disabled when both auto stop and auto start features are di allow_user_autostop: false, }, }; - jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); render(<WorkspaceScheduleForm {...props} />); const submitButton = await screen.findByRole("button", { name: "Submit" }); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 7830a161c879e..cb5870b3c9db8 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 { client } from "api/api"; import { checkAuthorization } from "api/queries/authCheck"; import { templateByName } from "api/queries/templates"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; @@ -72,7 +68,10 @@ export const WorkspaceSchedulePage: FC = () => { const [isConfirmingApply, setIsConfirmingApply] = useState(false); const { mutate: updateWorkspace } = useMutation({ mutationFn: () => - startWorkspace(workspace.id, workspace.template_active_version_id), + client.api.startWorkspace( + workspace.id, + workspace.template_active_version_id, + ), }); return ( @@ -167,11 +166,11 @@ const submitSchedule = async (data: SubmitScheduleData) => { const actions: Promise<void>[] = []; if (autostartChanged) { - actions.push(putWorkspaceAutostart(workspace.id, autostart)); + actions.push(client.api.putWorkspaceAutostart(workspace.id, autostart)); } if (autostopChanged) { - actions.push(putWorkspaceAutostop(workspace.id, ttl)); + actions.push(client.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..e2b5acf67e2d8 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 { client } 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(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace }); // Mock the API calls that submit data const patchWorkspaceSpy = jest - .spyOn(api, "patchWorkspace") + .spyOn(client.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(client.api, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false }); renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, { route: "/@test-user/test-workspace/settings", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index e289a58c5ce59..e15d429178920 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 { client } 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( + client.api.patchWorkspace(workspace.id, { name: formValues.name }), + client.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..cc49cd091f22d 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 { client } 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<BatchUpdateConfirmationProps> = ({ // ...but the query _also_ doesn't have everything we need, like the // template display name! ...version, - ...(await getTemplateVersion(version.id)), + ...(await client.api.getTemplateVersion(version.id)), }), })), }); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 5b0dc1b2a959e..196ba291dd942 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 { client } from "api/api"; import type { Workspace } from "api/typesGenerated"; import { MockStoppedWorkspace, @@ -58,9 +58,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "3" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const deleteWorkspace = jest.spyOn(API, "deleteWorkspace"); + const deleteWorkspace = jest.spyOn(client.api, "deleteWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -95,9 +95,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "4" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(API, "updateWorkspace"); + const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -134,9 +134,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "3" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(API, "updateWorkspace"); + const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -172,9 +172,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "3" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(API, "updateWorkspace"); + const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -212,9 +212,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "5" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(API, "updateWorkspace"); + const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -254,9 +254,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "3" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const stopWorkspace = jest.spyOn(API, "stopWorkspace"); + const stopWorkspace = jest.spyOn(client.api, "stopWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -281,9 +281,9 @@ describe("WorkspacesPage", () => { { ...MockStoppedWorkspace, id: "3" }, ]; jest - .spyOn(API, "getWorkspaces") + .spyOn(client.api, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const startWorkspace = jest.spyOn(API, "startWorkspace"); + const startWorkspace = jest.spyOn(client.api, "startWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx index a9e3eb1cf4c7c..0698c7b2a9c5c 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 { client } 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), + client.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) => client.api.stopWorkspace(w.id))); }, onSuccess, onError: () => { @@ -43,7 +36,9 @@ 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) => client.api.deleteWorkspace(w.id)), + ); }, onSuccess, onError: () => { @@ -56,7 +51,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) - .map((w) => updateWorkspace(w)), + .map((w) => client.api.updateWorkspace(w)), ); }, onSuccess, @@ -70,7 +65,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => !w.favorite) - .map((w) => putFavoriteWorkspace(w.id)), + .map((w) => client.api.putFavoriteWorkspace(w.id)), ); }, onSuccess, @@ -84,7 +79,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.favorite) - .map((w) => deleteFavoriteWorkspace(w.id)), + .map((w) => client.api.deleteFavoriteWorkspace(w.id)), ); }, onSuccess, diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index a785d00d03122..d6840a35c31fa 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 { client } from "api/api"; import { getErrorMessage } from "api/errors"; import type { Workspace, @@ -30,7 +30,7 @@ export const useWorkspacesData = ({ const result = useQuery({ queryKey, queryFn: () => - getWorkspaces({ + client.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: client.api.updateWorkspaceVersion, onMutate: async (workspace) => { await queryClient.cancelQueries({ queryKey }); queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => { diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts index a8db56dd5a226..48789d789997c 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 { client } 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 client.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 client.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..643847cdcdb26 100644 --- a/site/src/utils/terminal.ts +++ b/site/src/utils/terminal.ts @@ -1,4 +1,4 @@ -import * as API from "api/api"; +import { client } from "api/api"; export const terminalWebsocketUrl = async ( baseUrl: string | undefined, @@ -30,7 +30,7 @@ export const terminalWebsocketUrl = async ( } // Do ticket issuance and set the query parameter. - const tokenRes = await API.issueReconnectingPTYSignedToken({ + const tokenRes = await client.api.issueReconnectingPTYSignedToken({ url: url.toString(), agentID: agentId, }); From 654df181eefc251e629c397527900d624755433c Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Tue, 7 May 2024 13:47:55 +0000 Subject: [PATCH 5/7] fix: remove temp comments --- site/src/api/api.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5a8e4bfb1374f..b2252512366fe 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -25,10 +25,6 @@ import userAgentParser from "ua-parser-js"; import { delay } from "../utils/delay"; import * as TypesGen from "./typesGenerated"; -//////////////////////////////////////////////////////////////////////////////// -// START OF API FILE -//////////////////////////////////////////////////////////////////////////////// - const getMissingParameters = ( oldBuildParameters: TypesGen.WorkspaceBuildParameter[], newBuildParameters: TypesGen.WorkspaceBuildParameter[], @@ -1847,10 +1843,6 @@ export class Api { }; } -//////////////////////////////////////////////////////////////////////////////// -// START OF CLIENT FILE -//////////////////////////////////////////////////////////////////////////////// - // 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 From 2e2497374b2b58dbdaac84a376223543e8d2fe60 Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Thu, 9 May 2024 16:23:17 +0000 Subject: [PATCH 6/7] refactor: smoooooooosh the API --- site/e2e/api.ts | 12 +-- site/e2e/global.setup.ts | 4 +- site/e2e/helpers.ts | 4 +- site/e2e/reporter.ts | 4 +- site/e2e/tests/deployment/general.spec.ts | 4 +- site/e2e/tests/deployment/network.spec.ts | 4 +- .../tests/deployment/observability.spec.ts | 4 +- site/e2e/tests/deployment/security.spec.ts | 7 +- site/e2e/tests/deployment/userAuth.spec.ts | 4 +- .../tests/deployment/workspaceProxies.spec.ts | 4 +- site/e2e/tests/groups/removeMember.spec.ts | 4 +- .../templates/updateTemplateSchedule.spec.ts | 8 +- site/src/api/api.test.ts | 70 ++++++------ site/src/api/api.ts | 102 ++++++++++-------- site/src/api/queries/appearance.ts | 6 +- site/src/api/queries/audits.ts | 4 +- site/src/api/queries/authCheck.ts | 4 +- site/src/api/queries/buildInfo.ts | 4 +- site/src/api/queries/debug.ts | 10 +- site/src/api/queries/deployment.ts | 10 +- site/src/api/queries/entitlements.ts | 6 +- site/src/api/queries/experiments.ts | 6 +- site/src/api/queries/externalAuth.ts | 14 +-- site/src/api/queries/files.ts | 6 +- site/src/api/queries/groups.ts | 18 ++-- site/src/api/queries/insights.ts | 12 +-- site/src/api/queries/integrations.ts | 4 +- site/src/api/queries/oauth2.ts | 20 ++-- site/src/api/queries/roles.ts | 4 +- site/src/api/queries/settings.ts | 6 +- site/src/api/queries/sshKeys.ts | 6 +- site/src/api/queries/templates.ts | 56 +++++----- site/src/api/queries/updateCheck.ts | 4 +- site/src/api/queries/users.ts | 40 +++---- site/src/api/queries/workspaceBuilds.ts | 12 +-- site/src/api/queries/workspaceQuota.ts | 6 +- site/src/api/queries/workspaceportsharing.ts | 8 +- site/src/api/queries/workspaces.ts | 36 +++---- site/src/components/Filter/UserFilter.tsx | 6 +- site/src/contexts/ProxyContext.tsx | 6 +- site/src/contexts/auth/RequireAuth.tsx | 4 +- site/src/contexts/useProxyLatency.ts | 4 +- .../src/modules/resources/AppLink/AppLink.tsx | 4 +- .../modules/resources/PortForwardButton.tsx | 4 +- .../VSCodeDesktopButton.tsx | 8 +- site/src/pages/AuditPage/AuditPage.test.tsx | 16 ++- .../CreateTemplatePage.test.tsx | 59 +++++----- .../CreateTokenPage/CreateTokenPage.test.tsx | 4 +- .../pages/CreateTokenPage/CreateTokenPage.tsx | 6 +- .../CreateWorkspacePage.test.tsx | 72 ++++++------- .../CreateWorkspacePage.tsx | 4 +- .../AddNewLicensePage.tsx | 4 +- .../LicensesSettingsPage.tsx | 6 +- .../TemplateEmbedPage.test.tsx | 4 +- .../TemplateEmbedPage/TemplateEmbedPage.tsx | 4 +- .../src/pages/TemplatePage/TemplateLayout.tsx | 11 +- .../TemplateSummaryPage.tsx | 4 +- .../TemplateVersionsPage.tsx | 8 +- .../useDeletionDialogState.test.ts | 6 +- .../TemplatePage/useDeletionDialogState.ts | 4 +- .../TemplateSettingsPage.test.tsx | 18 +--- .../TemplateSettingsPage.tsx | 4 +- .../TemplateSchedulePage.test.tsx | 20 ++-- .../TemplateSchedulePage.tsx | 5 +- .../TemplateVariablesPage.test.tsx | 28 ++--- .../TemplateVersionEditorPage.test.tsx | 18 ++-- .../TemplateVersionEditorPage.tsx | 6 +- .../pages/TerminalPage/TerminalPage.test.tsx | 6 +- .../AccountPage/AccountPage.test.tsx | 50 +++++---- .../AppearancePage/AppearancePage.test.tsx | 18 ++-- .../SSHKeysPage/SSHKeysPage.test.tsx | 10 +- .../SecurityPage/SecurityPage.test.tsx | 46 +++----- .../SecurityPage/SecurityPage.tsx | 4 +- .../SecurityPage/SingleSignOnSection.tsx | 4 +- .../UserSettingsPage/TokensPage/hooks.ts | 9 +- site/src/pages/UsersPage/UsersPage.test.tsx | 16 ++- .../WorkspaceBuildPage.test.tsx | 4 +- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 4 +- .../BuildParametersPopover.tsx | 4 +- .../WorkspacePage/WorkspacePage.test.tsx | 58 +++++----- .../WorkspacePage/WorkspaceReadyPage.tsx | 4 +- .../WorkspaceScheduleControls.test.tsx | 10 +- .../WorkspaceParametersPage.test.tsx | 22 ++-- .../WorkspaceParametersPage.tsx | 10 +- .../WorkspaceScheduleForm.test.tsx | 16 +-- .../WorkspaceSchedulePage.tsx | 11 +- .../WorkspaceSettingsPage.test.tsx | 8 +- .../WorkspaceSettingsPage.tsx | 6 +- .../BatchUpdateConfirmation.tsx | 4 +- .../WorkspacesPage/WorkspacesPage.test.tsx | 30 +++--- .../src/pages/WorkspacesPage/batchActions.tsx | 16 ++- site/src/pages/WorkspacesPage/data.ts | 6 +- site/src/pages/WorkspacesPage/filter/menus.ts | 6 +- site/src/utils/terminal.ts | 4 +- 94 files changed, 593 insertions(+), 677 deletions(-) diff --git a/site/e2e/api.ts b/site/e2e/api.ts index fb442ca7f5bd7..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 { type DeploymentConfig, client } 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"; @@ -11,26 +11,26 @@ let currentOrgId: string; export const setupApiCalls = async (page: Page) => { try { const token = await findSessionToken(page); - client.setSessionToken(token); + API.setSessionToken(token); } catch { // If this fails, we have an unauthenticated client. } - client.setHost(`http://127.0.0.1:${coderPort}`); + API.setHost(`http://127.0.0.1:${coderPort}`); }; export const getCurrentOrgId = async (): Promise<string> => { if (currentOrgId) { return currentOrgId; } - const currentUser = await client.api.getAuthenticatedUser(); + const currentUser = await API.getAuthenticatedUser(); currentOrgId = currentUser.organization_ids[0]; return currentOrgId; }; export const createUser = async (orgId: string) => { const name = randomName(); - const user = await client.api.createUser({ + const user = await API.createUser({ email: `${name}@coder.com`, username: name, password: "s3cure&password!", @@ -43,7 +43,7 @@ export const createUser = async (orgId: string) => { export const createGroup = async (orgId: string) => { const name = randomName(); - const group = await client.api.createGroup(orgId, { + const group = await API.createGroup(orgId, { name, display_name: `Display ${name}`, avatar_url: "/emojis/1f60d.png", diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts index e577e048ddb27..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 { client } 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 client.api.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 474b378904371..95c38cf5bf533 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 { client } from "api/api"; +import { API } from "api/api"; import type { WorkspaceBuildParameter, UpdateTemplateMeta, @@ -396,7 +396,7 @@ export const waitUntilUrlIsNotResponding = async (url: string) => { const retryIntervalMs = 1000; let retries = 0; - const axiosInstance = client.getAxiosInstance(); + 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 f2410a55859a8..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 { client } from "api/api"; +import { API } from "api/api"; import { coderdPProfPort, enterpriseLicense } from "./constants"; class CoderReporter implements Reporter { @@ -143,7 +143,7 @@ const logLines = (chunk: string | Buffer): string[] => { }; const exportDebugPprof = async (outputFile: string) => { - const axiosInstance = client.getAxiosInstance(); + 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 c52ec09b641f0..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 { client } from "api/api"; +import { API } from "api/api"; import { setupApiCalls } from "../../api"; import { e2eFakeExperiment1, e2eFakeExperiment2 } from "../../constants"; @@ -7,7 +7,7 @@ test("experiments", async ({ page }) => { await setupApiCalls(page); // Load experiments from backend API - const availableExperiments = await client.api.getAvailableExperiments(); + const availableExperiments = await API.getAvailableExperiments(); // Verify if the site lists the same experiments await page.goto("/deployment/general", { waitUntil: "networkidle" }); diff --git a/site/e2e/tests/deployment/network.spec.ts b/site/e2e/tests/deployment/network.spec.ts index 36ea46d7f45ac..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 { client } 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 client.api.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 0268c8baa3ab1..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 { client } 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 client.api.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 e8a79776a8ebc..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 { client } 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 client.api.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 8083e4f66537d..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 { client } 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 client.api.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 ff02853f01b83..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 { client } 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 client.api.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 3d2fb6534e846..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 { client } from "api/api"; +import { API } from "api/api"; import { createGroup, createUser, @@ -19,7 +19,7 @@ test("remove member", async ({ page, baseURL }) => { createGroup(orgId), createUser(orgId), ]); - await client.api.addMember(group.id, member.id); + await API.addMember(group.id, member.id); await page.goto(`${baseURL}/groups/${group.id}`, { waitUntil: "domcontentloaded", diff --git a/site/e2e/tests/templates/updateTemplateSchedule.spec.ts b/site/e2e/tests/templates/updateTemplateSchedule.spec.ts index 768d41518d7a3..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 { client } 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 client.api.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 client.api.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 client.api.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 f2547a22cd80c..af5f5e22d61ba 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -6,10 +6,10 @@ import { MockWorkspaceBuild, MockWorkspaceBuildParameter1, } from "testHelpers/entities"; -import { client, getURLWithSearchParams, MissingBuildParameters } from "./api"; +import { API, getURLWithSearchParams, MissingBuildParameters } from "./api"; import type * as TypesGen from "./typesGenerated"; -const axiosInstance = client.getAxiosInstance(); +const axiosInstance = API.getAxiosInstance(); describe("api.ts", () => { describe("login", () => { @@ -24,7 +24,7 @@ describe("api.ts", () => { .mockResolvedValueOnce({ data: loginResponse }); // when - const result = await client.api.login("test", "123"); + const result = await API.login("test", "123"); // then expect(axiosInstance.post).toHaveBeenCalled(); @@ -45,7 +45,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await client.api.login("test", "123"); + await API.login("test", "123"); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -61,7 +61,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; // when - await client.api.logout(); + await API.logout(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -81,7 +81,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await client.api.logout(); + await API.logout(); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -101,7 +101,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; // when - const result = await client.api.getApiKey(); + const result = await API.getApiKey(); // then expect(axiosMockPost).toHaveBeenCalled(); @@ -122,7 +122,7 @@ describe("api.ts", () => { axiosInstance.post = axiosMockPost; try { - await client.api.getApiKey(); + await API.getApiKey(); } catch (error) { expect(error).toStrictEqual(expectedError); } @@ -173,30 +173,25 @@ describe("api.ts", () => { describe("update", () => { it("creates a build with start and the latest template", async () => { jest - .spyOn(client.api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); - await client.api.updateWorkspace(MockWorkspace); - expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith( - MockWorkspace.id, - { - transition: "start", - template_version_id: MockTemplate.active_version_id, - rich_parameter_values: [], - }, - ); + 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: [], + }); }); it("fails when having missing parameters", async () => { jest - .spyOn(client.api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValue(MockWorkspaceBuild); - jest.spyOn(client.api, "getTemplate").mockResolvedValue(MockTemplate); - jest - .spyOn(client.api, "getWorkspaceBuildParameters") - .mockResolvedValue([]); + jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]); jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValue([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, @@ -204,7 +199,7 @@ describe("api.ts", () => { let error = new Error(); try { - await client.api.updateWorkspace(MockWorkspace); + await API.updateWorkspace(MockWorkspace); } catch (e) { error = e as Error; } @@ -219,26 +214,23 @@ describe("api.ts", () => { it("creates a build with the no parameters if it is already filled", async () => { jest - .spyOn(client.api, "postWorkspaceBuild") + .spyOn(API, "postWorkspaceBuild") .mockResolvedValueOnce(MockWorkspaceBuild); - jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); jest - .spyOn(client.api, "getWorkspaceBuildParameters") + .spyOn(API, "getWorkspaceBuildParameters") .mockResolvedValue([MockWorkspaceBuildParameter1]); jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValue([ { ...MockTemplateVersionParameter1, required: true, mutable: false }, ]); - await client.api.updateWorkspace(MockWorkspace); - expect(client.api.postWorkspaceBuild).toHaveBeenCalledWith( - MockWorkspace.id, - { - transition: "start", - template_version_id: MockTemplate.active_version_id, - rich_parameter_values: [], - }, - ); + 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 b2252512366fe..9bc747f1c17f6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -367,8 +367,21 @@ export class MissingBuildParameters extends Error { } } -export class Api { - constructor(private readonly axios: AxiosInstance) {} +/** + * 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, @@ -1858,56 +1871,57 @@ const tokenMetadataElement = ? document.head.querySelector('meta[property="csrf-token"]') : null; -interface ClientApi { - api: Api; +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 Client implements ClientApi { - private readonly axios: AxiosInstance; - readonly api: Api; - +export class Api extends ApiMethods implements ClientApi { constructor() { - this.axios = this.getConfiguredAxiosInstance(); - this.api = new Api(this.axios); + const scopedAxiosInstance = getConfiguredAxiosInstance(); + super(scopedAxiosInstance); } - private getConfiguredAxiosInstance(): AxiosInstance { - const axios = globalAxios.create(); - - // Adds 304 for the default axios validateStatus function - // https://github.com/axios/axios#handling-errors Check status here - // https://httpstatusdogs.com/ - axios.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 - this.axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; - axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; - tokenMetadataElement.setAttribute("content", csrfToken); - } else { - axios.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 axios; - } + // 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; @@ -1926,4 +1940,4 @@ export class Client implements ClientApi { }; } -export const client = new Client(); +export const API = new Api(); diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index de867f72cb09e..8819d45fdfff9 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 { client } from "api/api"; +import { API } from "api/api"; import type { AppearanceConfig } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; @@ -10,13 +10,13 @@ export const appearance = (metadata: MetadataState<AppearanceConfig>) => { return cachedQuery({ metadata, queryKey: ["appearance"], - queryFn: () => client.api.getAppearance(), + queryFn: () => API.getAppearance(), }); }; export const updateAppearance = (queryClient: QueryClient) => { return { - mutationFn: client.api.updateAppearance, + mutationFn: API.updateAppearance, onSuccess: (newConfig: AppearanceConfig) => { queryClient.setQueryData(appearanceConfigKey, newConfig); }, diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts index 22326c1c8d4a8..1dce9a29eaab8 100644 --- a/site/src/api/queries/audits.ts +++ b/site/src/api/queries/audits.ts @@ -1,4 +1,4 @@ -import { client } 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 client.api.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 470685f354789..3248f35357f25 100644 --- a/site/src/api/queries/authCheck.ts +++ b/site/src/api/queries/authCheck.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; import type { AuthorizationRequest } from "api/typesGenerated"; export const AUTHORIZATION_KEY = "authorization"; @@ -9,6 +9,6 @@ export const getAuthorizationKey = (req: AuthorizationRequest) => export const checkAuthorization = (req: AuthorizationRequest) => { return { queryKey: getAuthorizationKey(req), - queryFn: () => client.api.checkAuthorization(req), + queryFn: () => API.checkAuthorization(req), }; }; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index b4152904d7e14..43dac7d20334f 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; import type { BuildInfoResponse } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; @@ -10,6 +10,6 @@ export const buildInfo = (metadata: MetadataState<BuildInfoResponse>) => { return cachedQuery({ metadata, queryKey: buildInfoKey, - queryFn: () => client.api.getBuildInfo(), + queryFn: () => API.getBuildInfo(), }); }; diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts index 5cd4f40c7b635..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 { client } from "api/api"; +import { API } from "api/api"; import type { HealthSettings, UpdateHealthSettings } from "api/typesGenerated"; export const HEALTH_QUERY_KEY = ["health"]; @@ -7,14 +7,14 @@ export const HEALTH_QUERY_SETTINGS_KEY = ["health", "settings"]; export const health = () => ({ queryKey: HEALTH_QUERY_KEY, - queryFn: async () => client.api.getHealth(), + queryFn: async () => API.getHealth(), }); export const refreshHealth = (queryClient: QueryClient) => { return { mutationFn: async () => { await queryClient.cancelQueries(HEALTH_QUERY_KEY); - const newHealthData = await client.api.getHealth(true); + const newHealthData = await API.getHealth(true); queryClient.setQueryData(HEALTH_QUERY_KEY, newHealthData); }, }; @@ -23,7 +23,7 @@ export const refreshHealth = (queryClient: QueryClient) => { export const healthSettings = () => { return { queryKey: HEALTH_QUERY_SETTINGS_KEY, - queryFn: client.api.getHealthSettings, + queryFn: API.getHealthSettings, }; }; @@ -36,7 +36,7 @@ export const updateHealthSettings = ( unknown > => { return { - mutationFn: client.api.updateHealthSettings, + mutationFn: API.updateHealthSettings, onSuccess: async (_, newSettings) => { await queryClient.invalidateQueries(HEALTH_QUERY_KEY); queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, newSettings); diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index e4ef76c623b34..fa4d37967af18 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -1,29 +1,29 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const deploymentConfig = () => { return { queryKey: ["deployment", "config"], - queryFn: client.api.getDeploymentConfig, + queryFn: API.getDeploymentConfig, }; }; export const deploymentDAUs = () => { return { queryKey: ["deployment", "daus"], - queryFn: () => client.api.getDeploymentDAUs(), + queryFn: () => API.getDeploymentDAUs(), }; }; export const deploymentStats = () => { return { queryKey: ["deployment", "stats"], - queryFn: client.api.getDeploymentStats, + queryFn: API.getDeploymentStats, }; }; export const deploymentSSHConfig = () => { return { queryKey: ["deployment", "sshConfig"], - queryFn: client.api.getDeploymentSSHConfig, + queryFn: API.getDeploymentSSHConfig, }; }; diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts index a3b3d2201c28c..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 { client } from "api/api"; +import { API } from "api/api"; import type { Entitlements } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; @@ -10,13 +10,13 @@ export const entitlements = (metadata: MetadataState<Entitlements>) => { return cachedQuery({ metadata, queryKey: entitlementsQueryKey, - queryFn: () => client.api.getEntitlements(), + queryFn: () => API.getEntitlements(), }); }; export const refreshEntitlements = (queryClient: QueryClient) => { return { - mutationFn: client.api.refreshEntitlements, + mutationFn: API.refreshEntitlements, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: entitlementsQueryKey, diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index fd81196a906b6..86fd9096ae9f2 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; import type { Experiments } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; @@ -9,13 +9,13 @@ export const experiments = (metadata: MetadataState<Experiments>) => { return cachedQuery({ metadata, queryKey: experimentsKey, - queryFn: () => client.api.getExperiments(), + queryFn: () => API.getExperiments(), }); }; export const availableExperiments = () => { return { queryKey: ["availableExperiments"], - queryFn: async () => client.api.getAvailableExperiments(), + queryFn: async () => API.getAvailableExperiments(), }; }; diff --git a/site/src/api/queries/externalAuth.ts b/site/src/api/queries/externalAuth.ts index d3d905d9844d3..eda68713aa5fc 100644 --- a/site/src/api/queries/externalAuth.ts +++ b/site/src/api/queries/externalAuth.ts @@ -1,25 +1,25 @@ import type { QueryClient, UseMutationOptions } from "react-query"; -import { client } from "api/api"; +import { API } from "api/api"; import type { ExternalAuth } from "api/typesGenerated"; // Returns all configured external auths for a given user. export const externalAuths = () => { return { queryKey: ["external-auth"], - queryFn: () => client.api.getUserExternalAuthProviders(), + queryFn: () => API.getUserExternalAuthProviders(), }; }; export const externalAuthProvider = (providerId: string) => { return { queryKey: ["external-auth", providerId], - queryFn: () => client.api.getExternalAuthProvider(providerId), + queryFn: () => API.getExternalAuthProvider(providerId), }; }; export const externalAuthDevice = (providerId: string) => { return { - queryFn: () => client.api.getExternalAuthDevice(providerId), + queryFn: () => API.getExternalAuthDevice(providerId), queryKey: ["external-auth", providerId, "device"], }; }; @@ -31,7 +31,7 @@ export const exchangeExternalAuthDevice = ( ) => { return { queryFn: () => - client.api.exchangeExternalAuthDevice(providerId, { + API.exchangeExternalAuthDevice(providerId, { device_code: deviceCode, }), queryKey: ["external-auth", providerId, "device", deviceCode], @@ -46,7 +46,7 @@ export const validateExternalAuth = ( queryClient: QueryClient, ): UseMutationOptions<ExternalAuth, unknown, string> => { return { - mutationFn: client.api.getExternalAuthProvider, + mutationFn: API.getExternalAuthProvider, onSuccess: (data, providerId) => { queryClient.setQueryData(["external-auth", providerId], data); }, @@ -55,7 +55,7 @@ export const validateExternalAuth = ( export const unlinkExternalAuths = (queryClient: QueryClient) => { return { - mutationFn: client.api.unlinkExternalAuthProvider, + mutationFn: API.unlinkExternalAuthProvider, onSuccess: async () => { await queryClient.invalidateQueries(["external-auth"]); }, diff --git a/site/src/api/queries/files.ts b/site/src/api/queries/files.ts index c8e30e3b9bd0d..a363e03f94473 100644 --- a/site/src/api/queries/files.ts +++ b/site/src/api/queries/files.ts @@ -1,14 +1,14 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const uploadFile = () => { return { - mutationFn: client.api.uploadFile, + mutationFn: API.uploadFile, }; }; export const file = (fileId: string) => { return { queryKey: ["files", fileId], - queryFn: () => client.api.getFile(fileId), + queryFn: () => API.getFile(fileId), }; }; diff --git a/site/src/api/queries/groups.ts b/site/src/api/queries/groups.ts index 1bd4d1554ca1b..5c34758df069f 100644 --- a/site/src/api/queries/groups.ts +++ b/site/src/api/queries/groups.ts @@ -1,5 +1,5 @@ import type { QueryClient, UseQueryOptions } from "react-query"; -import { client } from "api/api"; +import { API } from "api/api"; import type { CreateGroupRequest, Group, @@ -14,14 +14,14 @@ const getGroupQueryKey = (groupId: string) => ["group", groupId]; export const groups = (organizationId: string) => { return { queryKey: GROUPS_QUERY_KEY, - queryFn: () => client.api.getGroups(organizationId), + queryFn: () => API.getGroups(organizationId), } satisfies UseQueryOptions<Group[]>; }; export const group = (groupId: string) => { return { queryKey: getGroupQueryKey(groupId), - queryFn: () => client.api.getGroup(groupId), + queryFn: () => API.getGroup(groupId), }; }; @@ -71,7 +71,7 @@ export const groupPermissions = (groupId: string) => { return { queryKey: [...getGroupQueryKey(groupId), "permissions"], queryFn: () => - client.api.checkAuthorization({ + API.checkAuthorization({ checks: { canUpdateGroup: { object: { @@ -91,7 +91,7 @@ export const createGroup = (queryClient: QueryClient) => { organizationId, ...request }: CreateGroupRequest & { organizationId: string }) => - client.api.createGroup(organizationId, request), + API.createGroup(organizationId, request), onSuccess: async () => { await queryClient.invalidateQueries(GROUPS_QUERY_KEY); }, @@ -104,7 +104,7 @@ export const patchGroup = (queryClient: QueryClient) => { groupId, ...request }: PatchGroupRequest & { groupId: string }) => - client.api.patchGroup(groupId, request), + API.patchGroup(groupId, request), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; @@ -112,7 +112,7 @@ export const patchGroup = (queryClient: QueryClient) => { export const deleteGroup = (queryClient: QueryClient) => { return { - mutationFn: client.api.deleteGroup, + mutationFn: API.deleteGroup, onSuccess: async (_: void, groupId: string) => invalidateGroup(queryClient, groupId), }; @@ -121,7 +121,7 @@ export const deleteGroup = (queryClient: QueryClient) => { export const addMember = (queryClient: QueryClient) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => - client.api.addMember(groupId, userId), + API.addMember(groupId, userId), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; @@ -130,7 +130,7 @@ export const addMember = (queryClient: QueryClient) => { export const removeMember = (queryClient: QueryClient) => { return { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => - client.api.removeMember(groupId, userId), + API.removeMember(groupId, userId), onSuccess: async (updatedGroup: Group) => invalidateGroup(queryClient, updatedGroup.id), }; diff --git a/site/src/api/queries/insights.ts b/site/src/api/queries/insights.ts index 7e2f5bd5b9c0a..4b6dad8cd2fc8 100644 --- a/site/src/api/queries/insights.ts +++ b/site/src/api/queries/insights.ts @@ -1,26 +1,22 @@ -import { - type InsightsParams, - type InsightsTemplateParams, - client, -} from "api/api"; +import { type InsightsParams, type InsightsTemplateParams, API } from "api/api"; export const insightsTemplate = (params: InsightsTemplateParams) => { return { queryKey: ["insights", "templates", params.template_ids, params], - queryFn: () => client.api.getInsightsTemplate(params), + queryFn: () => API.getInsightsTemplate(params), }; }; export const insightsUserLatency = (params: InsightsParams) => { return { queryKey: ["insights", "userLatency", params.template_ids, params], - queryFn: () => client.api.getInsightsUserLatency(params), + queryFn: () => API.getInsightsUserLatency(params), }; }; export const insightsUserActivity = (params: InsightsParams) => { return { queryKey: ["insights", "userActivity", params.template_ids, params], - queryFn: () => client.api.getInsightsUserActivity(params), + queryFn: () => API.getInsightsUserActivity(params), }; }; diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts index 0e26194b80b22..c0e7f6f28ce9d 100644 --- a/site/src/api/queries/integrations.ts +++ b/site/src/api/queries/integrations.ts @@ -1,9 +1,9 @@ import type { GetJFrogXRayScanParams } from "api/api"; -import { client } from "api/api"; +import { API } from "api/api"; export const xrayScan = (params: GetJFrogXRayScanParams) => { return { queryKey: ["xray", params], - queryFn: () => client.api.getJFrogXRayScan(params), + queryFn: () => API.getJFrogXRayScan(params), }; }; diff --git a/site/src/api/queries/oauth2.ts b/site/src/api/queries/oauth2.ts index 06f8f9d9b4097..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 { client } from "api/api"; +import { API } from "api/api"; import type * as TypesGen from "api/typesGenerated"; const appsKey = ["oauth2-provider", "apps"]; @@ -10,20 +10,20 @@ const appSecretsKey = (appId: string) => appKey(appId).concat("secrets"); export const getApps = (userId?: string) => { return { queryKey: userId ? appsKey.concat(userId) : appsKey, - queryFn: () => client.api.getOAuth2ProviderApps({ user_id: userId }), + queryFn: () => API.getOAuth2ProviderApps({ user_id: userId }), }; }; export const getApp = (id: string) => { return { queryKey: appKey(id), - queryFn: () => client.api.getOAuth2ProviderApp(id), + queryFn: () => API.getOAuth2ProviderApp(id), }; }; export const postApp = (queryClient: QueryClient) => { return { - mutationFn: client.api.postOAuth2ProviderApp, + mutationFn: API.postOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: appsKey, @@ -40,7 +40,7 @@ export const putApp = (queryClient: QueryClient) => { }: { id: string; req: TypesGen.PutOAuth2ProviderAppRequest; - }) => client.api.putOAuth2ProviderApp(id, req), + }) => API.putOAuth2ProviderApp(id, req), onSuccess: async (app: TypesGen.OAuth2ProviderApp) => { await queryClient.invalidateQueries({ queryKey: appKey(app.id), @@ -51,7 +51,7 @@ export const putApp = (queryClient: QueryClient) => { export const deleteApp = (queryClient: QueryClient) => { return { - mutationFn: client.api.deleteOAuth2ProviderApp, + mutationFn: API.deleteOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: appsKey, @@ -63,13 +63,13 @@ export const deleteApp = (queryClient: QueryClient) => { export const getAppSecrets = (id: string) => { return { queryKey: appSecretsKey(id), - queryFn: () => client.api.getOAuth2ProviderAppSecrets(id), + queryFn: () => API.getOAuth2ProviderAppSecrets(id), }; }; export const postAppSecret = (queryClient: QueryClient) => { return { - mutationFn: client.api.postOAuth2ProviderAppSecret, + mutationFn: API.postOAuth2ProviderAppSecret, onSuccess: async ( _: TypesGen.OAuth2ProviderAppSecretFull, appId: string, @@ -84,7 +84,7 @@ export const postAppSecret = (queryClient: QueryClient) => { export const deleteAppSecret = (queryClient: QueryClient) => { return { mutationFn: ({ appId, secretId }: { appId: string; secretId: string }) => - client.api.deleteOAuth2ProviderAppSecret(appId, secretId), + API.deleteOAuth2ProviderAppSecret(appId, secretId), onSuccess: async (_: void, { appId }: { appId: string }) => { await queryClient.invalidateQueries({ queryKey: appSecretsKey(appId), @@ -95,7 +95,7 @@ export const deleteAppSecret = (queryClient: QueryClient) => { export const revokeApp = (queryClient: QueryClient, userId: string) => { return { - mutationFn: client.api.revokeOAuth2ProviderApp, + mutationFn: API.revokeOAuth2ProviderApp, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: userAppsKey(userId), diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 7bfe89162316b..2a6c1700b53a7 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -1,8 +1,8 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const roles = () => { return { queryKey: ["roles"], - queryFn: client.api.getRoles, + queryFn: API.getRoles, }; }; diff --git a/site/src/api/queries/settings.ts b/site/src/api/queries/settings.ts index bd4ef7f944dce..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 { client } from "api/api"; +import { API } from "api/api"; import type { UpdateUserQuietHoursScheduleRequest, UserQuietHoursScheduleResponse, @@ -16,7 +16,7 @@ export const userQuietHoursSchedule = ( ): QueryOptions<UserQuietHoursScheduleResponse> => { return { queryKey: userQuietHoursScheduleKey(userId), - queryFn: () => client.api.getUserQuietHoursSchedule(userId), + queryFn: () => API.getUserQuietHoursSchedule(userId), }; }; @@ -26,7 +26,7 @@ export const updateUserQuietHoursSchedule = ( ) => { return { mutationFn: (request: UpdateUserQuietHoursScheduleRequest) => - client.api.updateUserQuietHoursSchedule(userId, request), + API.updateUserQuietHoursSchedule(userId, request), onSuccess: async () => { await queryClient.invalidateQueries(userQuietHoursScheduleKey(userId)); }, diff --git a/site/src/api/queries/sshKeys.ts b/site/src/api/queries/sshKeys.ts index 878a80523863e..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 { client } from "api/api"; +import { API } from "api/api"; import type { GitSSHKey } from "api/typesGenerated"; const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"]; @@ -7,7 +7,7 @@ const getUserSSHKeyQueryKey = (userId: string) => [userId, "sshKey"]; export const userSSHKey = (userId: string) => { return { queryKey: getUserSSHKeyQueryKey(userId), - queryFn: () => client.api.getUserSSHKey(userId), + queryFn: () => API.getUserSSHKey(userId), }; }; @@ -16,7 +16,7 @@ export const regenerateUserSSHKey = ( queryClient: QueryClient, ) => { return { - mutationFn: () => client.api.regenerateUserSSHKey(userId), + mutationFn: () => API.regenerateUserSSHKey(userId), onSuccess: (newKey: GitSSHKey) => { queryClient.setQueryData(getUserSSHKeyQueryKey(userId), newKey); }, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 7e485a0e039b4..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 { client } from "api/api"; +import { API } from "api/api"; import type { CreateTemplateRequest, CreateTemplateVersionRequest, @@ -26,7 +26,7 @@ export const templateByName = ( ): QueryOptions<Template> => { return { queryKey: templateByNameKey(organizationId, name), - queryFn: async () => client.api.getTemplateByName(organizationId, name), + queryFn: async () => API.getTemplateByName(organizationId, name), }; }; @@ -39,27 +39,27 @@ const getTemplatesQueryKey = (organizationId: string, deprecated?: boolean) => [ export const templates = (organizationId: string, deprecated?: boolean) => { return { queryKey: getTemplatesQueryKey(organizationId, deprecated), - queryFn: () => client.api.getTemplates(organizationId, { deprecated }), + queryFn: () => API.getTemplates(organizationId, { deprecated }), }; }; export const templateACL = (templateId: string) => { return { queryKey: ["templateAcl", templateId], - queryFn: () => client.api.getTemplateACL(templateId), + queryFn: () => API.getTemplateACL(templateId), }; }; export const setUserRole = ( queryClient: QueryClient, ): MutationOptions< - Awaited<ReturnType<typeof client.api.updateTemplateACL>>, + Awaited<ReturnType<typeof API.updateTemplateACL>>, unknown, { templateId: string; userId: string; role: TemplateRole } > => { return { mutationFn: ({ templateId, userId, role }) => - client.api.updateTemplateACL(templateId, { + API.updateTemplateACL(templateId, { user_perms: { [userId]: role, }, @@ -73,13 +73,13 @@ export const setUserRole = ( export const setGroupRole = ( queryClient: QueryClient, ): MutationOptions< - Awaited<ReturnType<typeof client.api.updateTemplateACL>>, + Awaited<ReturnType<typeof API.updateTemplateACL>>, unknown, { templateId: string; groupId: string; role: TemplateRole } > => { return { mutationFn: ({ templateId, groupId, role }) => - client.api.updateTemplateACL(templateId, { + API.updateTemplateACL(templateId, { group_perms: { [groupId]: role, }, @@ -93,14 +93,14 @@ export const setGroupRole = ( export const templateExamples = (organizationId: string) => { return { queryKey: [...getTemplatesQueryKey(organizationId), "examples"], - queryFn: () => client.api.getTemplateExamples(organizationId), + queryFn: () => API.getTemplateExamples(organizationId), }; }; export const templateVersion = (versionId: string) => { return { queryKey: ["templateVersion", versionId], - queryFn: () => client.api.getTemplateVersion(versionId), + queryFn: () => API.getTemplateVersion(versionId), }; }; @@ -112,18 +112,14 @@ export const templateVersionByName = ( return { queryKey: ["templateVersion", organizationId, templateName, versionName], queryFn: () => - client.api.getTemplateVersionByName( - organizationId, - templateName, - versionName, - ), + API.getTemplateVersionByName(organizationId, templateName, versionName), }; }; export const templateVersions = (templateId: string) => { return { queryKey: ["templateVersions", templateId], - queryFn: () => client.api.getTemplateVersions(templateId), + queryFn: () => API.getTemplateVersions(templateId), }; }; @@ -136,14 +132,14 @@ export const templateVersionVariablesKey = (versionId: string) => [ export const templateVersionVariables = (versionId: string) => { return { queryKey: templateVersionVariablesKey(versionId), - queryFn: () => client.api.getTemplateVersionVariables(versionId), + queryFn: () => API.getTemplateVersionVariables(versionId), }; }; export const createTemplateVersion = (organizationId: string) => { return { mutationFn: async (request: CreateTemplateVersionRequest) => { - const newVersion = await client.api.createTemplateVersion( + const newVersion = await API.createTemplateVersion( organizationId, request, ); @@ -155,7 +151,7 @@ export const createTemplateVersion = (organizationId: string) => { export const createAndBuildTemplateVersion = (organizationId: string) => { return { mutationFn: async (request: CreateTemplateVersionRequest) => { - const newVersion = await client.api.createTemplateVersion( + const newVersion = await API.createTemplateVersion( organizationId, request, ); @@ -171,7 +167,7 @@ export const updateActiveTemplateVersion = ( ) => { return { mutationFn: (versionId: string) => - client.api.updateActiveTemplateVersion(template.id, { + API.updateActiveTemplateVersion(template.id, { id: versionId, }), onSuccess: async () => { @@ -189,7 +185,7 @@ export const templaceACLAvailable = ( ) => { return { queryKey: ["template", templateId, "aclAvailable", options], - queryFn: () => client.api.getTemplateACLAvailable(templateId, options), + queryFn: () => API.getTemplateACLAvailable(templateId, options), }; }; @@ -202,7 +198,7 @@ export const templateVersionExternalAuthKey = (versionId: string) => [ export const templateVersionExternalAuth = (versionId: string) => { return { queryKey: templateVersionExternalAuthKey(versionId), - queryFn: () => client.api.getTemplateVersionExternalAuth(versionId), + queryFn: () => API.getTemplateVersionExternalAuth(versionId), }; }; @@ -221,13 +217,13 @@ export type CreateTemplateOptions = { }; const createTemplateFn = async (options: CreateTemplateOptions) => { - const version = await client.api.createTemplateVersion( + const version = await API.createTemplateVersion( options.organizationId, options.version, ); options.onCreateVersion?.(version); await waitBuildToBeFinished(version, options.onTemplateVersionChanges); - return client.api.createTemplate(options.organizationId, { + return API.createTemplate(options.organizationId, { ...options.template, template_version_id: version.id, }); @@ -236,21 +232,21 @@ const createTemplateFn = async (options: CreateTemplateOptions) => { export const templateVersionLogs = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "logs"], - queryFn: () => client.api.getTemplateVersionLogs(versionId), + queryFn: () => API.getTemplateVersionLogs(versionId), }; }; export const richParameters = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "richParameters"], - queryFn: () => client.api.getTemplateVersionRichParameters(versionId), + queryFn: () => API.getTemplateVersionRichParameters(versionId), }; }; export const resources = (versionId: string) => { return { queryKey: ["templateVersion", versionId, "resources"], - queryFn: () => client.api.getTemplateVersionResources(versionId), + queryFn: () => API.getTemplateVersionResources(versionId), }; }; @@ -258,7 +254,7 @@ export const templateFiles = (fileId: string) => { return { queryKey: ["templateFiles", fileId], queryFn: async () => { - const tarFile = await client.api.getFile(fileId); + const tarFile = await API.getFile(fileId); return getTemplateVersionFiles(tarFile); }, }; @@ -278,7 +274,7 @@ export const previousTemplateVersion = ( "previous", ], queryFn: async () => { - const result = await client.api.getPreviousTemplateVersionByName( + const result = await API.getPreviousTemplateVersionByName( organizationId, templateName, versionName, @@ -298,7 +294,7 @@ const waitBuildToBeFinished = async ( do { // When pending we want to poll more frequently await delay(jobStatus === "pending" ? 250 : 1000); - data = await client.api.getTemplateVersion(version.id); + data = await API.getTemplateVersion(version.id); onRequest?.(data); jobStatus = data.job.status; diff --git a/site/src/api/queries/updateCheck.ts b/site/src/api/queries/updateCheck.ts index 6c7a2add42ef4..e8dc1b2cc3e41 100644 --- a/site/src/api/queries/updateCheck.ts +++ b/site/src/api/queries/updateCheck.ts @@ -1,8 +1,8 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const updateCheck = () => { return { queryKey: ["updateCheck"], - queryFn: () => client.api.getUpdateCheck(), + queryFn: () => API.getUpdateCheck(), }; }; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 982c1ca78717c..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 { client } from "api/api"; +import { API } from "api/api"; import type { AuthorizationRequest, GetUsersResponse, @@ -41,14 +41,14 @@ export function paginatedUsers( }, queryKey: ({ payload }) => usersKey(payload), - queryFn: ({ payload, signal }) => client.api.getUsers(payload, signal), + queryFn: ({ payload, signal }) => API.getUsers(payload, signal), }; } export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => { return { queryKey: usersKey(req), - queryFn: ({ signal }) => client.api.getUsers(req, signal), + queryFn: ({ signal }) => API.getUsers(req, signal), cacheTime: 5 * 1000 * 60, }; }; @@ -59,13 +59,13 @@ export const updatePassword = () => { userId, ...request }: UpdateUserPasswordRequest & { userId: string }) => - client.api.updateUserPassword(userId, request), + API.updateUserPassword(userId, request), }; }; export const createUser = (queryClient: QueryClient) => { return { - mutationFn: client.api.createUser, + mutationFn: API.createUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -74,13 +74,13 @@ export const createUser = (queryClient: QueryClient) => { export const createFirstUser = () => { return { - mutationFn: client.api.createFirstUser, + mutationFn: API.createFirstUser, }; }; export const suspendUser = (queryClient: QueryClient) => { return { - mutationFn: client.api.suspendUser, + mutationFn: API.suspendUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -89,7 +89,7 @@ export const suspendUser = (queryClient: QueryClient) => { export const activateUser = (queryClient: QueryClient) => { return { - mutationFn: client.api.activateUser, + mutationFn: API.activateUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -98,7 +98,7 @@ export const activateUser = (queryClient: QueryClient) => { export const deleteUser = (queryClient: QueryClient) => { return { - mutationFn: client.api.deleteUser, + mutationFn: API.deleteUser, onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -108,7 +108,7 @@ export const deleteUser = (queryClient: QueryClient) => { export const updateRoles = (queryClient: QueryClient) => { return { mutationFn: ({ userId, roles }: { userId: string; roles: string[] }) => - client.api.updateUserRoles(roles, userId), + API.updateUserRoles(roles, userId), onSuccess: async () => { await queryClient.invalidateQueries(["users"]); }, @@ -120,7 +120,7 @@ export const authMethods = () => { // Even the endpoint being /users/authmethods we don't want to revalidate it // when users change so its better add a unique query key queryKey: ["authMethods"], - queryFn: client.api.getAuthMethods, + queryFn: API.getAuthMethods, }; }; @@ -130,14 +130,14 @@ export const me = (metadata: MetadataState<User>) => { return cachedQuery({ metadata, queryKey: meKey, - queryFn: client.api.getAuthenticatedUser, + queryFn: API.getAuthenticatedUser, }); }; export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> { return { queryKey: [...meKey, "apiKey"], - queryFn: () => client.api.getApiKey(), + queryFn: () => API.getApiKey(), }; } @@ -145,7 +145,7 @@ export const hasFirstUser = (userMetadata: MetadataState<User>) => { return cachedQuery({ metadata: userMetadata, queryKey: ["hasFirstUser"], - queryFn: client.api.hasFirstUser, + queryFn: API.hasFirstUser, }); }; @@ -175,10 +175,10 @@ const loginFn = async ({ password: string; authorization: AuthorizationRequest; }) => { - await client.api.login(email, password); + await API.login(email, password); const [user, permissions] = await Promise.all([ - client.api.getAuthenticatedUser(), - client.api.checkAuthorization(authorization), + API.getAuthenticatedUser(), + API.checkAuthorization(authorization), ]); return { user, @@ -188,7 +188,7 @@ const loginFn = async ({ export const logout = (queryClient: QueryClient) => { return { - mutationFn: client.api.logout, + mutationFn: API.logout, onSuccess: () => { /** * 2024-05-02 - If we persist any form of user data after the user logs @@ -214,7 +214,7 @@ export const logout = (queryClient: QueryClient) => { export const updateProfile = (userId: string) => { return { mutationFn: (req: UpdateUserProfileRequest) => - client.api.updateProfile(userId, req), + API.updateProfile(userId, req), }; }; @@ -228,7 +228,7 @@ export const updateAppearanceSettings = ( unknown > => { return { - mutationFn: (req) => client.api.updateAppearanceSettings(userId, req), + mutationFn: (req) => API.updateAppearanceSettings(userId, req), onMutate: async (patch) => { // Mutate the `queryClient` optimistically to make the theme switcher // more responsive. diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index d4cc127c6814e..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 { client } from "api/api"; +import { API } from "api/api"; import type { WorkspaceBuild, WorkspaceBuildParameter, @@ -13,7 +13,7 @@ export function workspaceBuildParametersKey(workspaceBuildId: string) { export function workspaceBuildParameters(workspaceBuildId: string) { return { queryKey: workspaceBuildParametersKey(workspaceBuildId), - queryFn: () => client.api.getWorkspaceBuildParameters(workspaceBuildId), + queryFn: () => API.getWorkspaceBuildParameters(workspaceBuildId), } as const satisfies QueryOptions<WorkspaceBuildParameter[]>; } @@ -25,11 +25,7 @@ export const workspaceBuildByNumber = ( return { queryKey: ["workspaceBuild", username, workspaceName, buildNumber], queryFn: () => - client.api.getWorkspaceBuildByNumber( - username, - workspaceName, - buildNumber, - ), + API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber), }; }; @@ -53,7 +49,7 @@ export const infiniteWorkspaceBuilds = ( return pages.length + 1; }, queryFn: ({ pageParam = 0 }) => { - return client.api.getWorkspaceBuilds(workspaceId, { + return API.getWorkspaceBuilds(workspaceId, { limit, offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit, }); diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index ec27415b7ce2e..1735b0f71279b 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const getWorkspaceQuotaQueryKey = (username: string) => [ username, @@ -8,7 +8,7 @@ export const getWorkspaceQuotaQueryKey = (username: string) => [ export const workspaceQuota = (username: string) => { return { queryKey: getWorkspaceQuotaQueryKey(username), - queryFn: () => client.api.getWorkspaceQuota(username), + queryFn: () => API.getWorkspaceQuota(username), }; }; @@ -20,6 +20,6 @@ export const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [ export const workspaceResolveAutostart = (workspaceId: string) => { return { queryKey: getWorkspaceResolveAutostartQueryKey(workspaceId), - queryFn: () => client.api.getWorkspaceResolveAutostart(workspaceId), + queryFn: () => API.getWorkspaceResolveAutostart(workspaceId), }; }; diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts index fd37af90fa926..60bd99285aa54 100644 --- a/site/src/api/queries/workspaceportsharing.ts +++ b/site/src/api/queries/workspaceportsharing.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; import type { DeleteWorkspaceAgentPortShareRequest, UpsertWorkspaceAgentPortShareRequest, @@ -7,14 +7,14 @@ import type { export const workspacePortShares = (workspaceId: string) => { return { queryKey: ["sharedPorts", workspaceId], - queryFn: () => client.api.getWorkspaceAgentSharedPorts(workspaceId), + queryFn: () => API.getWorkspaceAgentSharedPorts(workspaceId), }; }; export const upsertWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { - await client.api.upsertWorkspaceAgentSharedPort(workspaceId, options); + await API.upsertWorkspaceAgentSharedPort(workspaceId, options); }, }; }; @@ -22,7 +22,7 @@ export const upsertWorkspacePortShare = (workspaceId: string) => { export const deleteWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { - await client.api.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 da68598cc6358..95df3b7f592f6 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -4,7 +4,7 @@ import type { QueryOptions, UseMutationOptions, } from "react-query"; -import { type DeleteWorkspaceOptions, client } from "api/api"; +import { type DeleteWorkspaceOptions, API } from "api/api"; import type { CreateWorkspaceRequest, ProvisionerLogLevel, @@ -27,7 +27,7 @@ export const workspaceByOwnerAndName = (owner: string, name: string) => { return { queryKey: workspaceByOwnerAndNameKey(owner, name), queryFn: () => - client.api.getWorkspaceByOwnerAndName(owner, name, { + API.getWorkspaceByOwnerAndName(owner, name, { include_deleted: true, }), }; @@ -50,7 +50,7 @@ export const createWorkspace = (queryClient: QueryClient) => { return { mutationFn: async (variables: CreateWorkspaceMutationVariables) => { const { userId, organizationId, ...req } = variables; - return client.api.createWorkspace(organizationId, userId, req); + return API.createWorkspace(organizationId, userId, req); }, onSuccess: async () => { await queryClient.invalidateQueries(["workspaces"]); @@ -72,14 +72,14 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => { if (versionId) { templateVersionParameters = { template_version_id: versionId }; } else { - const template = await client.api.getTemplateByName( + const template = await API.getTemplateByName( organizationId, templateName, ); templateVersionParameters = { template_id: template.id }; } - return client.api.createWorkspace(organizationId, "me", { + return API.createWorkspace(organizationId, "me", { ...templateVersionParameters, name: defaultName, rich_parameter_values: defaultBuildParameters, @@ -103,7 +103,7 @@ export function workspaces(config: WorkspacesRequest = {}) { return { queryKey: workspacesKey(config), - queryFn: () => client.api.getWorkspaces({ q, limit }), + queryFn: () => API.getWorkspaces({ q, limit }), } as const satisfies QueryOptions<WorkspacesResponse>; } @@ -112,7 +112,7 @@ export const updateDeadline = ( ): UseMutationOptions<void, unknown, Dayjs> => { return { mutationFn: (deadline: Dayjs) => { - return client.api.putWorkspaceExtension(workspace.id, deadline); + return API.putWorkspaceExtension(workspace.id, deadline); }, }; }; @@ -129,11 +129,7 @@ export const changeVersion = ( versionId: string; buildParameters?: WorkspaceBuildParameter[]; }) => { - return client.api.changeWorkspaceVersion( - workspace, - versionId, - buildParameters, - ); + return API.changeWorkspaceVersion(workspace, versionId, buildParameters); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -147,7 +143,7 @@ export const updateWorkspace = ( ) => { return { mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { - return client.api.updateWorkspace(workspace, buildParameters); + return API.updateWorkspace(workspace, buildParameters); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -161,7 +157,7 @@ export const deleteWorkspace = ( ) => { return { mutationFn: (options: DeleteWorkspaceOptions) => { - return client.api.deleteWorkspace(workspace.id, options); + return API.deleteWorkspace(workspace.id, options); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -175,7 +171,7 @@ export const stopWorkspace = ( ) => { return { mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { - return client.api.stopWorkspace(workspace.id, logLevel); + return API.stopWorkspace(workspace.id, logLevel); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -195,7 +191,7 @@ export const startWorkspace = ( buildParameters?: WorkspaceBuildParameter[]; logLevel?: ProvisionerLogLevel; }) => { - return client.api.startWorkspace( + return API.startWorkspace( workspace.id, workspace.latest_build.template_version_id, logLevel, @@ -211,7 +207,7 @@ export const startWorkspace = ( export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { - return client.api.cancelWorkspaceBuild(workspace.latest_build.id); + return API.cancelWorkspaceBuild(workspace.latest_build.id); }, onSuccess: async () => { await queryClient.invalidateQueries({ @@ -224,7 +220,7 @@ export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { export const activate = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { - return client.api.updateWorkspaceDormancy(workspace.id, false); + return API.updateWorkspaceDormancy(workspace.id, false); }, onSuccess: (updatedWorkspace: Workspace) => { queryClient.setQueryData( @@ -268,9 +264,9 @@ export const toggleFavorite = ( return { mutationFn: () => { if (workspace.favorite) { - return client.api.deleteFavoriteWorkspace(workspace.id); + return API.deleteFavoriteWorkspace(workspace.id); } else { - return client.api.putFavoriteWorkspace(workspace.id); + return API.putFavoriteWorkspace(workspace.id); } }, onSuccess: async () => { diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index fe44940880380..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 { client } 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 client.api.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 client.api.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 e05c1c04e8e55..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 { client } 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"; @@ -109,8 +109,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => { queryKey: ["get-proxies"], queryFn: async (): Promise<readonly Region[]> => { const apiCall = permissions.editWorkspaceProxies - ? client.api.getWorkspaceProxies - : client.api.getWorkspaceProxyRegions; + ? 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 682320e10aa27..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 { client } 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,7 +18,7 @@ export const RequireAuth: FC = () => { return; } - const axiosInstance = client.getAxiosInstance(); + 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 fb3d03d0269e4..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 { client } from "api/api"; +import { API } from "api/api"; import type { Region } from "api/typesGenerated"; import { generateRandomString } from "utils/random"; @@ -197,7 +197,7 @@ export const useProxyLatency = ( // The resource requests include xmlhttp requests. observer.observe({ entryTypes: ["resource"] }); - const axiosInstance = client.getAxiosInstance(); + const axiosInstance = API.getAxiosInstance(); const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => { return axiosInstance.get(latencyURL, { withCredentials: false, diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index bfbc03f4a9a91..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 { client } 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<AppLinkProps> = ({ app, workspace, agent }) => { let url = href; if (hasMagicToken !== -1) { setFetchingSessionToken(true); - const key = await client.api.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 984b21fa6632e..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 { client } from "api/api"; +import { API } from "api/api"; import { deleteWorkspacePortShare, upsertWorkspacePortShare, @@ -70,7 +70,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => { const portsQuery = useQuery({ queryKey: ["portForward", agent.id], - queryFn: () => client.api.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 36b6f5822f804..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 { client } 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,8 +119,7 @@ const VSCodeButton: FC<VSCodeDesktopButtonProps> = ({ disabled={loading} onClick={() => { setLoading(true); - client.api - .getApiKey() + API.getApiKey() .then(({ key }) => { const query = new URLSearchParams({ owner: userName, @@ -164,8 +163,7 @@ const VSCodeInsidersButton: FC<VSCodeDesktopButtonProps> = ({ disabled={loading} onClick={() => { setLoading(true); - client.api - .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 ea5e8b438606f..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 { client } from "api/api"; +import { API } from "api/api"; import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; import { MockAuditLog, @@ -61,12 +61,10 @@ describe("AuditPage", () => { it("renders page 5", async () => { // Given const page = 5; - const getAuditLogsSpy = jest - .spyOn(client.api, "getAuditLogs") - .mockResolvedValue({ - audit_logs: [MockAuditLog, MockAuditLog2], - count: 2, - }); + const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs").mockResolvedValue({ + audit_logs: [MockAuditLog, MockAuditLog2], + count: 2, + }); // When await renderPage({ page: page }); @@ -84,7 +82,7 @@ describe("AuditPage", () => { describe("Filtering", () => { it("filters by URL", async () => { const getAuditLogsSpy = jest - .spyOn(client.api, "getAuditLogs") + .spyOn(API, "getAuditLogs") .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 }); const query = "resource_type:workspace action:create"; @@ -100,7 +98,7 @@ describe("AuditPage", () => { it("resets page to 1 when filter is changed", async () => { await renderPage({ page: 2 }); - const getAuditLogsSpy = jest.spyOn(client.api, "getAuditLogs"); + const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs"); getAuditLogsSpy.mockClear(); const filterField = screen.getByLabelText("Filter"); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx index 1c28b4a1e3afb..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 { client } from "api/api"; +import { API } from "api/api"; import { MockTemplateExample, MockTemplateVersion, @@ -35,14 +35,14 @@ test("Create template from starter template", async () => { const { router, container } = await renderPage(searchParams); const form = container.querySelector("form") as HTMLFormElement; - jest.spyOn(client.api, "createTemplateVersion").mockResolvedValueOnce({ + jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({ ...MockTemplateVersion, job: { ...MockTemplateVersion.job, status: "pending", }, }); - jest.spyOn(client.api, "getTemplateVersion").mockResolvedValue({ + jest.spyOn(API, "getTemplateVersion").mockResolvedValue({ ...MockTemplateVersion, job: { ...MockTemplateVersion.job, @@ -51,7 +51,7 @@ test("Create template from starter template", async () => { }, }); jest - .spyOn(client.api, "getTemplateVersionVariables") + .spyOn(API, "getTemplateVersionVariables") .mockResolvedValue([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, @@ -85,42 +85,35 @@ test("Create template from starter template", async () => { // Setup the mock for the second template version creation before submit the form jest.clearAllMocks(); jest - .spyOn(client.api, "createTemplateVersion") + .spyOn(API, "createTemplateVersion") .mockResolvedValue(MockTemplateVersion); - jest - .spyOn(client.api, "getTemplateVersion") - .mockResolvedValue(MockTemplateVersion); - jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); + jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( within(form).getByRole("button", { name: /create template/i }), ); - await waitFor(() => expect(client.api.createTemplate).toBeCalledTimes(1)); + await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1)); expect(router.state.location.pathname).toEqual( `/templates/${MockTemplate.name}/files`, ); - expect(client.api.createTemplateVersion).toHaveBeenCalledWith( - MockOrganization.id, - { - example_id: "aws-windows", - provisioner: "terraform", - storage_method: "file", - tags: {}, - user_variable_values: [ - { name: "first_variable", value: "First value" }, - { name: "second_variable", value: "2" }, - { name: "third_variable", value: "true" }, - ], - }, - ); + expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, { + example_id: "aws-windows", + provisioner: "terraform", + storage_method: "file", + tags: {}, + user_variable_values: [ + { name: "first_variable", value: "First value" }, + { name: "second_variable", value: "2" }, + { name: "third_variable", value: "true" }, + ], + }); }); test("Create template from duplicating a template", async () => { - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); jest - .spyOn(client.api, "getTemplateVersion") - .mockResolvedValue(MockTemplateVersion); - jest - .spyOn(client.api, "getTemplateVersionVariables") + .spyOn(API, "getTemplateVersionVariables") .mockResolvedValue([MockTemplateVersionVariable1]); const searchParams = new URLSearchParams({ @@ -142,12 +135,10 @@ test("Create template from duplicating a template", async () => { ).toHaveValue(MockTemplateVersionVariable1.value); // Create template jest - .spyOn(client.api, "createTemplateVersion") - .mockResolvedValue(MockTemplateVersion); - jest - .spyOn(client.api, "getTemplateVersion") + .spyOn(API, "createTemplateVersion") .mockResolvedValue(MockTemplateVersion); - jest.spyOn(client.api, "createTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion); + jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( screen.getByRole("button", { name: /create template/i }), ); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx index 20f74d4c4dd79..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 { client } from "api/api"; +import { API } from "api/api"; import { renderWithAuth, waitForLoaderToBeRemoved, @@ -9,7 +9,7 @@ import { CreateTokenPage } from "./CreateTokenPage"; describe("TokenPage", () => { it("shows the success modal", async () => { - jest.spyOn(client.api, "createToken").mockResolvedValueOnce({ + jest.spyOn(API, "createToken").mockResolvedValueOnce({ key: "abcd", }); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx index a5883041b9299..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 { client } 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(client.api.createToken); + } = useMutation(API.createToken); const { data: tokenConfig, isLoading: fetchingTokenConfig, @@ -36,7 +36,7 @@ export const CreateTokenPage: FC = () => { error: tokenFetchError, } = useQuery({ queryKey: ["tokenconfig"], - queryFn: client.api.getTokenConfig, + queryFn: API.getTokenConfig, }); const [formError, setFormError] = useState<unknown>(undefined); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 47c29e4ba91b7..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 { client } from "api/api"; +import { API } from "api/api"; import { MockTemplate, MockUser, @@ -36,16 +36,14 @@ const renderCreateWorkspacePage = () => { describe("CreateWorkspacePage", () => { it("succeeds with default owner", async () => { jest - .spyOn(client.api, "getUsers") + .spyOn(API, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); jest - .spyOn(client.api, "getWorkspaceQuota") + .spyOn(API, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); + jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(client.api, "createWorkspace") - .mockResolvedValueOnce(MockWorkspace); - jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]); renderCreateWorkspacePage(); @@ -61,7 +59,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(client.api.createWorkspace).toBeCalledWith( + expect(API.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -75,7 +73,7 @@ describe("CreateWorkspacePage", () => { const param = "first_parameter"; const paramValue = "It works!"; jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]); renderWithAuth(<CreateWorkspacePage />, { @@ -91,7 +89,7 @@ describe("CreateWorkspacePage", () => { it("rich parameter: number validation fails", async () => { jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter2, @@ -126,7 +124,7 @@ describe("CreateWorkspacePage", () => { it("rich parameter: string validation fails", async () => { jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter3, @@ -159,16 +157,14 @@ describe("CreateWorkspacePage", () => { }); it("rich parameter: number validation fails with custom error", async () => { - jest - .spyOn(client.api, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([ - MockTemplateVersionParameter1, - { - ...MockTemplateVersionParameter2, - validation_error: "These are values: {min}, {max}, and {value}.", - validation_monotonic: undefined, // only needs min-max rules - }, - ]); + jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ + MockTemplateVersionParameter1, + { + ...MockTemplateVersionParameter2, + validation_error: "These are values: {min}, {max}, and {value}.", + validation_monotonic: undefined, // only needs min-max rules + }, + ]); renderCreateWorkspacePage(); await waitForLoaderToBeRemoved(); @@ -191,16 +187,14 @@ describe("CreateWorkspacePage", () => { it("external auth authenticates and succeeds", async () => { jest - .spyOn(client.api, "getWorkspaceQuota") + .spyOn(API, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); jest - .spyOn(client.api, "getUsers") + .spyOn(API, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); + jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(client.api, "createWorkspace") - .mockResolvedValueOnce(MockWorkspace); - jest - .spyOn(client.api, "getTemplateVersionExternalAuth") + .spyOn(API, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); renderCreateWorkspacePage(); @@ -216,7 +210,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(githubButton); jest - .spyOn(client.api, "getTemplateVersionExternalAuth") + .spyOn(API, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithubAuthenticated]); await screen.findByText( @@ -229,7 +223,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(client.api.createWorkspace).toBeCalledWith( + expect(API.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -241,16 +235,14 @@ describe("CreateWorkspacePage", () => { it("optional external auth is optional", async () => { jest - .spyOn(client.api, "getWorkspaceQuota") + .spyOn(API, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota); jest - .spyOn(client.api, "getUsers") + .spyOn(API, "getUsers") .mockResolvedValueOnce({ users: [MockUser], count: 1 }); + jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest - .spyOn(client.api, "createWorkspace") - .mockResolvedValueOnce(MockWorkspace); - jest - .spyOn(client.api, "getTemplateVersionExternalAuth") + .spyOn(API, "getTemplateVersionExternalAuth") .mockResolvedValue([ { ...MockTemplateVersionExternalAuthGithub, optional: true }, ]); @@ -271,7 +263,7 @@ describe("CreateWorkspacePage", () => { await userEvent.click(submitButton); await waitFor(() => - expect(client.api.createWorkspace).toBeCalledWith( + expect(API.createWorkspace).toBeCalledWith( MockUser.organization_ids[0], MockUser.id, expect.objectContaining({ @@ -284,7 +276,7 @@ describe("CreateWorkspacePage", () => { it("auto create a workspace if uses mode=auto", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); renderWithAuth(<CreateWorkspacePage />, { route: @@ -315,10 +307,10 @@ describe("CreateWorkspacePage", () => { it("disables mode=auto if a required external auth provider is not connected", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); const externalAuthSpy = jest - .spyOn(client.api, "getTemplateVersionExternalAuth") + .spyOn(API, "getTemplateVersionExternalAuth") .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); renderWithAuth(<CreateWorkspacePage />, { @@ -344,7 +336,7 @@ describe("CreateWorkspacePage", () => { it("auto create a workspace if uses mode=auto and version=version-id", async () => { const param = "first_parameter"; const paramValue = "It works!"; - const createWorkspaceSpy = jest.spyOn(client.api, "createWorkspace"); + const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); renderWithAuth(<CreateWorkspacePage />, { route: diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 48dd90aa3232d..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 { client } 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: () => client.api.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 f470446e4655e..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 { client } 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(client.api.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 bdcae4546e332..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 { client } 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(client.api.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: () => client.api.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 e1afd45f64531..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 { client } from "api/api"; +import { API } from "api/api"; import { TemplateLayout } from "pages/TemplatePage/TemplateLayout"; import { MockTemplate, @@ -15,7 +15,7 @@ import TemplateEmbedPage from "./TemplateEmbedPage"; test("Users can fill the parameters and copy the open in coder url", async () => { jest - .spyOn(client.api, "getTemplateVersionRichParameters") + .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValue([parameter1, parameter2]); renderWithAuth( diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index 6263126eda7b7..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 { client } 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"; @@ -25,7 +25,7 @@ const TemplateEmbedPage: FC = () => { const { data: templateParameters } = useQuery({ queryKey: ["template", template.id, "embed"], queryFn: () => - client.api.getTemplateVersionRichParameters(template.active_version_id), + API.getTemplateVersionRichParameters(template.active_version_id), }); return ( diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index 23c663e32ba1c..e388c81feb27e 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -7,7 +7,7 @@ import { } from "react"; import { useQuery } from "react-query"; import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; -import { client } 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"; @@ -35,14 +35,11 @@ const templatePermissions = ( }); const fetchTemplate = async (organizationId: string, templateName: string) => { - const template = await client.api.getTemplateByName( - organizationId, - templateName, - ); + const template = await API.getTemplateByName(organizationId, templateName); const [activeVersion, permissions] = await Promise.all([ - client.api.getTemplateVersion(template.active_version_id), - client.api.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 fbd1ff993c802..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 { client } 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: () => client.api.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 d08de7669e505..df05f167e776e 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; -import { client } 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"; @@ -13,7 +13,7 @@ const TemplateVersionsPage = () => { const { template, permissions } = useTemplateLayoutContext(); const { data } = useQuery({ queryKey: ["template", "versions", template.id], - queryFn: () => client.api.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( @@ -21,7 +21,7 @@ const TemplateVersionsPage = () => { ); const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({ mutationFn: (templateVersionId: string) => { - return client.api.updateActiveTemplateVersion(template.id, { + return API.updateActiveTemplateVersion(template.id, { id: templateVersionId, }); }, @@ -37,7 +37,7 @@ const TemplateVersionsPage = () => { const { mutate: archiveVersion, isLoading: isArchiving } = useMutation({ mutationFn: (templateVersionId: string) => { - return client.api.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 5f8421f2d6584..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 { client } from "api/api"; +import { API } from "api/api"; import { MockTemplate } from "testHelpers/entities"; import { useDeletionDialogState } from "./useDeletionDialogState"; @@ -23,9 +23,9 @@ test("confirm template deletion", async () => { expect(result.current.isDeleteDialogOpen).toBeTruthy(); // Confirm delete - jest.spyOn(client.api, "deleteTemplate"); + jest.spyOn(API, "deleteTemplate"); await act(async () => result.current.confirmDelete()); - await waitFor(() => expect(client.api.deleteTemplate).toBeCalledTimes(1)); + await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1)); expect(onDeleteTemplate).toBeCalledTimes(1); }); diff --git a/site/src/pages/TemplatePage/useDeletionDialogState.ts b/site/src/pages/TemplatePage/useDeletionDialogState.ts index 8185d128efa20..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 { client } 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 client.api.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 4b0120a29c546..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 { client, withDefaultFeatures } 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"; @@ -104,14 +104,12 @@ const fillAndSubmitForm = async ({ describe("TemplateSettingsPage", () => { it("succeeds", async () => { await renderTemplateSettingsPage(); - jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); - await waitFor(() => - expect(client.api.updateTemplateMeta).toBeCalledTimes(1), - ); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); }); it("allows a description of 128 chars", () => { @@ -146,10 +144,7 @@ describe("TemplateSettingsPage", () => { }); }), ); - const updateTemplateMetaSpy = jest.spyOn( - client.api, - "updateTemplateMeta", - ); + const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta"); const deprecationMessage = "This template is deprecated"; await renderTemplateSettingsPage(); @@ -174,10 +169,7 @@ describe("TemplateSettingsPage", () => { }); }), ); - const updateTemplateMetaSpy = jest.spyOn( - client.api, - "updateTemplateMeta", - ); + const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta"); await renderTemplateSettingsPage(); await deprecateTemplate( diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 4d6bd9a21329c..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 { client } 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"; @@ -31,7 +31,7 @@ export const TemplateSettingsPage: FC = () => { error: submitError, } = useMutation( (data: UpdateTemplateMeta) => { - return client.api.updateTemplateMeta(template.id, data); + return API.updateTemplateMeta(template.id, data); }, { onSuccess: async (data) => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 61e474be7ee84..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 { client } from "api/api"; +import { API } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockEntitlementsWithScheduling, @@ -127,38 +127,38 @@ function waitForWithCutoff(callback: () => void | Promise<void>) { describe("TemplateSchedulePage", () => { beforeEach(() => { jest - .spyOn(client.api, "getEntitlements") + .spyOn(API, "getEntitlements") .mockResolvedValue(MockEntitlementsWithScheduling); }); it("Calls the API when user fills in and submits a form", async () => { await renderTemplateSchedulePage(); - jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(client.api.updateTemplateMeta).toBeCalledTimes(1), + expect(API.updateTemplateMeta).toBeCalledTimes(1), ); }); test("default is converted to and from hours", async () => { await renderTemplateSchedulePage(); - jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(client.api.updateTemplateMeta).toBeCalledTimes(1), + expect(API.updateTemplateMeta).toBeCalledTimes(1), ); await waitForWithCutoff(() => { - expect(client.api.updateTemplateMeta).toBeCalledWith( + expect(API.updateTemplateMeta).toBeCalledWith( "test-template", expect.objectContaining({ default_ttl_ms: (validFormValues.default_ttl_ms || 0) * 3600000, @@ -170,18 +170,18 @@ describe("TemplateSchedulePage", () => { test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => { await renderTemplateSchedulePage(); - jest.spyOn(client.api, "updateTemplateMeta").mockResolvedValueOnce({ + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }); await fillAndSubmitForm(validFormValues); await waitForWithCutoff(() => - expect(client.api.updateTemplateMeta).toBeCalledTimes(1), + expect(API.updateTemplateMeta).toBeCalledTimes(1), ); await waitForWithCutoff(() => { - expect(client.api.updateTemplateMeta).toBeCalledWith( + expect(API.updateTemplateMeta).toBeCalledWith( "test-template", expect.objectContaining({ failure_ttl_ms: (validFormValues.failure_ttl_ms || 0) * 86400000, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 4c420bae5568e..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 { client } 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,8 +27,7 @@ const TemplateSchedulePage: FC = () => { isLoading: isSubmitting, error: submitError, } = useMutation( - (data: UpdateTemplateMeta) => - client.api.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 99047c540d352..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 { client } from "api/api"; +import { API } from "api/api"; import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockTemplate, @@ -32,14 +32,12 @@ const renderTemplateVariablesPage = async () => { describe("TemplateVariablesPage", () => { it("renders with variables", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest - .spyOn(client.api, "getTemplateByName") - .mockResolvedValueOnce(MockTemplate); - jest - .spyOn(client.api, "getTemplateVersion") + .spyOn(API, "getTemplateVersion") .mockResolvedValueOnce(MockTemplateVersion); jest - .spyOn(client.api, "getTemplateVersionVariables") + .spyOn(API, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, @@ -59,26 +57,22 @@ describe("TemplateVariablesPage", () => { }); it("user submits the form successfully", async () => { + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest - .spyOn(client.api, "getTemplateByName") - .mockResolvedValueOnce(MockTemplate); - jest - .spyOn(client.api, "getTemplateVersion") + .spyOn(API, "getTemplateVersion") .mockResolvedValue(MockTemplateVersion); jest - .spyOn(client.api, "getTemplateVersionVariables") + .spyOn(API, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, ]); jest - .spyOn(client.api, "createTemplateVersion") + .spyOn(API, "createTemplateVersion") .mockResolvedValueOnce(MockTemplateVersion2); - jest - .spyOn(client.api, "updateActiveTemplateVersion") - .mockResolvedValueOnce({ - message: "done", - }); + jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ + message: "done", + }); await renderTemplateVariablesPage(); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 22b172c9cd9ed..cd07c9688ea36 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -26,7 +26,7 @@ import type { MonacoEditorProps } from "./MonacoEditor"; import { Language } from "./PublishTemplateVersionDialog"; import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; -const { client } = api; +const { API } = api; // 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 @@ -74,8 +74,8 @@ const buildTemplateVersion = async ( user: UserEvent, topbar: HTMLElement, ) => { - jest.spyOn(client.api, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); - jest.spyOn(client.api, "createTemplateVersion").mockResolvedValue({ + jest.spyOn(API, "uploadFile").mockResolvedValueOnce({ hash: "hash" }); + jest.spyOn(API, "createTemplateVersion").mockResolvedValue({ ...templateVersion, job: { ...templateVersion.job, @@ -83,7 +83,7 @@ const buildTemplateVersion = async ( }, }); jest - .spyOn(client.api, "getTemplateVersionByName") + .spyOn(API, "getTemplateVersionByName") .mockResolvedValue(templateVersion); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") @@ -118,10 +118,10 @@ test("Use custom name, message and set it as active when publishing", async () = // Publish const patchTemplateVersion = jest - .spyOn(client.api, "patchTemplateVersion") + .spyOn(API, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(client.api, "updateActiveTemplateVersion") + .spyOn(API, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); const publishButton = within(topbar).getByRole("button", { name: "Publish", @@ -164,10 +164,10 @@ test("Do not mark as active if promote is not checked", async () => { // Publish const patchTemplateVersion = jest - .spyOn(client.api, "patchTemplateVersion") + .spyOn(API, "patchTemplateVersion") .mockResolvedValue(newTemplateVersion); const updateActiveTemplateVersion = jest - .spyOn(client.api, "updateActiveTemplateVersion") + .spyOn(API, "updateActiveTemplateVersion") .mockResolvedValue({ message: "" }); const publishButton = within(topbar).getByRole("button", { name: "Publish", @@ -209,7 +209,7 @@ test("Patch request is not send when there are no changes", async () => { // Publish const patchTemplateVersion = jest - .spyOn(client.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 97e752c799459..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 { client } 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<unknown>[] = []; if (haveChanges) { - publishActions.push(client.api.patchTemplateVersion(version.id, data)); + publishActions.push(API.patchTemplateVersion(version.id, data)); } if (isActiveVersion) { publishActions.push( - client.api.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 538087ca1b281..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 { client } from "api/api"; +import { API } from "api/api"; import { MockUser, MockWorkspace, @@ -56,7 +56,7 @@ describe("TerminalPage", () => { it("loads the right workspace data", async () => { jest - .spyOn(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValue(MockWorkspace); new WS( `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, @@ -65,7 +65,7 @@ describe("TerminalPage", () => { `/${MockUser.username}/${MockWorkspace.name}/terminal`, ); await waitFor(() => { - expect(client.api.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( + expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( MockUser.username, MockWorkspace.name, { include_deleted: true }, diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 52a53b8a0803e..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 { client } from "api/api"; +import { API } from "api/api"; import { mockApiError } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import * as AccountForm from "./AccountForm"; @@ -25,36 +25,34 @@ const fillAndSubmitForm = async () => { describe("AccountPage", () => { describe("when it is a success", () => { it("shows the success message", async () => { - jest - .spyOn(client.api, "updateProfile") - .mockImplementationOnce((userId, data) => - Promise.resolve({ - id: userId, - email: "user@coder.com", - created_at: new Date().toISOString(), - status: "active", - organization_ids: ["123"], - roles: [], - avatar_url: "", - last_seen_at: new Date().toISOString(), - login_type: "password", - theme_preference: "", - ...data, - }), - ); + jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) => + Promise.resolve({ + id: userId, + email: "user@coder.com", + created_at: new Date().toISOString(), + status: "active", + organization_ids: ["123"], + roles: [], + avatar_url: "", + last_seen_at: new Date().toISOString(), + login_type: "password", + theme_preference: "", + ...data, + }), + ); renderWithAuth(<AccountPage />); await fillAndSubmitForm(); const successMessage = await screen.findByText("Updated settings."); expect(successMessage).toBeDefined(); - expect(client.api.updateProfile).toBeCalledTimes(1); - expect(client.api.updateProfile).toBeCalledWith("me", newData); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith("me", newData); }); }); describe("when the username is already taken", () => { it("shows an error", async () => { - jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce( + jest.spyOn(API, "updateProfile").mockRejectedValueOnce( mockApiError({ message: "Invalid profile", validations: [ @@ -70,14 +68,14 @@ describe("AccountPage", () => { "Username is already in use", ); expect(errorMessage).toBeDefined(); - expect(client.api.updateProfile).toBeCalledTimes(1); - expect(client.api.updateProfile).toBeCalledWith("me", newData); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith("me", newData); }); }); describe("when it is an unknown error", () => { it("shows a generic error message", async () => { - jest.spyOn(client.api, "updateProfile").mockRejectedValueOnce({ + jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ data: "unknown error", }); @@ -86,8 +84,8 @@ describe("AccountPage", () => { const errorMessage = await screen.findByText("Something went wrong."); expect(errorMessage).toBeDefined(); - expect(client.api.updateProfile).toBeCalledTimes(1); - expect(client.api.updateProfile).toBeCalledWith("me", newData); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith("me", newData); }); }); }); diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index 8cca935805d8e..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 { client } from "api/api"; +import { API } from "api/api"; import { MockUser } from "testHelpers/entities"; import { renderWithAuth } from "testHelpers/renderHelpers"; import { AppearancePage } from "./AppearancePage"; @@ -9,7 +9,7 @@ describe("appearance page", () => { it("does nothing when selecting current theme", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "dark", }); @@ -18,13 +18,13 @@ describe("appearance page", () => { await userEvent.click(dark); // Check if the API was called correctly - expect(client.api.updateAppearanceSettings).toBeCalledTimes(0); + expect(API.updateAppearanceSettings).toBeCalledTimes(0); }); it("changes theme to dark blue", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "darkBlue", }); @@ -33,8 +33,8 @@ describe("appearance page", () => { await userEvent.click(darkBlue); // Check if the API was called correctly - expect(client.api.updateAppearanceSettings).toBeCalledTimes(1); - expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", { + expect(API.updateAppearanceSettings).toBeCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", { theme_preference: "darkBlue", }); }); @@ -42,7 +42,7 @@ describe("appearance page", () => { it("changes theme to light", async () => { renderWithAuth(<AppearancePage />); - jest.spyOn(client.api, "updateAppearanceSettings").mockResolvedValueOnce({ + jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "light", }); @@ -51,8 +51,8 @@ describe("appearance page", () => { await userEvent.click(light); // Check if the API was called correctly - expect(client.api.updateAppearanceSettings).toBeCalledTimes(1); - expect(client.api.updateAppearanceSettings).toHaveBeenCalledWith("me", { + expect(API.updateAppearanceSettings).toBeCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", { theme_preference: "light", }); }); diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index e7c46bc466adf..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 { client } 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"; @@ -28,7 +28,7 @@ describe("SSH keys Page", () => { const newUserSSHKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"; - jest.spyOn(client.api, "regenerateUserSSHKey").mockResolvedValueOnce({ + jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({ ...MockGitSSHKey, public_key: newUserSSHKey, }); @@ -43,7 +43,7 @@ describe("SSH keys Page", () => { await screen.findByText("SSH Key regenerated successfully."); // Check if the API was called correctly - expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1); + expect(API.regenerateUserSSHKey).toBeCalledTimes(1); // Check if the SSH key is updated await screen.findByText(newUserSSHKey); @@ -57,7 +57,7 @@ describe("SSH keys Page", () => { // Wait to the ssh be rendered on the screen await screen.findByText(MockGitSSHKey.public_key); - jest.spyOn(client.api, "regenerateUserSSHKey").mockRejectedValueOnce( + jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce( mockApiError({ message: SSHKeysPageLanguage.regenerationError, }), @@ -82,7 +82,7 @@ describe("SSH keys Page", () => { expect(alert).toHaveTextContent(SSHKeysPageLanguage.regenerationError); // Check if the API was called correctly - expect(client.api.regenerateUserSSHKey).toBeCalledTimes(1); + expect(API.regenerateUserSSHKey).toBeCalledTimes(1); }); }); }); diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index 1665ece78b8b4..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 { client } from "api/api"; +import { API } from "api/api"; import type { OAuthConversionResponse } from "api/typesGenerated"; import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities"; import { @@ -37,34 +37,29 @@ const fillAndSubmitSecurityForm = () => { }; beforeEach(() => { - jest - .spyOn(client.api, "getAuthMethods") - .mockResolvedValue(MockAuthMethodsAll); - jest.spyOn(client.api, "getUserLoginType").mockResolvedValue({ + jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll); + jest.spyOn(API, "getUserLoginType").mockResolvedValue({ login_type: "password", }); }); test("update password successfully", async () => { jest - .spyOn(client.api, "updateUserPassword") + .spyOn(API, "updateUserPassword") .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined)); const { user } = await renderPage(); fillAndSubmitSecurityForm(); const successMessage = await screen.findByText("Updated password."); expect(successMessage).toBeDefined(); - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith( - user.id, - newSecurityFormValues, - ); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); await waitFor(() => expect(window.location).toBeAt("/")); }); test("update password with incorrect old password", async () => { - jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce( + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( mockApiError({ message: "Incorrect password.", validations: [{ detail: "Incorrect password.", field: "old_password" }], @@ -77,15 +72,12 @@ test("update password with incorrect old password", async () => { const errorMessage = await screen.findAllByText("Incorrect password."); expect(errorMessage).toBeDefined(); expect(errorMessage).toHaveLength(2); - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith( - user.id, - newSecurityFormValues, - ); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); }); test("update password with invalid password", async () => { - jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce( + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( mockApiError({ message: "Invalid password.", validations: [{ detail: "Invalid password.", field: "password" }], @@ -98,15 +90,12 @@ test("update password with invalid password", async () => { const errorMessage = await screen.findAllByText("Invalid password."); expect(errorMessage).toBeDefined(); expect(errorMessage).toHaveLength(2); - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith( - user.id, - newSecurityFormValues, - ); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); }); test("update password when submit returns an unknown error", async () => { - jest.spyOn(client.api, "updateUserPassword").mockRejectedValueOnce({ + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ data: "unknown error", }); @@ -115,18 +104,15 @@ test("update password when submit returns an unknown error", async () => { const errorMessage = await screen.findByText("Something went wrong."); expect(errorMessage).toBeDefined(); - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith( - user.id, - newSecurityFormValues, - ); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); }); test("change login type to OIDC", async () => { const user = userEvent.setup(); const { user: userData } = await renderPage(); const convertToOAUTHSpy = jest - .spyOn(client.api, "convertToOAUTH") + .spyOn(API, "convertToOAUTH") .mockResolvedValue({ state_string: "some-state-string", expires_at: "2021-01-01T00:00:00Z", diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx index afd0a66498d9f..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 { client } 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: client.api.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 4b8bb9c9b11fd..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 { client } 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<LoginTypeConfirmation>({ open: false, selectedType: undefined }); - const mutation = useMutation(client.api.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 3e923a89cd5bf..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 { client } 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: () => - client.api.getTokens({ - include_all, - }), + queryFn: () => API.getTokens({ include_all }), }); return { @@ -29,7 +26,7 @@ export const useDeleteToken = (queryKey: QueryKey) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: client.api.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 815b36a721b02..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 { client } from "api/api"; +import { API } from "api/api"; import type { Role } from "api/typesGenerated"; import { MockUser, @@ -261,7 +261,7 @@ describe("UsersPage", () => { await resetUserPassword(() => { jest - .spyOn(client.api, "updateUserPassword") + .spyOn(API, "updateUserPassword") .mockResolvedValueOnce(undefined); }); @@ -269,8 +269,8 @@ describe("UsersPage", () => { await screen.findByText("Successfully updated the user password."); // Check if the API was called correctly - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, { + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", }); @@ -281,17 +281,15 @@ describe("UsersPage", () => { renderPage(); await resetUserPassword(() => { - jest - .spyOn(client.api, "updateUserPassword") - .mockRejectedValueOnce({}); + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({}); }); // Check if the error message is displayed await screen.findByText("Error on resetting the user password."); // Check if the API was called correctly - expect(client.api.updateUserPassword).toBeCalledTimes(1); - expect(client.api.updateUserPassword).toBeCalledWith(MockUser.id, { + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", }); diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index 523ef6c808df6..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 { client } from "api/api"; +import { API } from "api/api"; import { MockWorkspace, MockWorkspaceAgent, @@ -18,7 +18,7 @@ afterEach(() => { describe("WorkspaceBuildPage", () => { test("gets the right workspace build", async () => { const getWorkspaceBuildSpy = jest - .spyOn(client.api, "getWorkspaceBuildByNumber") + .spyOn(API, "getWorkspaceBuildByNumber") .mockResolvedValue(MockWorkspaceBuild); renderWithAuth(<WorkspaceBuildPage />, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 0345137b691c0..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 { client } 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 client.api.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 3299e25b1a522..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 { client } from "api/api"; +import { API } from "api/api"; import type { TemplateVersionParameter, Workspace, @@ -49,7 +49,7 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({ }) => { const { data: parameters } = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], - queryFn: () => client.api.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 2ac588f5ce373..757ea44927a3a 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 api from "api/api"; import type { TemplateVersionParameter, Workspace } from "api/typesGenerated"; import EventSourceMock from "eventsourcemock"; import { @@ -22,22 +22,18 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; -const { client } = API; +const { API } = api; // 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(client.api, "getWorkspaceByOwnerAndName") - .mockResolvedValue(workspace); - jest.spyOn(client.api, "getTemplate").mockResolvedValueOnce(MockTemplate); - jest - .spyOn(client.api, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([]); - jest - .spyOn(client.api, "getDeploymentConfig") + .spyOn(API, "getDeploymentConfig") .mockResolvedValueOnce(MockDeploymentConfig); jest - .spyOn(API, "watchWorkspaceAgentLogs") + .spyOn(api, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { options.onDone?.(); return new WebSocket(""); @@ -93,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(client.api, "deleteWorkspace") + .spyOn(API, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await renderWorkspacePage(MockWorkspace); @@ -133,7 +129,7 @@ describe("WorkspacePage", () => { ); const deleteWorkspaceMock = jest - .spyOn(client.api, "deleteWorkspace") + .spyOn(API, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuildDelete); await renderWorkspacePage(MockFailedWorkspace); @@ -179,7 +175,7 @@ describe("WorkspacePage", () => { ); const startWorkspaceMock = jest - .spyOn(client.api, "startWorkspace") + .spyOn(API, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); @@ -187,7 +183,7 @@ describe("WorkspacePage", () => { it("requests a stop job when the user presses Stop", async () => { const stopWorkspaceMock = jest - .spyOn(client.api, "stopWorkspace") + .spyOn(API, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); await testButton(MockWorkspace, "Stop", stopWorkspaceMock); @@ -195,7 +191,7 @@ describe("WorkspacePage", () => { it("requests a stop when the user presses Restart", async () => { const stopWorkspaceMock = jest - .spyOn(client.api, "stopWorkspace") + .spyOn(API, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -221,7 +217,7 @@ describe("WorkspacePage", () => { ); const cancelWorkspaceMock = jest - .spyOn(client.api, "cancelWorkspaceBuild") + .spyOn(API, "cancelWorkspaceBuild") .mockImplementation(() => Promise.resolve({ message: "job canceled" })); await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock); @@ -230,11 +226,11 @@ describe("WorkspacePage", () => { it("requests an update when the user presses Update", async () => { // Mocks jest - .spyOn(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceMock = jest - .spyOn(client.api, "updateWorkspace") + .spyOn(API, "updateWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); // Render @@ -255,12 +251,12 @@ describe("WorkspacePage", () => { it("updates the parameters when they are missing during update", async () => { // Mocks jest - .spyOn(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceSpy = jest - .spyOn(client.api, "updateWorkspace") + .spyOn(API, "updateWorkspace") .mockRejectedValueOnce( - new API.MissingBuildParameters( + new api.MissingBuildParameters( [MockTemplateVersionParameter1, MockTemplateVersionParameter2], MockOutdatedWorkspace.template_active_version_id, ), @@ -277,7 +273,7 @@ describe("WorkspacePage", () => { // The update was called await waitFor(() => { - expect(client.api.updateWorkspace).toBeCalled(); + expect(API.updateWorkspace).toBeCalled(); updateWorkspaceSpy.mockClear(); }); @@ -300,7 +296,7 @@ describe("WorkspacePage", () => { // Check if the update was called using the values from the form await waitFor(() => { - expect(client.api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ + expect(API.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ { name: MockTemplateVersionParameter1.name, value: "some-value", @@ -315,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(client.api, "getWorkspaceParameters").mockResolvedValue({ + jest.spyOn(API, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { ...MockTemplateVersionParameter1, @@ -327,7 +323,7 @@ describe("WorkspacePage", () => { ], buildParameters: [{ name: "rebuild", value: "false" }], }); - const restartWorkspaceSpy = jest.spyOn(client.api, "restartWorkspace"); + const restartWorkspaceSpy = jest.spyOn(API, "restartWorkspace"); const user = userEvent.setup(); await renderWorkspacePage(MockWorkspace); await user.click(screen.getByTestId("build-parameters-button")); @@ -357,7 +353,7 @@ describe("WorkspacePage", () => { const retryDebugButtonRe = /^Debug$/i; describe("Retries a failed 'Start' transition", () => { - const mockStart = jest.spyOn(client.api, "startWorkspace"); + const mockStart = jest.spyOn(API, "startWorkspace"); const failedStart: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -390,7 +386,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Stop' transition", () => { - const mockStop = jest.spyOn(client.api, "stopWorkspace"); + const mockStop = jest.spyOn(API, "stopWorkspace"); const failedStop: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -411,7 +407,7 @@ describe("WorkspacePage", () => { }); describe("Retries a failed 'Delete' transition", () => { - const mockDelete = jest.spyOn(client.api, "deleteWorkspace"); + const mockDelete = jest.spyOn(API, "deleteWorkspace"); const failedDelete: Workspace = { ...MockFailedWorkspace, latest_build: { @@ -456,7 +452,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(client.api, "startWorkspace"); + const startWorkspaceSpy = jest.spyOn(API, "startWorkspace"); await renderWorkspacePage(workspace); const retryWithBuildParametersButton = await screen.findByRole("button", { @@ -502,7 +498,7 @@ describe("WorkspacePage", () => { return HttpResponse.json([parameter]); }), ); - const startWorkspaceSpy = jest.spyOn(client.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 9b072cd4280f1..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, client } 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<WorkspaceReadyPageProps> = ({ }>({ open: false }); const { mutate: mutateRestartWorkspace, isLoading: isRestarting } = useMutation({ - mutationFn: client.api.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 19a0ef19fe34a..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 { client } 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"; @@ -62,7 +62,7 @@ const renderScheduleControls = async () => { test("add 3 hours to deadline", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(client.api, "putWorkspaceExtension") + .spyOn(API, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); @@ -91,7 +91,7 @@ test("add 3 hours to deadline", async () => { test("remove 2 hours to deadline", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(client.api, "putWorkspaceExtension") + .spyOn(API, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); @@ -119,7 +119,7 @@ test("remove 2 hours to deadline", async () => { test("rollback to previous deadline on error", async () => { const user = userEvent.setup(); const initialScheduleMessage = "Stop in 3 hours"; - jest.spyOn(client.api, "putWorkspaceExtension").mockRejectedValue({}); + jest.spyOn(API, "putWorkspaceExtension").mockRejectedValue({}); await renderScheduleControls(); @@ -139,7 +139,7 @@ test("rollback to previous deadline on error", async () => { test("request is only sent once when clicking multiple times", async () => { const user = userEvent.setup(); const updateDeadlineSpy = jest - .spyOn(client.api, "putWorkspaceExtension") + .spyOn(API, "putWorkspaceExtension") .mockResolvedValue(); await renderScheduleControls(); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 1879a28c01564..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 { client } from "api/api"; +import { API } from "api/api"; import { MockWorkspace, MockTemplateVersionParameter1, @@ -20,17 +20,15 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest - .spyOn(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce(MockWorkspace); - jest - .spyOn(client.api, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([ - MockTemplateVersionParameter1, - MockTemplateVersionParameter2, - // Immutable parameters - MockTemplateVersionParameter4, - ]); - jest.spyOn(client.api, "getWorkspaceBuildParameters").mockResolvedValueOnce([ + jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + // Immutable parameters + MockTemplateVersionParameter4, + ]); + jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([ MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, // Immutable value @@ -38,7 +36,7 @@ test("Submit the workspace settings page successfully", async () => { ]); // Mock the API calls that submit data const postWorkspaceBuildSpy = jest - .spyOn(client.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 72dc73d672185..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 { client } 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: () => client.api.getWorkspaceParameters(workspace), + queryFn: () => API.getWorkspaceParameters(workspace), }); const navigate = useNavigate(); const updateParameters = useMutation({ mutationFn: (buildParameters: WorkspaceBuildParameter[]) => - client.api.postWorkspaceBuild(workspace.id, { + API.postWorkspaceBuild(workspace.id, { transition: "start", rich_parameter_values: buildParameters, }), @@ -93,9 +93,7 @@ const WorkspaceParametersPage: FC = () => { export type WorkspaceParametersPageViewProps = { workspace: Workspace; canChangeVersions: boolean; - data: - | Awaited<ReturnType<typeof client.api.getWorkspaceParameters>> - | undefined; + data: Awaited<ReturnType<typeof API.getWorkspaceParameters>> | 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 920c9e4b2e352..629c4d24173e3 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 { client } 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"; @@ -271,7 +271,7 @@ const defaultFormProps: WorkspaceScheduleFormProps = { describe("templateInheritance", () => { it("disables the entire autostart feature appropriately", async () => { - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); const props = { ...defaultFormProps, template: { @@ -299,7 +299,7 @@ describe("templateInheritance", () => { it("disables the autostart days of the week appropriately", async () => { const enabledDayLabels = ["Sat", "Sun"]; - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); const props = { ...defaultFormProps, template: { @@ -343,7 +343,7 @@ describe("templateInheritance", () => { allow_user_autostop: false, }, }; - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); render(<WorkspaceScheduleForm {...props} />); const autoStopToggle = await screen.findByLabelText("Enable Autostop"); @@ -355,7 +355,7 @@ describe("templateInheritance", () => { expect(ttlInput).toBeDisabled(); }); it("disables secondary autostart fields if main feature switch is toggled off", async () => { - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -379,7 +379,7 @@ describe("templateInheritance", () => { }); }); it("disables secondary autostop fields if main feature switch is toggled off", async () => { - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -398,7 +398,7 @@ describe("templateInheritance", () => { }); test("form should be enabled when both auto stop and auto start features are disabled, given that the template permits these actions", async () => { - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); render( <WorkspaceScheduleForm {...defaultFormProps} @@ -423,7 +423,7 @@ test("form should be disabled when both auto stop and auto start features are di allow_user_autostop: false, }, }; - jest.spyOn(client.api, "getTemplateByName").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate); render(<WorkspaceScheduleForm {...props} />); const submitButton = await screen.findByRole("button", { name: "Submit" }); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index cb5870b3c9db8..79b14bec16184 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -3,7 +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 { client } 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"; @@ -68,10 +68,7 @@ export const WorkspaceSchedulePage: FC = () => { const [isConfirmingApply, setIsConfirmingApply] = useState(false); const { mutate: updateWorkspace } = useMutation({ mutationFn: () => - client.api.startWorkspace( - workspace.id, - workspace.template_active_version_id, - ), + API.startWorkspace(workspace.id, workspace.template_active_version_id), }); return ( @@ -166,11 +163,11 @@ const submitSchedule = async (data: SubmitScheduleData) => { const actions: Promise<void>[] = []; if (autostartChanged) { - actions.push(client.api.putWorkspaceAutostart(workspace.id, autostart)); + actions.push(API.putWorkspaceAutostart(workspace.id, autostart)); } if (autostopChanged) { - actions.push(client.api.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 e2b5acf67e2d8..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 { client } 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(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace }); // Mock the API calls that submit data const patchWorkspaceSpy = jest - .spyOn(client.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(client.api, "getWorkspaceByOwnerAndName") + .spyOn(API, "getWorkspaceByOwnerAndName") .mockResolvedValueOnce({ ...MockWorkspace, allow_renames: false }); renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, { route: "/@test-user/test-workspace/settings", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index e15d429178920..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 { client } 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([ - client.api.patchWorkspace(workspace.id, { name: formValues.name }), - client.api.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 cc49cd091f22d..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 { client } 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<BatchUpdateConfirmationProps> = ({ // ...but the query _also_ doesn't have everything we need, like the // template display name! ...version, - ...(await client.api.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 196ba291dd942..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 { client } from "api/api"; +import { API } from "api/api"; import type { Workspace } from "api/typesGenerated"; import { MockStoppedWorkspace, @@ -58,9 +58,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "3" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const deleteWorkspace = jest.spyOn(client.api, "deleteWorkspace"); + const deleteWorkspace = jest.spyOn(API, "deleteWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -95,9 +95,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "4" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); + const updateWorkspace = jest.spyOn(API, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -134,9 +134,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "3" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); + const updateWorkspace = jest.spyOn(API, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -172,9 +172,9 @@ describe("WorkspacesPage", () => { { ...MockOutdatedWorkspace, id: "3" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); + const updateWorkspace = jest.spyOn(API, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -212,9 +212,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "5" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const updateWorkspace = jest.spyOn(client.api, "updateWorkspace"); + const updateWorkspace = jest.spyOn(API, "updateWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -254,9 +254,9 @@ describe("WorkspacesPage", () => { { ...MockWorkspace, id: "3" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const stopWorkspace = jest.spyOn(client.api, "stopWorkspace"); + const stopWorkspace = jest.spyOn(API, "stopWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); @@ -281,9 +281,9 @@ describe("WorkspacesPage", () => { { ...MockStoppedWorkspace, id: "3" }, ]; jest - .spyOn(client.api, "getWorkspaces") + .spyOn(API, "getWorkspaces") .mockResolvedValue({ workspaces, count: workspaces.length }); - const startWorkspace = jest.spyOn(client.api, "startWorkspace"); + const startWorkspace = jest.spyOn(API, "startWorkspace"); const user = userEvent.setup(); renderWithAuth(<WorkspacesPage />); await waitForLoaderToBeRemoved(); diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx index 0698c7b2a9c5c..38819cdf60c88 100644 --- a/site/src/pages/WorkspacesPage/batchActions.tsx +++ b/site/src/pages/WorkspacesPage/batchActions.tsx @@ -1,5 +1,5 @@ import { useMutation } from "react-query"; -import { client } from "api/api"; +import { API } from "api/api"; import type { Workspace } from "api/typesGenerated"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -14,7 +14,7 @@ export function useBatchActions(options: UseBatchActionsProps) { mutationFn: (workspaces: readonly Workspace[]) => { return Promise.all( workspaces.map((w) => - client.api.startWorkspace(w.id, w.latest_build.template_version_id), + API.startWorkspace(w.id, w.latest_build.template_version_id), ), ); }, @@ -26,7 +26,7 @@ export function useBatchActions(options: UseBatchActionsProps) { const stopAllMutation = useMutation({ mutationFn: (workspaces: readonly Workspace[]) => { - return Promise.all(workspaces.map((w) => client.api.stopWorkspace(w.id))); + return Promise.all(workspaces.map((w) => API.stopWorkspace(w.id))); }, onSuccess, onError: () => { @@ -36,9 +36,7 @@ export function useBatchActions(options: UseBatchActionsProps) { const deleteAllMutation = useMutation({ mutationFn: (workspaces: readonly Workspace[]) => { - return Promise.all( - workspaces.map((w) => client.api.deleteWorkspace(w.id)), - ); + return Promise.all(workspaces.map((w) => API.deleteWorkspace(w.id))); }, onSuccess, onError: () => { @@ -51,7 +49,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) - .map((w) => client.api.updateWorkspace(w)), + .map((w) => API.updateWorkspace(w)), ); }, onSuccess, @@ -65,7 +63,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => !w.favorite) - .map((w) => client.api.putFavoriteWorkspace(w.id)), + .map((w) => API.putFavoriteWorkspace(w.id)), ); }, onSuccess, @@ -79,7 +77,7 @@ export function useBatchActions(options: UseBatchActionsProps) { return Promise.all( workspaces .filter((w) => w.favorite) - .map((w) => client.api.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 d6840a35c31fa..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 { client } 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: () => - client.api.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: client.api.updateWorkspaceVersion, + mutationFn: API.updateWorkspaceVersion, onMutate: async (workspace) => { await queryClient.cancelQueries({ queryKey }); queryClient.setQueryData<WorkspacesResponse>(queryKey, (oldResponse) => { diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts index 48789d789997c..f8b6755f50e82 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.ts +++ b/site/src/pages/WorkspacesPage/filter/menus.ts @@ -1,4 +1,4 @@ -import { client } 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 client.api.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 client.api.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 643847cdcdb26..82c98a370a51f 100644 --- a/site/src/utils/terminal.ts +++ b/site/src/utils/terminal.ts @@ -1,4 +1,4 @@ -import { client } from "api/api"; +import { API } from "api/api"; export const terminalWebsocketUrl = async ( baseUrl: string | undefined, @@ -30,7 +30,7 @@ export const terminalWebsocketUrl = async ( } // Do ticket issuance and set the query parameter. - const tokenRes = await client.api.issueReconnectingPTYSignedToken({ + const tokenRes = await API.issueReconnectingPTYSignedToken({ url: url.toString(), agentID: agentId, }); From c52827c1130a3752967767b0c05a7dd9c23ce9c7 Mon Sep 17 00:00:00 2001 From: Parkreiner <throwawayclover@gmail.com> Date: Sun, 12 May 2024 18:55:54 +0000 Subject: [PATCH 7/7] refactor: update import setup for tests --- site/src/modules/resources/AgentLogs/AgentLogs.tsx | 4 ++-- .../TemplateVersionEditorPage.test.tsx | 6 +++--- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) 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/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index cd07c9688ea36..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,7 +26,7 @@ import type { MonacoEditorProps } from "./MonacoEditor"; import { Language } from "./PublishTemplateVersionDialog"; import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; -const { API } = api; +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 @@ -86,7 +86,7 @@ const buildTemplateVersion = async ( .spyOn(API, "getTemplateVersionByName") .mockResolvedValue(templateVersion); jest - .spyOn(api, "watchBuildLogsByTemplateVersionId") + .spyOn(apiModule, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { options.onMessage(MockWorkspaceBuildLogs[0]); options.onDone?.(); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 757ea44927a3a..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,7 +22,7 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; -const { API } = api; +const { API, MissingBuildParameters } = apiModule; // Renders the workspace page and waits for it be loaded const renderWorkspacePage = async (workspace: Workspace) => { @@ -33,7 +33,7 @@ const renderWorkspacePage = async (workspace: Workspace) => { .spyOn(API, "getDeploymentConfig") .mockResolvedValueOnce(MockDeploymentConfig); jest - .spyOn(api, "watchWorkspaceAgentLogs") + .spyOn(apiModule, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { options.onDone?.(); return new WebSocket(""); @@ -256,7 +256,7 @@ describe("WorkspacePage", () => { const updateWorkspaceSpy = jest .spyOn(API, "updateWorkspace") .mockRejectedValueOnce( - new api.MissingBuildParameters( + new MissingBuildParameters( [MockTemplateVersionParameter1, MockTemplateVersionParameter2], MockOutdatedWorkspace.template_active_version_id, ),