Files
coder/site/src/api/api.ts
T

2205 lines
58 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Coder is starting to import the Coder API file into more and more
* external projects, as a "pseudo-SDK". We are not at a stage where we are
* ready to commit to maintaining a public SDK, but we need equivalent
* functionality in other places.
*
* Message somebody from Team Blueberry if you need more context, but so far,
* these projects are importing the file:
*
* - The Coder VS Code extension
* @see {@link https://github.com/coder/vscode-coder}
* - The Coder Backstage plugin
* @see {@link https://github.com/coder/backstage-plugins}
*
* It is important that this file not do any aliased imports, or else the other
* consumers could break (particularly for platforms that limit how much you can
* touch their configuration files, like Backstage). Relative imports are still
* safe, though.
*
* For example, `utils/delay` must be imported using `../utils/delay` instead.
*/
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";
import type { PostWorkspaceUsageRequest } from "./typesGenerated";
const getMissingParameters = (
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
newBuildParameters: TypesGen.WorkspaceBuildParameter[],
templateParameters: TypesGen.TemplateVersionParameter[],
) => {
const missingParameters: TypesGen.TemplateVersionParameter[] = [];
const requiredParameters: TypesGen.TemplateVersionParameter[] = [];
for (const p of templateParameters) {
// 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.
for (const templateParameter of templateParameters) {
if (templateParameter.options.length === 0) {
continue;
}
// 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) {
continue;
}
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)
*/
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();
for (const [key, value] of Object.entries(options)) {
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 = (
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"];
};
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?.();
});
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?.(new Error("Connection for logs failed."));
socket.close();
});
socket.addEventListener("close", () => {
// When the socket closes, logs have finished streaming!
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
const BASE_CONTENT_TYPE_JSON = {
"Content-Type": "application/json",
} as const satisfies HeadersInit;
export type GetTemplatesOptions = Readonly<{
readonly deprecated?: boolean;
}>;
export type GetTemplatesQuery = Readonly<{
readonly q: string;
}>;
function normalizeGetTemplatesOptions(
options: GetTemplatesOptions | GetTemplatesQuery = {},
): Record<string, string> {
if ("q" in options) {
return options;
}
const params: Record<string, string> = {};
if (options.deprecated !== undefined) {
params.deprecated = String(options.deprecated);
}
return params;
}
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 type DeploymentConfig = Readonly<{
config: TypesGen.DeploymentValues;
options: TypesGen.SerpentOption[];
}>;
type Claims = {
license_expires: number;
account_type?: string;
account_id?: string;
trial: boolean;
all_features: boolean;
// feature_set is omitted on legacy licenses
feature_set?: string;
version: number;
features: Record<string, number>;
require_telemetry?: boolean;
};
export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
claims: Claims;
expires_at: string;
};
export type InsightsParams = {
start_time: string;
end_time: string;
template_ids: string;
};
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;
}
}
/**
* This is the container for all API methods. It's split off to make it more
* clear where API methods should go, but it is eventually merged into the Api
* class with a more flat hierarchy
*
* All public methods should be defined as arrow functions to ensure that they
* can be passed around the React UI without losing their `this` context.
*
* This is one of the few cases where you have to worry about the difference
* between traditional methods and arrow function properties. Arrow functions
* disable JS's dynamic scope, and force all `this` references to resolve via
* lexical scope.
*/
class ApiMethods {
constructor(protected readonly axios: AxiosInstance) {}
login = async (
email: string,
password: string,
): Promise<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");
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;
};
createOrganization = async (params: TypesGen.CreateOrganizationRequest) => {
const response = await this.axios.post<TypesGen.Organization>(
"/api/v2/organizations",
params,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
updateOrganization = async (
organization: string,
params: TypesGen.UpdateOrganizationRequest,
) => {
const response = await this.axios.patch<TypesGen.Organization>(
`/api/v2/organizations/${organization}`,
params,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
deleteOrganization = async (organization: string) => {
await this.axios.delete<TypesGen.Organization>(
`/api/v2/organizations/${organization}`,
);
};
/**
* @param organization Can be the organization's ID or name
*/
getOrganization = async (
organization: string,
): Promise<TypesGen.Organization> => {
const response = await this.axios.get<TypesGen.Organization>(
`/api/v2/organizations/${organization}`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getOrganizationMembers = async (organization: string) => {
const response = await this.axios.get<
TypesGen.OrganizationMemberWithUserData[]
>(`/api/v2/organizations/${organization}/members`);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getOrganizationRoles = async (organization: string) => {
const response = await this.axios.get<TypesGen.AssignableRoles[]>(
`/api/v2/organizations/${organization}/members/roles`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
updateOrganizationMemberRoles = async (
organization: string,
userId: string,
roles: TypesGen.SlimRole["name"][],
): Promise<TypesGen.User> => {
const response = await this.axios.put<TypesGen.User>(
`/api/v2/organizations/${organization}/members/${userId}/roles`,
{ roles },
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
createOrganizationRole = async (
organization: string,
role: TypesGen.Role,
): Promise<TypesGen.Role> => {
const response = await this.axios.post<TypesGen.Role>(
`/api/v2/organizations/${organization}/members/roles`,
role,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
updateOrganizationRole = async (
organization: string,
role: TypesGen.Role,
): Promise<TypesGen.Role> => {
const response = await this.axios.put<TypesGen.Role>(
`/api/v2/organizations/${organization}/members/roles`,
role,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
deleteOrganizationRole = async (organization: string, roleName: string) => {
await this.axios.delete(
`/api/v2/organizations/${organization}/members/roles/${roleName}`,
);
};
/**
* @param organization Can be the organization's ID or name
*/
addOrganizationMember = async (organization: string, userId: string) => {
const response = await this.axios.post<TypesGen.OrganizationMember>(
`/api/v2/organizations/${organization}/members/${userId}`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
removeOrganizationMember = async (organization: string, userId: string) => {
await this.axios.delete(
`/api/v2/organizations/${organization}/members/${userId}`,
);
};
getOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await this.axios.get<TypesGen.Organization[]>(
"/api/v2/organizations",
);
return response.data;
};
getMyOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await this.axios.get<TypesGen.Organization[]>(
"/api/v2/users/me/organizations",
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getProvisionerDaemonsByOrganization = async (
organization: string,
): Promise<TypesGen.ProvisionerDaemon[]> => {
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
`/api/v2/organizations/${organization}/provisionerdaemons`,
);
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 (
options?: GetTemplatesOptions | GetTemplatesQuery,
): Promise<TypesGen.Template[]> => {
const params = normalizeGetTemplatesOptions(options);
const response = await this.axios.get<TypesGen.Template[]>(
"/api/v2/templates",
{ params },
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getTemplatesByOrganization = async (
organization: string,
options?: GetTemplatesOptions,
): Promise<TypesGen.Template[]> => {
const params = normalizeGetTemplatesOptions(options);
const response = await this.axios.get<TypesGen.Template[]>(
`/api/v2/organizations/${organization}/templates`,
{ params },
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getTemplateByName = async (
organization: string,
name: string,
): Promise<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/organizations/${organization}/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;
};
/**
* @param organization Can be the organization's ID or name
*/
getTemplateVersionByName = async (
organization: string,
templateName: string,
versionName: string,
): Promise<TypesGen.TemplateVersion> => {
const response = await this.axios.get<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organization}/templates/${templateName}/versions/${versionName}`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getPreviousTemplateVersionByName = async (
organization: string,
templateName: string,
versionName: string,
) => {
try {
const response = await this.axios.get<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organization}/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;
}
};
/**
* @param organization Can be the organization's ID or name
*/
createTemplateVersion = async (
organization: string,
data: TypesGen.CreateTemplateVersionRequest,
): Promise<TypesGen.TemplateVersion> => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organization}/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;
};
/**
* @param organization Can be the organization's ID or name
*/
createTemplate = async (
organization: string,
data: TypesGen.CreateTemplateRequest,
): Promise<TypesGen.Template> => {
const response = await this.axios.post(
`/api/v2/organizations/${organization}/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 this.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 this.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 (
userId = "me",
workspace: TypesGen.CreateWorkspaceRequest,
): Promise<TypesGen.Workspace> => {
const response = await this.axios.post<TypesGen.Workspace>(
`/api/v2/users/${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;
};
updateUserRoles = async (
roles: TypesGen.SlimRole["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;
};
getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await this.axios.get<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await this.axios.put<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
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;
};
getWorkspaceBuildLogs = async (
buildId: string,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/workspacebuilds/${buildId}/logs`,
);
return response.data;
};
getWorkspaceAgentLogs = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentLog[]> => {
const response = await this.axios.get<TypesGen.WorkspaceAgentLog[]>(
`/api/v2/workspaceagents/${agentID}/logs`,
);
return response.data;
};
putWorkspaceExtension = async (
workspaceId: string,
newDeadline: dayjs.Dayjs,
): Promise<void> => {
await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, {
deadline: newDeadline,
});
};
refreshEntitlements = async (): Promise<void> => {
await this.axios.post("/api/v2/licenses/refresh-entitlements");
};
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;
}
};
getExperiments = async (): Promise<TypesGen.Experiment[]> => {
try {
const response = await this.axios.get<TypesGen.Experiment[]>(
"/api/v2/experiments",
);
return response.data;
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
return [];
}
throw error;
}
};
getAvailableExperiments =
async (): Promise<TypesGen.AvailableExperiments> => {
try {
const response = await this.axios.get("/api/v2/experiments/available");
return response.data;
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
return { safe: [] };
}
throw error;
}
};
getExternalAuthProvider = async (
provider: string,
): Promise<TypesGen.ExternalAuth> => {
const res = await this.axios.get(`/api/v2/external-auth/${provider}`);
return res.data;
};
getExternalAuthDevice = async (
provider: string,
): Promise<TypesGen.ExternalAuthDevice> => {
const resp = await this.axios.get(
`/api/v2/external-auth/${provider}/device`,
);
return resp.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;
};
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;
};
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;
};
deleteOAuth2ProviderApp = async (id: string): Promise<void> => {
await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`);
};
getOAuth2ProviderAppSecrets = async (
id: string,
): Promise<TypesGen.OAuth2ProviderAppSecret[]> => {
const resp = await this.axios.get(
`/api/v2/oauth2-provider/apps/${id}/secrets`,
);
return resp.data;
};
postOAuth2ProviderAppSecret = async (
id: string,
): Promise<TypesGen.OAuth2ProviderAppSecretFull> => {
const resp = await this.axios.post(
`/api/v2/oauth2-provider/apps/${id}/secrets`,
);
return resp.data;
};
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;
};
getTemplateDAUs = async (
templateId: string,
): Promise<TypesGen.DAUsResponse> => {
const response = await this.axios.get(
`/api/v2/templates/${templateId}/daus`,
);
return response.data;
};
getDeploymentDAUs = async (
// Default to user's local timezone.
// As /api/v2/insights/daus only accepts whole-number values for tz_offset
// we truncate the tz offset down to the closest hour.
offset = Math.trunc(new Date().getTimezoneOffset() / 60),
): Promise<TypesGen.DAUsResponse> => {
const response = await this.axios.get(
`/api/v2/insights/daus?tz_offset=${offset}`,
);
return response.data;
};
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;
};
getTemplateACL = async (
templateId: string,
): Promise<TypesGen.TemplateACL> => {
const response = await this.axios.get(
`/api/v2/templates/${templateId}/acl`,
);
return response.data;
};
updateTemplateACL = async (
templateId: string,
data: TypesGen.UpdateTemplateACL,
): Promise<{ message: string }> => {
const response = await this.axios.patch(
`/api/v2/templates/${templateId}/acl`,
data,
);
return response.data;
};
getApplicationsHost = async (): Promise<TypesGen.AppHostResponse> => {
const response = await this.axios.get("/api/v2/applications/host");
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getGroups = async (organization: string): Promise<TypesGen.Group[]> => {
const response = await this.axios.get(
`/api/v2/organizations/${organization}/groups`,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
createGroup = async (
organization: string,
data: TypesGen.CreateGroupRequest,
): Promise<TypesGen.Group> => {
const response = await this.axios.post(
`/api/v2/organizations/${organization}/groups`,
data,
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getGroup = async (
organization: string,
groupName: string,
): Promise<TypesGen.Group> => {
const response = await this.axios.get(
`/api/v2/organizations/${organization}/groups/${groupName}`,
);
return response.data;
};
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 this.patchGroup(groupId, {
name: "",
add_users: [userId],
remove_users: [],
});
};
removeMember = async (groupId: string, userId: string) => {
return this.patchGroup(groupId, {
name: "",
display_name: "",
add_users: [],
remove_users: [userId],
});
};
deleteGroup = async (groupId: string): Promise<void> => {
await this.axios.delete(`/api/v2/groups/${groupId}`);
};
getWorkspaceQuota = async (
username: string,
): Promise<TypesGen.WorkspaceQuota> => {
const response = await this.axios.get(
`/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;
};
getWorkspaceAgentSharedPorts = async (
workspaceID: string,
): Promise<TypesGen.WorkspaceAgentPortShares> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceID}/port-share`,
);
return response.data;
};
upsertWorkspaceAgentSharedPort = async (
workspaceID: string,
req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
): Promise<TypesGen.WorkspaceAgentPortShares> => {
const response = await this.axios.post(
`/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;
};
// 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;
};
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;
};
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;
};
getWorkspaceProxyRegions = async (): Promise<
TypesGen.RegionsResponse<TypesGen.Region>
> => {
const response =
await this.axios.get<TypesGen.RegionsResponse<TypesGen.Region>>(
"/api/v2/regions",
);
return response.data;
};
getWorkspaceProxies = async (): Promise<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
> => {
const response = await this.axios.get<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
>("/api/v2/workspaceproxies");
return response.data;
};
createWorkspaceProxy = async (
b: TypesGen.CreateWorkspaceProxyRequest,
): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
const response = await this.axios.post("/api/v2/workspaceproxies", b);
return response.data;
};
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: "",
announcement_banners: [],
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;
};
/**
* @param organization Can be the organization's ID or name
*/
getTemplateExamples = async (): Promise<TypesGen.TemplateExample[]> => {
const response = await this.axios.get("/api/v2/templates/examples");
return response.data;
};
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;
};
getTemplateVersionLogs = async (
versionId: string,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/templateversions/${versionId}/logs`,
);
return response.data;
};
updateWorkspaceVersion = async (
workspace: TypesGen.Workspace,
): Promise<TypesGen.WorkspaceBuild> => {
const template = await this.getTemplate(workspace.template_id);
return this.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;
};
getLicenses = async (): Promise<GetLicensesResponse[]> => {
const response = await this.axios.get("/api/v2/licenses");
return response.data;
};
createLicense = async (
data: TypesGen.AddLicenseRequest,
): Promise<TypesGen.AddLicenseRequest> => {
const response = await this.axios.post("/api/v2/licenses", data);
return response.data;
};
removeLicense = async (licenseId: number): Promise<void> => {
await this.axios.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
*/
changeWorkspaceVersion = async (
workspace: TypesGen.Workspace,
templateVersionId: string,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
): Promise<TypesGen.WorkspaceBuild> => {
const [currentBuildParameters, templateParameters] = await Promise.all([
this.getWorkspaceBuildParameters(workspace.latest_build.id),
this.getTemplateVersionRichParameters(templateVersionId),
]);
const missingParameters = getMissingParameters(
currentBuildParameters,
newBuildParameters,
templateParameters,
);
if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters, templateVersionId);
}
return this.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
*/
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 this.getTemplateVersionRichParameters(activeVersionId);
const missingParameters = getMissingParameters(
oldBuildParameters,
newBuildParameters,
templateParameters,
);
if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters, activeVersionId);
}
return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: activeVersionId,
rich_parameter_values: newBuildParameters,
});
};
getWorkspaceResolveAutostart = async (
workspaceId: string,
): Promise<TypesGen.ResolveAutostartResponse> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceId}/resolve-autostart`,
);
return response.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;
};
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,
};
};
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;
};
getInsightsUserActivity = async (
filters: InsightsParams,
): Promise<TypesGen.UserActivityInsightsResponse> => {
const params = new URLSearchParams(filters);
const response = await this.axios.get(
`/api/v2/insights/user-activity?${params}`,
);
return response.data;
};
getInsightsTemplate = async (
params: InsightsTemplateParams,
): Promise<TypesGen.TemplateInsightsResponse> => {
const searchParams = new URLSearchParams(params);
const response = await this.axios.get(
`/api/v2/insights/templates?${searchParams}`,
);
return response.data;
};
getHealth = async (force = false) => {
const params = new URLSearchParams({ force: force.toString() });
const response = await this.axios.get<TypesGen.HealthcheckReport>(
`/api/v2/debug/health?${params}`,
);
return response.data;
};
getHealthSettings = async (): Promise<TypesGen.HealthSettings> => {
const res = await this.axios.get<TypesGen.HealthSettings>(
"/api/v2/debug/health/settings",
);
return res.data;
};
updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => {
const response = await this.axios.put<TypesGen.HealthSettings>(
"/api/v2/debug/health/settings",
data,
);
return response.data;
};
putFavoriteWorkspace = async (workspaceID: string) => {
await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`);
};
deleteFavoriteWorkspace = async (workspaceID: string) => {
await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
};
getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => {
const searchParams = new URLSearchParams({
workspace_id: options.workspaceId,
agent_id: options.agentId,
});
try {
const res = await this.axios.get<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;
}
};
postWorkspaceUsage = async (
workspaceID: string,
options: PostWorkspaceUsageRequest,
) => {
const response = await this.axios.post(
`/api/v2/workspaces/${workspaceID}/usage`,
options,
);
return response.data;
};
getUserNotificationPreferences = async (userId: string) => {
const res = await this.axios.get<TypesGen.NotificationPreference[] | null>(
`/api/v2/users/${userId}/notifications/preferences`,
);
return res.data ?? [];
};
putUserNotificationPreferences = async (
userId: string,
req: TypesGen.UpdateUserNotificationPreferences,
) => {
const res = await this.axios.put<TypesGen.NotificationPreference[]>(
`/api/v2/users/${userId}/notifications/preferences`,
req,
);
return res.data;
};
getSystemNotificationTemplates = async () => {
const res = await this.axios.get<TypesGen.NotificationTemplate[]>(
"/api/v2/notifications/templates/system",
);
return res.data;
};
getNotificationDispatchMethods = async () => {
const res = await this.axios.get<TypesGen.NotificationMethodsResponse>(
"/api/v2/notifications/dispatch-methods",
);
return res.data;
};
updateNotificationTemplateMethod = async (
templateId: string,
req: TypesGen.UpdateNotificationTemplateMethod,
) => {
const res = await this.axios.put<void>(
`/api/v2/notifications/templates/${templateId}/method`,
req,
);
return res.data;
};
}
// This is a hard coded CSRF token/cookie pair for local development. In prod,
// the GoLang webserver generates a random cookie with a new token for each
// document request. For local development, we don't use the Go webserver for
// static files, so this is the 'hack' to make local development work with
// remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4="
const csrfToken =
"KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==";
// Always attach CSRF token to all requests. In puppeteer the document is
// undefined. In those cases, just do nothing.
const tokenMetadataElement =
typeof document !== "undefined"
? document.head.querySelector('meta[property="csrf-token"]')
: null;
function getConfiguredAxiosInstance(): AxiosInstance {
const instance = globalAxios.create();
// Adds 304 for the default axios validateStatus function
// https://github.com/axios/axios#handling-errors Check status here
// https://httpstatusdogs.com/
instance.defaults.validateStatus = (status) => {
return (status >= 200 && status < 300) || status === 304;
};
const metadataIsAvailable =
tokenMetadataElement !== null &&
tokenMetadataElement.getAttribute("content") !== null;
if (metadataIsAvailable) {
if (process.env.NODE_ENV === "development") {
// Development mode uses a hard-coded CSRF token
instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
tokenMetadataElement.setAttribute("content", csrfToken);
} else {
instance.defaults.headers.common["X-CSRF-TOKEN"] =
tokenMetadataElement.getAttribute("content") ?? "";
}
} else {
// Do not write error logs if we are in a FE unit test.
if (process.env.JEST_WORKER_ID === undefined) {
console.error("CSRF token not found");
}
}
return instance;
}
// Other non-API methods defined here to make it a little easier to find them.
interface ClientApi extends ApiMethods {
getCsrfToken: () => string;
setSessionToken: (token: string) => void;
setHost: (host: string | undefined) => void;
getAxiosInstance: () => AxiosInstance;
}
export class Api extends ApiMethods implements ClientApi {
constructor() {
const scopedAxiosInstance = getConfiguredAxiosInstance();
super(scopedAxiosInstance);
}
// As with ApiMethods, all public methods should be defined with arrow
// function syntax to ensure they can be passed around the React UI without
// losing/detaching their `this` context!
getCsrfToken = (): string => {
return csrfToken;
};
setSessionToken = (token: string): void => {
this.axios.defaults.headers.common["Coder-Session-Token"] = token;
};
setHost = (host: string | undefined): void => {
this.axios.defaults.baseURL = host;
};
getAxiosInstance = (): AxiosInstance => {
return this.axios;
};
}
export const API = new Api();