From a27e95e1a4997dd3c2352873bd144edb2813c544 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 14:58:28 +0000 Subject: [PATCH 01/22] chore: add vendored version of experimental Coder SDK --- plugins/backstage-plugin-coder/package.json | 3 + .../src/api/vendoredSdk/README.md | 20 + .../src/api/vendoredSdk/api/api.ts | 1940 ++++++++++++ .../src/api/vendoredSdk/api/errors.ts | 124 + .../src/api/vendoredSdk/api/typesGenerated.ts | 2599 +++++++++++++++++ .../src/api/vendoredSdk/index.ts | 8 + .../src/api/vendoredSdk/utils/delay.ts | 4 + yarn.lock | 30 +- 8 files changed, 4713 insertions(+), 15 deletions(-) create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts create mode 100644 plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts diff --git a/plugins/backstage-plugin-coder/package.json b/plugins/backstage-plugin-coder/package.json index e21caf74..1d21b960 100644 --- a/plugins/backstage-plugin-coder/package.json +++ b/plugins/backstage-plugin-coder/package.json @@ -42,6 +42,8 @@ "@material-ui/lab": "4.0.0-alpha.61", "@tanstack/react-query": "4.36.1", "axios": "^1.6.8", + "dayjs": "^1.11.11", + "ua-parser-js": "^1.0.37", "use-sync-external-store": "^1.2.1", "valibot": "^0.28.1" }, @@ -57,6 +59,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.0.0", + "@types/ua-parser-js": "^0.7.39", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md b/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md new file mode 100644 index 00000000..354acb1c --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/README.md @@ -0,0 +1,20 @@ +# Coder SDK - Experimental Vendored Version + +This is a vendored version of the main API files from the +[core Coder OSS repo](https://github.com/coder/coder/tree/main/site/src/api). All files (aside from test files) have been copied over directly, with only a +few changes made to satisfy default Backstage ESLint rules. + +While there is a risk of this getting out of sync with the versions of the +files in Coder OSS, the Coder API itself should be treated as stable. Breaking +changes are only made when absolutely necessary. + +## General approach + +- Copy over relevant files from Coder OSS and place them in relevant folders + - As much as possible, the file structure of the vendored files should match the file structure of Coder OSS to make it easier to copy updated files over. +- Have a single file at the top level of this directory that exports out the files for consumption elsewhere in the plugin. No plugin code should interact with the vendored files directly. + +## Eventual plans + +Coder has eventual plans to create a true SDK published through NPM. Once +that is published, all of this vendored code should be removed in favor of it. diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts new file mode 100644 index 00000000..e0eafd1d --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -0,0 +1,1940 @@ +/** + * @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'; + +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) + */ +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 = ( + fs: Partial, +): 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; + +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' +>; + +type Claims = { + license_expires: number; + account_type?: string; + account_id?: string; + trial: boolean; + all_features: boolean; + version: number; + features: Record; + require_telemetry?: boolean; +}; + +export type GetLicensesResponse = Omit & { + 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 => { + const payload = JSON.stringify({ email, password }); + const response = await this.axios.post( + '/api/v2/users/login', + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + + return response.data; + }; + + convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { + const response = await this.axios.post( + '/api/v2/users/me/convert-login', + request, + ); + + return response.data; + }; + + logout = async (): Promise => { + return this.axios.post('/api/v2/users/logout'); + }; + + getAuthenticatedUser = async () => { + const response = await this.axios.get('/api/v2/users/me'); + return response.data; + }; + + getUserParameters = async (templateID: string) => { + const response = await this.axios.get( + `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, + ); + + return response.data; + }; + + getAuthMethods = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/authmethods', + ); + + return response.data; + }; + + getUserLoginType = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/login-type', + ); + + return response.data; + }; + + checkAuthorization = async ( + params: TypesGen.AuthorizationRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/authcheck`, + params, + ); + + return response.data; + }; + + getApiKey = async (): Promise => { + const response = await this.axios.post( + '/api/v2/users/me/keys', + ); + + return response.data; + }; + + getTokens = async ( + params: TypesGen.TokensFilter, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/me/keys/tokens`, + { params }, + ); + + return response.data; + }; + + deleteToken = async (keyId: string): Promise => { + await this.axios.delete(`/api/v2/users/me/keys/${keyId}`); + }; + + createToken = async ( + params: TypesGen.CreateTokenRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/users/me/keys/tokens`, + params, + ); + + return response.data; + }; + + getTokenConfig = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/keys/tokens/tokenconfig', + ); + + return response.data; + }; + + getUsers = async ( + options: TypesGen.UsersRequest, + signal?: AbortSignal, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/users', options); + const response = await this.axios.get( + url.toString(), + { signal }, + ); + + return response.data; + }; + + getOrganization = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}`, + ); + + return response.data; + }; + + getOrganizations = async (): Promise => { + const response = await this.axios.get( + '/api/v2/users/me/organizations', + ); + return response.data; + }; + + getTemplate = async (templateId: string): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getTemplates = async ( + organizationId: string, + options?: TemplateOptions, + ): Promise => { + const params: Record = {}; + if (options?.deprecated !== undefined) { + // Just want to check if it isn't undefined. If it has + // a boolean value, convert it to a string and include + // it as a param. + params.deprecated = String(options.deprecated); + } + + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates`, + { params }, + ); + + return response.data; + }; + + getTemplateByName = async ( + organizationId: string, + name: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/${name}`, + ); + + return response.data; + }; + + getTemplateVersion = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}`, + ); + + return response.data; + }; + + getTemplateVersionResources = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/resources`, + ); + + return response.data; + }; + + getTemplateVersionVariables = async ( + versionId: string, + ): Promise => { + // Defined as separate variable to avoid wonky Prettier formatting because + // the type definition is so long + type VerArray = TypesGen.TemplateVersionVariable[]; + + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/variables`, + ); + + return response.data; + }; + + getTemplateVersions = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/versions`, + ); + return response.data; + }; + + getTemplateVersionByName = async ( + organizationId: string, + templateName: string, + versionName: string, + ): Promise => { + const response = await this.axios.get( + `/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( + `/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 => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/templateversions`, + data, + ); + + return response.data; + }; + + getTemplateVersionExternalAuth = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/external-auth`, + ); + + return response.data; + }; + + getTemplateVersionRichParameters = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ); + return response.data; + }; + + createTemplate = async ( + organizationId: string, + data: TypesGen.CreateTemplateRequest, + ): Promise => { + 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( + `/api/v2/templates/${templateId}/versions`, + data, + ); + return response.data; + }; + + patchTemplateVersion = async ( + templateVersionId: string, + data: TypesGen.PatchTemplateVersionRequest, + ) => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}`, + data, + ); + + return response.data; + }; + + archiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/archive`, + ); + + return response.data; + }; + + unarchiveTemplateVersion = async (templateVersionId: string) => { + const response = await this.axios.post( + `/api/v2/templateversions/${templateVersionId}/unarchive`, + ); + return response.data; + }; + + updateTemplateMeta = async ( + templateId: string, + data: TypesGen.UpdateTemplateMeta, + ): Promise => { + const response = await this.axios.patch( + `/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 => { + const response = await this.axios.delete( + `/api/v2/templates/${templateId}`, + ); + + return response.data; + }; + + getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}`, + { params }, + ); + + return response.data; + }; + + getWorkspaces = async ( + options: TypesGen.WorkspacesRequest, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/workspaces', options); + const response = await this.axios.get(url); + return response.data; + }; + + getWorkspaceByOwnerAndName = async ( + username = 'me', + workspaceName: string, + params?: TypesGen.WorkspaceOptions, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}`, + { params }, + ); + + return response.data; + }; + + getWorkspaceBuildByNumber = async ( + username = 'me', + workspaceName: string, + buildNumber: number, + ): Promise => { + const response = await this.axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ); + + return response.data; + }; + + waitForBuild = (build: TypesGen.WorkspaceBuild) => { + return new Promise((res, reject) => { + void (async () => { + let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; + + while ( + // eslint-disable-next-line no-loop-func -- Not great, but should be harmless + !['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 => { + 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 => { + const response = await this.axios.patch( + `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, + ); + + return response.data; + }; + + updateWorkspaceDormancy = async ( + workspaceId: string, + dormant: boolean, + ): Promise => { + const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; + const response = await this.axios.put( + `/api/v2/workspaces/${workspaceId}/dormant`, + data, + ); + + return response.data; + }; + + updateWorkspaceAutomaticUpdates = async ( + workspaceId: string, + automaticUpdates: TypesGen.AutomaticUpdates, + ): Promise => { + 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 => { + 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 => { + const response = await this.axios.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ); + + return response.data; + }; + + createUser = async ( + user: TypesGen.CreateUserRequest, + ): Promise => { + const response = await this.axios.post( + '/api/v2/users', + user, + ); + + return response.data; + }; + + createWorkspace = async ( + organizationId: string, + userId = 'me', + workspace: TypesGen.CreateWorkspaceRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + workspace, + ); + + return response.data; + }; + + patchWorkspace = async ( + workspaceId: string, + data: TypesGen.UpdateWorkspaceRequest, + ): Promise => { + await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); + }; + + getBuildInfo = async (): Promise => { + const response = await this.axios.get('/api/v2/buildinfo'); + return response.data; + }; + + getUpdateCheck = async (): Promise => { + const response = await this.axios.get('/api/v2/updatecheck'); + return response.data; + }; + + putWorkspaceAutostart = async ( + workspaceID: string, + autostart: TypesGen.UpdateWorkspaceAutostartRequest, + ): Promise => { + const payload = JSON.stringify(autostart); + await this.axios.put( + `/api/v2/workspaces/${workspaceID}/autostart`, + payload, + { headers: { ...BASE_CONTENT_TYPE_JSON } }, + ); + }; + + putWorkspaceAutostop = async ( + workspaceID: string, + ttl: TypesGen.UpdateWorkspaceTTLRequest, + ): Promise => { + const payload = JSON.stringify(ttl); + await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { + headers: { ...BASE_CONTENT_TYPE_JSON }, + }); + }; + + updateProfile = async ( + userId: string, + data: TypesGen.UpdateUserProfileRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/profile`, + data, + ); + return response.data; + }; + + updateAppearanceSettings = async ( + userId: string, + data: TypesGen.UpdateUserAppearanceSettingsRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/appearance`, + data, + ); + return response.data; + }; + + getUserQuietHoursSchedule = async ( + userId: TypesGen.User['id'], + ): Promise => { + 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 => { + const response = await this.axios.put( + `/api/v2/users/${userId}/quiet-hours`, + data, + ); + + return response.data; + }; + + activateUser = async ( + userId: TypesGen.User['id'], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/activate`, + ); + return response.data; + }; + + suspendUser = async (userId: TypesGen.User['id']): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/status/suspend`, + ); + + return response.data; + }; + + deleteUser = async (userId: TypesGen.User['id']): Promise => { + 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 => { + 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 => { + const response = await this.axios.post(`/api/v2/users/first`, req); + return response.data; + }; + + updateUserPassword = async ( + userId: TypesGen.User['id'], + updatePassword: TypesGen.UpdateUserPasswordRequest, + ): Promise => { + await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); + }; + + getRoles = async (): Promise> => { + const response = await this.axios.get( + `/api/v2/users/roles`, + ); + + return response.data; + }; + + updateUserRoles = async ( + roles: TypesGen.SlimRole['name'][], + userId: TypesGen.User['id'], + ): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/roles`, + { roles }, + ); + + return response.data; + }; + + getUserSSHKey = async (userId = 'me'): Promise => { + const response = await this.axios.get( + `/api/v2/users/${userId}/gitsshkey`, + ); + + return response.data; + }; + + regenerateUserSSHKey = async (userId = 'me'): Promise => { + const response = await this.axios.put( + `/api/v2/users/${userId}/gitsshkey`, + ); + + return response.data; + }; + + getWorkspaceBuilds = async ( + workspaceId: string, + req?: TypesGen.WorkspaceBuildsRequest, + ) => { + const response = await this.axios.get( + getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), + ); + + return response.data; + }; + + getWorkspaceBuildLogs = async ( + buildId: string, + before: Date, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, + ); + + return response.data; + }; + + getWorkspaceAgentLogs = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/logs`, + ); + + return response.data; + }; + + putWorkspaceExtension = async ( + workspaceId: string, + newDeadline: dayjs.Dayjs, + ): Promise => { + await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { + deadline: newDeadline, + }); + }; + + refreshEntitlements = async (): Promise => { + await this.axios.post('/api/v2/licenses/refresh-entitlements'); + }; + + getEntitlements = async (): Promise => { + try { + const response = await this.axios.get( + '/api/v2/entitlements', + ); + + 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 => { + try { + const response = await this.axios.get( + '/api/v2/experiments', + ); + + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return []; + } + + throw error; + } + }; + + getAvailableExperiments = + async (): Promise => { + try { + const response = await this.axios.get('/api/v2/experiments/available'); + + return response.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + return { safe: [] }; + } + throw error; + } + }; + + getExternalAuthProvider = async ( + provider: string, + ): Promise => { + const res = await this.axios.get(`/api/v2/external-auth/${provider}`); + return res.data; + }; + + getExternalAuthDevice = async ( + provider: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/external-auth/${provider}/device`, + ); + return resp.data; + }; + + exchangeExternalAuthDevice = async ( + provider: string, + req: TypesGen.ExternalAuthDeviceExchange, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/external-auth/${provider}/device`, + req, + ); + + return resp.data; + }; + + getUserExternalAuthProviders = + async (): Promise => { + const resp = await this.axios.get(`/api/v2/external-auth`); + return resp.data; + }; + + unlinkExternalAuthProvider = async (provider: string): Promise => { + const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`); + return resp.data; + }; + + getOAuth2ProviderApps = async ( + filter?: TypesGen.OAuth2ProviderAppFilter, + ): Promise => { + const params = filter?.user_id + ? new URLSearchParams({ user_id: filter.user_id }).toString() + : ''; + + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`); + return resp.data; + }; + + getOAuth2ProviderApp = async ( + id: string, + ): Promise => { + const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`); + return resp.data; + }; + + postOAuth2ProviderApp = async ( + data: TypesGen.PostOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/oauth2-provider/apps`, + data, + ); + return response.data; + }; + + putOAuth2ProviderApp = async ( + id: string, + data: TypesGen.PutOAuth2ProviderAppRequest, + ): Promise => { + const response = await this.axios.put( + `/api/v2/oauth2-provider/apps/${id}`, + data, + ); + return response.data; + }; + + deleteOAuth2ProviderApp = async (id: string): Promise => { + await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`); + }; + + getOAuth2ProviderAppSecrets = async ( + id: string, + ): Promise => { + const resp = await this.axios.get( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + postOAuth2ProviderAppSecret = async ( + id: string, + ): Promise => { + const resp = await this.axios.post( + `/api/v2/oauth2-provider/apps/${id}/secrets`, + ); + return resp.data; + }; + + deleteOAuth2ProviderAppSecret = async ( + appId: string, + secretId: string, + ): Promise => { + await this.axios.delete( + `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, + ); + }; + + revokeOAuth2ProviderApp = async (appId: string): Promise => { + await this.axios.delete(`/oauth2/tokens?client_id=${appId}`); + }; + + getAuditLogs = async ( + options: TypesGen.AuditLogsRequest, + ): Promise => { + const url = getURLWithSearchParams('/api/v2/audit', options); + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateDAUs = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/daus`, + ); + + return response.data; + }; + + getDeploymentDAUs = async ( + // Default to user's local timezone. + // As /api/v2/insights/daus only accepts whole-number values for tz_offset + // we truncate the tz offset down to the closest hour. + offset = Math.trunc(new Date().getTimezoneOffset() / 60), + ): Promise => { + const response = await this.axios.get( + `/api/v2/insights/daus?tz_offset=${offset}`, + ); + + return response.data; + }; + + getTemplateACLAvailable = async ( + templateId: string, + options: TypesGen.UsersRequest, + ): Promise => { + const url = getURLWithSearchParams( + `/api/v2/templates/${templateId}/acl/available`, + options, + ).toString(); + + const response = await this.axios.get(url); + return response.data; + }; + + getTemplateACL = async ( + templateId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templates/${templateId}/acl`, + ); + + return response.data; + }; + + updateTemplateACL = async ( + templateId: string, + data: TypesGen.UpdateTemplateACL, + ): Promise<{ message: string }> => { + const response = await this.axios.patch( + `/api/v2/templates/${templateId}/acl`, + data, + ); + + return response.data; + }; + + getApplicationsHost = async (): Promise => { + const response = await this.axios.get(`/api/v2/applications/host`); + return response.data; + }; + + getGroups = async (organizationId: string): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/groups`, + ); + + return response.data; + }; + + createGroup = async ( + organizationId: string, + data: TypesGen.CreateGroupRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/organizations/${organizationId}/groups`, + data, + ); + return response.data; + }; + + getGroup = async (groupId: string): Promise => { + const response = await this.axios.get(`/api/v2/groups/${groupId}`); + return response.data; + }; + + patchGroup = async ( + groupId: string, + data: TypesGen.PatchGroupRequest, + ): Promise => { + const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data); + return response.data; + }; + + addMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: '', + add_users: [userId], + remove_users: [], + }); + }; + + removeMember = async (groupId: string, userId: string) => { + return this.patchGroup(groupId, { + name: '', + display_name: '', + add_users: [], + remove_users: [userId], + }); + }; + + deleteGroup = async (groupId: string): Promise => { + await this.axios.delete(`/api/v2/groups/${groupId}`); + }; + + getWorkspaceQuota = async ( + username: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspace-quota/${encodeURIComponent(username)}`, + ); + return response.data; + }; + + getAgentListeningPorts = async ( + agentID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaceagents/${agentID}/listening-ports`, + ); + return response.data; + }; + + getWorkspaceAgentSharedPorts = async ( + workspaceID: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceID}/port-share`, + ); + return response.data; + }; + + upsertWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.UpsertWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/workspaces/${workspaceID}/port-share`, + req, + ); + return response.data; + }; + + deleteWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.DeleteWorkspaceAgentPortShareRequest, + ): Promise => { + const response = await this.axios.delete( + `/api/v2/workspaces/${workspaceID}/port-share`, + { data: req }, + ); + + return response.data; + }; + + // getDeploymentSSHConfig is used by the VSCode-Extension. + getDeploymentSSHConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/ssh`); + return response.data; + }; + + getDeploymentConfig = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/config`); + return response.data; + }; + + getDeploymentStats = async (): Promise => { + const response = await this.axios.get(`/api/v2/deployment/stats`); + return response.data; + }; + + getReplicas = async (): Promise => { + const response = await this.axios.get(`/api/v2/replicas`); + return response.data; + }; + + getFile = async (fileId: string): Promise => { + const response = await this.axios.get( + `/api/v2/files/${fileId}`, + { responseType: 'arraybuffer' }, + ); + + return response.data; + }; + + getWorkspaceProxyRegions = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse + >(`/api/v2/regions`); + + return response.data; + }; + + getWorkspaceProxies = async (): Promise< + TypesGen.RegionsResponse + > => { + const response = await this.axios.get< + TypesGen.RegionsResponse + >(`/api/v2/workspaceproxies`); + + return response.data; + }; + + createWorkspaceProxy = async ( + b: TypesGen.CreateWorkspaceProxyRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/workspaceproxies`, b); + return response.data; + }; + + getAppearance = async (): Promise => { + try { + const response = await this.axios.get(`/api/v2/appearance`); + return response.data || {}; + } catch (ex) { + if (isAxiosError(ex) && ex.response?.status === 404) { + return { + application_name: '', + logo_url: '', + notification_banners: [], + service_banner: { + enabled: false, + }, + }; + } + + throw ex; + } + }; + + updateAppearance = async ( + b: TypesGen.AppearanceConfig, + ): Promise => { + const response = await this.axios.put(`/api/v2/appearance`, b); + return response.data; + }; + + getTemplateExamples = async ( + organizationId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/organizations/${organizationId}/templates/examples`, + ); + + return response.data; + }; + + uploadFile = async (file: File): Promise => { + const response = await this.axios.post('/api/v2/files', file, { + headers: { 'Content-Type': 'application/x-tar' }, + }); + + return response.data; + }; + + getTemplateVersionLogs = async ( + versionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${versionId}/logs`, + ); + return response.data; + }; + + updateWorkspaceVersion = async ( + workspace: TypesGen.Workspace, + ): Promise => { + const template = await this.getTemplate(workspace.template_id); + return this.startWorkspace(workspace.id, template.active_version_id); + }; + + getWorkspaceBuildParameters = async ( + workspaceBuildId: TypesGen.WorkspaceBuild['id'], + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, + ); + + return response.data; + }; + + getLicenses = async (): Promise => { + const response = await this.axios.get(`/api/v2/licenses`); + return response.data; + }; + + createLicense = async ( + data: TypesGen.AddLicenseRequest, + ): Promise => { + const response = await this.axios.post(`/api/v2/licenses`, data); + return response.data; + }; + + removeLicense = async (licenseId: number): Promise => { + 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 => { + 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 => { + 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 => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceId}/resolve-autostart`, + ); + return response.data; + }; + + issueReconnectingPTYSignedToken = async ( + params: TypesGen.IssueReconnectingPTYSignedTokenRequest, + ): Promise => { + 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 => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-latency?${params}`, + ); + + return response.data; + }; + + getInsightsUserActivity = async ( + filters: InsightsParams, + ): Promise => { + const params = new URLSearchParams(filters); + const response = await this.axios.get( + `/api/v2/insights/user-activity?${params}`, + ); + + return response.data; + }; + + getInsightsTemplate = async ( + params: InsightsTemplateParams, + ): Promise => { + const searchParams = new URLSearchParams(params); + const response = await this.axios.get( + `/api/v2/insights/templates?${searchParams}`, + ); + + return response.data; + }; + + getHealth = async (force: boolean = false) => { + const params = new URLSearchParams({ force: force.toString() }); + const response = await this.axios.get( + `/api/v2/debug/health?${params}`, + ); + return response.data; + }; + + getHealthSettings = async (): Promise => { + const res = await this.axios.get( + `/api/v2/debug/health/settings`, + ); + + return res.data; + }; + + updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => { + const response = await this.axios.put( + `/api/v2/debug/health/settings`, + data, + ); + + return response.data; + }; + + putFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + deleteFavoriteWorkspace = async (workspaceID: string) => { + await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); + }; + + getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { + const searchParams = new URLSearchParams({ + workspace_id: options.workspaceId, + agent_id: options.agentId, + }); + + try { + const res = await this.axios.get( + `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, + ); + + return res.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + // react-query library does not allow undefined to be returned as a + // query result + return null; + } + + throw error; + } + }; +} + +// This is a hard coded CSRF token/cookie pair for local development. In prod, +// the GoLang webserver generates a random cookie with a new token for each +// document request. For local development, we don't use the Go webserver for +// static files, so this is the 'hack' to make local development work with +// remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" +const csrfToken = + 'KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=='; + +// Always attach CSRF token to all requests. In puppeteer the document is +// undefined. In those cases, just do nothing. +const tokenMetadataElement = + typeof document !== 'undefined' + ? document.head.querySelector('meta[property="csrf-token"]') + : null; + +function getConfiguredAxiosInstance(): AxiosInstance { + const instance = globalAxios.create(); + + // Adds 304 for the default axios validateStatus function + // https://github.com/axios/axios#handling-errors Check status here + // https://httpstatusdogs.com/ + instance.defaults.validateStatus = status => { + return (status >= 200 && status < 300) || status === 304; + }; + + const metadataIsAvailable = + tokenMetadataElement !== null && + tokenMetadataElement.getAttribute('content') !== null; + + if (metadataIsAvailable) { + if (process.env.NODE_ENV === 'development') { + // Development mode uses a hard-coded CSRF token + instance.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken; + instance.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken; + tokenMetadataElement.setAttribute('content', csrfToken); + } else { + instance.defaults.headers.common['X-CSRF-TOKEN'] = + tokenMetadataElement.getAttribute('content') ?? ''; + } + } else { + // Do not write error logs if we are in a FE unit test. + if (process.env.JEST_WORKER_ID === undefined) { + // eslint-disable-next-line no-console -- Function should never run in vendored version of SDK + console.error('CSRF token not found'); + } + } + + return instance; +} + +// Other non-API methods defined here to make it a little easier to find them. +interface ClientApi extends ApiMethods { + getCsrfToken: () => string; + setSessionToken: (token: string) => void; + setHost: (host: string | undefined) => void; + getAxiosInstance: () => AxiosInstance; +} + +export class Api extends ApiMethods implements ClientApi { + constructor() { + const scopedAxiosInstance = getConfiguredAxiosInstance(); + super(scopedAxiosInstance); + } + + // As with ApiMethods, all public methods should be defined with arrow + // function syntax to ensure they can be passed around the React UI without + // losing/detaching their `this` context! + + getCsrfToken = (): string => { + return csrfToken; + }; + + setSessionToken = (token: string): void => { + this.axios.defaults.headers.common['Coder-Session-Token'] = token; + }; + + setHost = (host: string | undefined): void => { + this.axios.defaults.baseURL = host; + }; + + getAxiosInstance = (): AxiosInstance => { + return this.axios; + }; +} + +export const API = new Api(); diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts new file mode 100644 index 00000000..6d401a11 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/errors.ts @@ -0,0 +1,124 @@ +import { type AxiosError, type AxiosResponse, isAxiosError } from 'axios'; + +const Language = { + errorsByCode: { + defaultErrorCode: 'Invalid value', + }, +}; + +export interface FieldError { + field: string; + detail: string; +} + +export type FieldErrors = Record; + +export interface ApiErrorResponse { + message: string; + detail?: string; + validations?: FieldError[]; +} + +export type ApiError = AxiosError & { + response: AxiosResponse; +}; + +export const isApiErrorResponse = (err: unknown): err is ApiErrorResponse => { + return ( + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + (!('detail' in err) || + err.detail === undefined || + typeof err.detail === 'string') && + (!('validations' in err) || + err.validations === undefined || + Array.isArray(err.validations)) + ); +}; + +export const isApiError = (err: unknown): err is ApiError => { + return ( + isAxiosError(err) && + err.response !== undefined && + isApiErrorResponse(err.response.data) + ); +}; + +export const hasApiFieldErrors = (error: ApiError): boolean => + Array.isArray(error.response.data.validations); + +export const isApiValidationError = (error: unknown): error is ApiError => { + return isApiError(error) && hasApiFieldErrors(error); +}; + +export const hasError = (error: unknown) => + error !== undefined && error !== null; + +export const mapApiErrorToFieldErrors = ( + apiErrorResponse: ApiErrorResponse, +): FieldErrors => { + const result: FieldErrors = {}; + + if (apiErrorResponse.validations) { + for (const error of apiErrorResponse.validations) { + result[error.field] = + error.detail || Language.errorsByCode.defaultErrorCode; + } + } + + return result; +}; + +/** + * + * @param error + * @param defaultMessage + * @returns error's message if ApiError or Error, else defaultMessage + */ +export const getErrorMessage = ( + error: unknown, + defaultMessage: string, +): string => { + // if error is API error + // 404s result in the default message being returned + if (isApiError(error) && error.response.data.message) { + return error.response.data.message; + } + if (isApiErrorResponse(error)) { + return error.message; + } + // if error is a non-empty string + if (error && typeof error === 'string') { + return error; + } + return defaultMessage; +}; + +/** + * + * @param error + * @returns a combined validation error message if the error is an ApiError + * and contains validation messages for different form fields. + */ +export const getValidationErrorMessage = (error: unknown): string => { + const validationErrors = + isApiError(error) && error.response.data.validations + ? error.response.data.validations + : []; + return validationErrors.map(error => error.detail).join('\n'); +}; + +export const getErrorDetail = (error: unknown): string | undefined | null => { + if (error instanceof Error) { + return 'Please check the developer console for more details.'; + } + if (isApiError(error)) { + return error.response.data.detail; + } + if (isApiErrorResponse(error)) { + return error.detail; + } + return null; +}; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts new file mode 100644 index 00000000..2e3b4f04 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/typesGenerated.ts @@ -0,0 +1,2599 @@ +// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. + +// The code below is generated from codersdk. + +// From codersdk/templates.go +export interface ACLAvailable { + readonly users: readonly ReducedUser[]; + readonly groups: readonly Group[]; +} + +// From codersdk/apikey.go +export interface APIKey { + readonly id: string; + readonly user_id: string; + readonly last_used: string; + readonly expires_at: string; + readonly created_at: string; + readonly updated_at: string; + readonly login_type: LoginType; + readonly scope: APIKeyScope; + readonly token_name: string; + readonly lifetime_seconds: number; +} + +// From codersdk/apikey.go +export interface APIKeyWithOwner extends APIKey { + readonly username: string; +} + +// From codersdk/licenses.go +export interface AddLicenseRequest { + readonly license: string; +} + +// From codersdk/templates.go +export interface AgentStatsReportResponse { + readonly num_comms: number; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/deployment.go +export interface AppHostResponse { + readonly host: string; +} + +// From codersdk/deployment.go +export interface AppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; + readonly support_links?: readonly LinkConfig[]; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsRequest { + readonly all: boolean; +} + +// From codersdk/templates.go +export interface ArchiveTemplateVersionsResponse { + readonly template_id: string; + readonly archived_ids: readonly string[]; +} + +// From codersdk/roles.go +export interface AssignableRoles extends Role { + readonly assignable: boolean; + readonly built_in: boolean; +} + +// From codersdk/audit.go +export type AuditDiff = Record; + +// From codersdk/audit.go +export interface AuditDiffField { + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly old?: any; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly new?: any; + readonly secret: boolean; +} + +// From codersdk/audit.go +export interface AuditLog { + readonly id: string; + readonly request_id: string; + readonly time: string; + readonly organization_id: string; + // Named type "net/netip.Addr" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly ip: any; + readonly user_agent: string; + readonly resource_type: ResourceType; + readonly resource_id: string; + readonly resource_target: string; + readonly resource_icon: string; + readonly action: AuditAction; + readonly diff: AuditDiff; + readonly status_code: number; + readonly additional_fields: Record; + readonly description: string; + readonly resource_link: string; + readonly is_deleted: boolean; + readonly user?: User; +} + +// From codersdk/audit.go +export interface AuditLogResponse { + readonly audit_logs: readonly AuditLog[]; + readonly count: number; +} + +// From codersdk/audit.go +export interface AuditLogsRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/users.go +export interface AuthMethod { + readonly enabled: boolean; +} + +// From codersdk/users.go +export interface AuthMethods { + readonly terms_of_service_url?: string; + readonly password: AuthMethod; + readonly github: AuthMethod; + readonly oidc: OIDCAuthMethod; +} + +// From codersdk/authorization.go +export interface AuthorizationCheck { + readonly object: AuthorizationObject; + readonly action: RBACAction; +} + +// From codersdk/authorization.go +export interface AuthorizationObject { + readonly resource_type: RBACResource; + readonly owner_id?: string; + readonly organization_id?: string; + readonly resource_id?: string; +} + +// From codersdk/authorization.go +export interface AuthorizationRequest { + readonly checks: Record; +} + +// From codersdk/authorization.go +export type AuthorizationResponse = Record; + +// From codersdk/deployment.go +export interface AvailableExperiments { + readonly safe: readonly Experiment[]; +} + +// From codersdk/deployment.go +export interface BannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface BuildInfoResponse { + readonly external_url: string; + readonly version: string; + readonly dashboard_url: string; + readonly workspace_proxy: boolean; + readonly agent_api_version: string; + readonly upgrade_message: string; + readonly deployment_id: string; +} + +// From codersdk/insights.go +export interface ConnectionLatency { + readonly p50: number; + readonly p95: number; +} + +// From codersdk/users.go +export interface ConvertLoginRequest { + readonly to_type: LoginType; + readonly password: string; +} + +// From codersdk/users.go +export interface CreateFirstUserRequest { + readonly email: string; + readonly username: string; + readonly password: string; + readonly trial: boolean; + readonly trial_info: CreateFirstUserTrialInfo; +} + +// From codersdk/users.go +export interface CreateFirstUserResponse { + readonly user_id: string; + readonly organization_id: string; +} + +// From codersdk/users.go +export interface CreateFirstUserTrialInfo { + readonly first_name: string; + readonly last_name: string; + readonly phone_number: string; + readonly job_title: string; + readonly company_name: string; + readonly country: string; + readonly developers: string; +} + +// From codersdk/groups.go +export interface CreateGroupRequest { + readonly name: string; + readonly display_name: string; + readonly avatar_url: string; + readonly quota_allowance: number; +} + +// From codersdk/organizations.go +export interface CreateOrganizationRequest { + readonly name: string; +} + +// From codersdk/organizations.go +export interface CreateTemplateRequest { + readonly name: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly template_version_id: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly failure_ttl_ms?: number; + readonly dormant_ttl_ms?: number; + readonly delete_ttl_ms?: number; + readonly disable_everyone_group_access: boolean; + readonly require_active_version: boolean; +} + +// From codersdk/templateversions.go +export interface CreateTemplateVersionDryRunRequest { + readonly workspace_name: string; + readonly rich_parameter_values: readonly WorkspaceBuildParameter[]; + readonly user_variable_values?: readonly VariableValue[]; +} + +// From codersdk/organizations.go +export interface CreateTemplateVersionRequest { + readonly name?: string; + readonly message?: string; + readonly template_id?: string; + readonly storage_method: ProvisionerStorageMethod; + readonly file_id?: string; + readonly example_id?: string; + readonly provisioner: ProvisionerType; + readonly tags: Record; + readonly user_variable_values?: readonly VariableValue[]; +} + +// From codersdk/audit.go +export interface CreateTestAuditLogRequest { + readonly action?: AuditAction; + readonly resource_type?: ResourceType; + readonly resource_id?: string; + readonly additional_fields?: Record; + readonly time?: string; + readonly build_reason?: BuildReason; +} + +// From codersdk/apikey.go +export interface CreateTokenRequest { + readonly lifetime: number; + readonly scope: APIKeyScope; + readonly token_name: string; +} + +// From codersdk/users.go +export interface CreateUserRequest { + readonly email: string; + readonly username: string; + readonly password: string; + readonly login_type: LoginType; + readonly disable_login: boolean; + readonly organization_id: string; +} + +// From codersdk/workspaces.go +export interface CreateWorkspaceBuildRequest { + readonly template_version_id?: string; + readonly transition: WorkspaceTransition; + readonly dry_run?: boolean; + readonly state?: string; + readonly orphan?: boolean; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; + readonly log_level?: ProvisionerLogLevel; +} + +// From codersdk/workspaceproxy.go +export interface CreateWorkspaceProxyRequest { + readonly name: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/organizations.go +export interface CreateWorkspaceRequest { + readonly template_id?: string; + readonly template_version_id?: string; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; + readonly automatic_updates?: AutomaticUpdates; +} + +// From codersdk/deployment.go +export interface DAUEntry { + readonly date: string; + readonly amount: number; +} + +// From codersdk/deployment.go +export interface DAURequest { + readonly TZHourOffset: number; +} + +// From codersdk/deployment.go +export interface DAUsResponse { + readonly entries: readonly DAUEntry[]; + readonly tz_hour_offset: number; +} + +// From codersdk/deployment.go +export interface DERP { + readonly server: DERPServerConfig; + readonly config: DERPConfig; +} + +// From codersdk/deployment.go +export interface DERPConfig { + readonly block_direct: boolean; + readonly force_websockets: boolean; + readonly url: string; + readonly path: string; +} + +// From codersdk/workspaceagents.go +export interface DERPRegion { + readonly preferred: boolean; + readonly latency_ms: number; +} + +// From codersdk/deployment.go +export interface DERPServerConfig { + readonly enable: boolean; + readonly region_id: number; + readonly region_code: string; + readonly region_name: string; + readonly stun_addresses: string[]; + readonly relay_url: string; +} + +// From codersdk/deployment.go +export interface DangerousConfig { + readonly allow_path_app_sharing: boolean; + readonly allow_path_app_site_owner_access: boolean; + readonly allow_all_cors: boolean; +} + +// From codersdk/workspaceagentportshare.go +export interface DeleteWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; +} + +// From codersdk/deployment.go +export interface DeploymentConfig { + readonly config?: DeploymentValues; + readonly options?: SerpentOptionSet; +} + +// From codersdk/deployment.go +export interface DeploymentStats { + readonly aggregated_from: string; + readonly collected_at: string; + readonly next_update_at: string; + readonly workspaces: WorkspaceDeploymentStats; + readonly session_count: SessionCountDeploymentStats; +} + +// From codersdk/deployment.go +export interface DeploymentValues { + readonly verbose?: boolean; + readonly access_url?: string; + readonly wildcard_access_url?: string; + readonly docs_url?: string; + readonly redirect_to_access_url?: boolean; + readonly http_address?: string; + readonly autobuild_poll_interval?: number; + readonly job_hang_detector_interval?: number; + readonly derp?: DERP; + readonly prometheus?: PrometheusConfig; + readonly pprof?: PprofConfig; + readonly proxy_trusted_headers?: string[]; + readonly proxy_trusted_origins?: string[]; + readonly cache_directory?: string; + readonly in_memory_database?: boolean; + readonly pg_connection_url?: string; + readonly pg_auth?: string; + readonly oauth2?: OAuth2Config; + readonly oidc?: OIDCConfig; + readonly telemetry?: TelemetryConfig; + readonly tls?: TLSConfig; + readonly trace?: TraceConfig; + readonly secure_auth_cookie?: boolean; + readonly strict_transport_security?: number; + readonly strict_transport_security_options?: string[]; + readonly ssh_keygen_algorithm?: string; + readonly metrics_cache_refresh_interval?: number; + readonly agent_stat_refresh_interval?: number; + readonly agent_fallback_troubleshooting_url?: string; + readonly browser_only?: boolean; + readonly scim_api_key?: string; + readonly external_token_encryption_keys?: string[]; + readonly provisioner?: ProvisionerConfig; + readonly rate_limit?: RateLimitConfig; + readonly experiments?: string[]; + readonly update_check?: boolean; + readonly swagger?: SwaggerConfig; + readonly logging?: LoggingConfig; + readonly dangerous?: DangerousConfig; + readonly disable_path_apps?: boolean; + readonly session_lifetime?: SessionLifetime; + readonly disable_password_auth?: boolean; + readonly support?: SupportConfig; + readonly external_auth?: readonly ExternalAuthConfig[]; + readonly config_ssh?: SSHConfig; + readonly wgtunnel_host?: string; + readonly disable_owner_workspace_exec?: boolean; + readonly proxy_health_status_interval?: number; + readonly enable_terraform_debug_mode?: boolean; + readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig; + readonly web_terminal_renderer?: string; + readonly allow_workspace_renames?: boolean; + readonly healthcheck?: HealthcheckConfig; + readonly cli_upgrade_message?: string; + readonly terms_of_service_url?: string; + readonly config?: string; + readonly write_config?: boolean; + readonly address?: string; +} + +// From codersdk/deployment.go +export interface Entitlements { + readonly features: Record; + readonly warnings: readonly string[]; + readonly errors: readonly string[]; + readonly has_license: boolean; + readonly trial: boolean; + readonly require_telemetry: boolean; + readonly refreshed_at: string; +} + +// From codersdk/deployment.go +export type Experiments = readonly Experiment[]; + +// From codersdk/externalauth.go +export interface ExternalAuth { + readonly authenticated: boolean; + readonly device: boolean; + readonly display_name: string; + readonly user?: ExternalAuthUser; + readonly app_installable: boolean; + readonly installations: readonly ExternalAuthAppInstallation[]; + readonly app_install_url: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthAppInstallation { + readonly id: number; + readonly account: ExternalAuthUser; + readonly configure_url: string; +} + +// From codersdk/deployment.go +export interface ExternalAuthConfig { + readonly type: string; + readonly client_id: string; + readonly id: string; + readonly auth_url: string; + readonly token_url: string; + readonly validate_url: string; + readonly app_install_url: string; + readonly app_installations_url: string; + readonly no_refresh: boolean; + readonly scopes: readonly string[]; + readonly extra_token_keys: readonly string[]; + readonly device_flow: boolean; + readonly device_code_url: string; + readonly regex: string; + readonly display_name: string; + readonly display_icon: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDevice { + readonly device_code: string; + readonly user_code: string; + readonly verification_uri: string; + readonly expires_in: number; + readonly interval: number; +} + +// From codersdk/externalauth.go +export interface ExternalAuthDeviceExchange { + readonly device_code: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLink { + readonly provider_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly has_refresh_token: boolean; + readonly expires: string; + readonly authenticated: boolean; + readonly validate_error: string; +} + +// From codersdk/externalauth.go +export interface ExternalAuthLinkProvider { + readonly id: string; + readonly type: string; + readonly device: boolean; + readonly display_name: string; + readonly display_icon: string; + readonly allow_refresh: boolean; + readonly allow_validate: boolean; +} + +// From codersdk/externalauth.go +export interface ExternalAuthUser { + readonly login: string; + readonly avatar_url: string; + readonly profile_url: string; + readonly name: string; +} + +// From codersdk/deployment.go +export interface Feature { + readonly entitlement: Entitlement; + readonly enabled: boolean; + readonly limit?: number; + readonly actual?: number; +} + +// From codersdk/apikey.go +export interface GenerateAPIKeyResponse { + readonly key: string; +} + +// From codersdk/users.go +export interface GetUsersResponse { + readonly users: readonly User[]; + readonly count: number; +} + +// From codersdk/gitsshkey.go +export interface GitSSHKey { + readonly user_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly public_key: string; +} + +// From codersdk/groups.go +export interface Group { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly organization_id: string; + readonly members: readonly ReducedUser[]; + readonly avatar_url: string; + readonly quota_allowance: number; + readonly source: GroupSource; +} + +// From codersdk/workspaceapps.go +export interface Healthcheck { + readonly url: string; + readonly interval: number; + readonly threshold: number; +} + +// From codersdk/deployment.go +export interface HealthcheckConfig { + readonly refresh: number; + readonly threshold_database: number; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenRequest { + readonly url: string; + readonly agentID: string; +} + +// From codersdk/workspaceagents.go +export interface IssueReconnectingPTYSignedTokenResponse { + readonly signed_token: string; +} + +// From codersdk/jfrog.go +export interface JFrogXrayScan { + readonly workspace_id: string; + readonly agent_id: string; + readonly critical: number; + readonly high: number; + readonly medium: number; + readonly results_url: string; +} + +// From codersdk/licenses.go +export interface License { + readonly id: number; + readonly uuid: string; + readonly uploaded_at: string; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly claims: Record; +} + +// From codersdk/deployment.go +export interface LinkConfig { + readonly name: string; + readonly target: string; + readonly icon: string; +} + +// From codersdk/externalauth.go +export interface ListUserExternalAuthResponse { + readonly providers: readonly ExternalAuthLinkProvider[]; + readonly links: readonly ExternalAuthLink[]; +} + +// From codersdk/deployment.go +export interface LoggingConfig { + readonly log_filter: string[]; + readonly human: string; + readonly json: string; + readonly stackdriver: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordRequest { + readonly email: string; + readonly password: string; +} + +// From codersdk/users.go +export interface LoginWithPasswordResponse { + readonly session_token: string; +} + +// From codersdk/users.go +export interface MinimalUser { + readonly id: string; + readonly username: string; + readonly avatar_url: string; +} + +// From codersdk/oauth2.go +export interface OAuth2AppEndpoints { + readonly authorization: string; + readonly token: string; + readonly device_authorization: string; +} + +// From codersdk/deployment.go +export interface OAuth2Config { + readonly github: OAuth2GithubConfig; +} + +// From codersdk/deployment.go +export interface OAuth2GithubConfig { + readonly client_id: string; + readonly client_secret: string; + readonly allowed_orgs: string[]; + readonly allowed_teams: string[]; + readonly allow_signups: boolean; + readonly allow_everyone: boolean; + readonly enterprise_base_url: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderApp { + readonly id: string; + readonly name: string; + readonly callback_url: string; + readonly icon: string; + readonly endpoints: OAuth2AppEndpoints; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppFilter { + readonly user_id?: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecret { + readonly id: string; + readonly last_used_at?: string; + readonly client_secret_truncated: string; +} + +// From codersdk/oauth2.go +export interface OAuth2ProviderAppSecretFull { + readonly id: string; + readonly client_secret_full: string; +} + +// From codersdk/users.go +export interface OAuthConversionResponse { + readonly state_string: string; + readonly expires_at: string; + readonly to_type: LoginType; + readonly user_id: string; +} + +// From codersdk/users.go +export interface OIDCAuthMethod extends AuthMethod { + readonly signInText: string; + readonly iconUrl: string; +} + +// From codersdk/deployment.go +export interface OIDCConfig { + readonly allow_signups: boolean; + readonly client_id: string; + readonly client_secret: string; + readonly client_key_file: string; + readonly client_cert_file: string; + readonly email_domain: string[]; + readonly issuer_url: string; + readonly scopes: string[]; + readonly ignore_email_verified: boolean; + readonly username_field: string; + readonly email_field: string; + readonly auth_url_params: Record; + readonly ignore_user_info: boolean; + readonly group_auto_create: boolean; + readonly group_regex_filter: string; + readonly group_allow_list: string[]; + readonly groups_field: string; + readonly group_mapping: Record; + readonly user_role_field: string; + readonly user_role_mapping: Record; + readonly user_roles_default: string[]; + readonly sign_in_text: string; + readonly icon_url: string; + readonly signups_disabled_text: string; +} + +// From codersdk/organizations.go +export interface Organization { + readonly id: string; + readonly name: string; + readonly created_at: string; + readonly updated_at: string; + readonly is_default: boolean; +} + +// From codersdk/organizations.go +export interface OrganizationMember { + readonly user_id: string; + readonly organization_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly roles: readonly SlimRole[]; +} + +// From codersdk/pagination.go +export interface Pagination { + readonly after_id?: string; + readonly limit?: number; + readonly offset?: number; +} + +// From codersdk/groups.go +export interface PatchGroupRequest { + readonly add_users: readonly string[]; + readonly remove_users: readonly string[]; + readonly name: string; + readonly display_name?: string; + readonly avatar_url?: string; + readonly quota_allowance?: number; +} + +// From codersdk/templateversions.go +export interface PatchTemplateVersionRequest { + readonly name: string; + readonly message?: string; +} + +// From codersdk/workspaceproxy.go +export interface PatchWorkspaceProxy { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon: string; + readonly regenerate_token: boolean; +} + +// From codersdk/roles.go +export interface Permission { + readonly negate: boolean; + readonly resource_type: RBACResource; + readonly action: RBACAction; +} + +// From codersdk/oauth2.go +export interface PostOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/deployment.go +export interface PprofConfig { + readonly enable: boolean; + readonly address: string; +} + +// From codersdk/deployment.go +export interface PrometheusConfig { + readonly enable: boolean; + readonly address: string; + readonly collect_agent_stats: boolean; + readonly collect_db_metrics: boolean; + readonly aggregate_agent_stats_by: string[]; +} + +// From codersdk/deployment.go +export interface ProvisionerConfig { + readonly daemons: number; + readonly daemon_types: string[]; + readonly daemon_poll_interval: number; + readonly daemon_poll_jitter: number; + readonly force_cancel_interval: number; + readonly daemon_psk: string; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerDaemon { + readonly id: string; + readonly created_at: string; + readonly last_seen_at?: string; + readonly name: string; + readonly version: string; + readonly api_version: string; + readonly provisioners: readonly ProvisionerType[]; + readonly tags: Record; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJob { + readonly id: string; + readonly created_at: string; + readonly started_at?: string; + readonly completed_at?: string; + readonly canceled_at?: string; + readonly error?: string; + readonly error_code?: JobErrorCode; + readonly status: ProvisionerJobStatus; + readonly worker_id?: string; + readonly file_id: string; + readonly tags: Record; + readonly queue_position: number; + readonly queue_size: number; +} + +// From codersdk/provisionerdaemons.go +export interface ProvisionerJobLog { + readonly id: number; + readonly created_at: string; + readonly log_source: LogSource; + readonly log_level: LogLevel; + readonly stage: string; + readonly output: string; +} + +// From codersdk/workspaceproxy.go +export interface ProxyHealthReport { + readonly errors: readonly string[]; + readonly warnings: readonly string[]; +} + +// From codersdk/workspaces.go +export interface PutExtendWorkspaceRequest { + readonly deadline: string; +} + +// From codersdk/oauth2.go +export interface PutOAuth2ProviderAppRequest { + readonly name: string; + readonly callback_url: string; + readonly icon: string; +} + +// From codersdk/deployment.go +export interface RateLimitConfig { + readonly disable_all: boolean; + readonly api: number; +} + +// From codersdk/users.go +export interface ReducedUser extends MinimalUser { + readonly name: string; + readonly email: string; + readonly created_at: string; + readonly last_seen_at: string; + readonly status: UserStatus; + readonly login_type: LoginType; + readonly theme_preference: string; +} + +// From codersdk/workspaceproxy.go +export interface Region { + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon_url: string; + readonly healthy: boolean; + readonly path_app_url: string; + readonly wildcard_hostname: string; +} + +// From codersdk/workspaceproxy.go +export interface RegionsResponse { + readonly regions: readonly R[]; +} + +// From codersdk/replicas.go +export interface Replica { + readonly id: string; + readonly hostname: string; + readonly created_at: string; + readonly relay_address: string; + readonly region_id: number; + readonly error: string; + readonly database_latency: number; +} + +// From codersdk/workspaces.go +export interface ResolveAutostartResponse { + readonly parameter_mismatch: boolean; +} + +// From codersdk/client.go +export interface Response { + readonly message: string; + readonly detail?: string; + readonly validations?: readonly ValidationError[]; +} + +// From codersdk/roles.go +export interface Role { + readonly name: string; + readonly organization_id: string; + readonly display_name: string; + readonly site_permissions: readonly Permission[]; + readonly organization_permissions: Record; + readonly user_permissions: readonly Permission[]; +} + +// From codersdk/deployment.go +export interface SSHConfig { + readonly DeploymentName: string; + readonly SSHConfigOptions: string[]; +} + +// From codersdk/deployment.go +export interface SSHConfigResponse { + readonly hostname_prefix: string; + readonly ssh_config_options: Record; +} + +// From codersdk/serversentevents.go +export interface ServerSentEvent { + readonly type: ServerSentEventType; + // Empty interface{} type, cannot resolve the type. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} + readonly data: any; +} + +// From codersdk/deployment.go +export interface ServiceBannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + +// From codersdk/deployment.go +export interface SessionCountDeploymentStats { + readonly vscode: number; + readonly ssh: number; + readonly jetbrains: number; + readonly reconnecting_pty: number; +} + +// From codersdk/deployment.go +export interface SessionLifetime { + readonly disable_expiry_refresh?: boolean; + readonly default_duration: number; + readonly max_token_lifetime?: number; +} + +// From codersdk/roles.go +export interface SlimRole { + readonly name: string; + readonly display_name: string; +} + +// From codersdk/deployment.go +export interface SupportConfig { + readonly links: readonly LinkConfig[]; +} + +// From codersdk/deployment.go +export interface SwaggerConfig { + readonly enable: boolean; +} + +// From codersdk/deployment.go +export interface TLSConfig { + readonly enable: boolean; + readonly address: string; + readonly redirect_http: boolean; + readonly cert_file: string[]; + readonly client_auth: string; + readonly client_ca_file: string; + readonly key_file: string[]; + readonly min_version: string; + readonly client_cert_file: string; + readonly client_key_file: string; + readonly supported_ciphers: string[]; + readonly allow_insecure_ciphers: boolean; +} + +// From codersdk/deployment.go +export interface TelemetryConfig { + readonly enable: boolean; + readonly trace: boolean; + readonly url: string; +} + +// From codersdk/templates.go +export interface Template { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly organization_id: string; + readonly name: string; + readonly display_name: string; + readonly provisioner: ProvisionerType; + readonly active_version_id: string; + readonly active_user_count: number; + readonly build_time_stats: TemplateBuildTimeStats; + readonly description: string; + readonly deprecated: boolean; + readonly deprecation_message: string; + readonly icon: string; + readonly default_ttl_ms: number; + readonly activity_bump_ms: number; + readonly autostop_requirement: TemplateAutostopRequirement; + readonly autostart_requirement: TemplateAutostartRequirement; + readonly created_by_id: string; + readonly created_by_name: string; + readonly allow_user_autostart: boolean; + readonly allow_user_autostop: boolean; + readonly allow_user_cancel_workspace_jobs: boolean; + readonly failure_ttl_ms: number; + readonly time_til_dormant_ms: number; + readonly time_til_dormant_autodelete_ms: number; + readonly require_active_version: boolean; + readonly max_port_share_level: WorkspaceAgentPortShareLevel; +} + +// From codersdk/templates.go +export interface TemplateACL { + readonly users: readonly TemplateUser[]; + readonly group: readonly TemplateGroup[]; +} + +// From codersdk/insights.go +export interface TemplateAppUsage { + readonly template_ids: readonly string[]; + readonly type: TemplateAppsType; + readonly display_name: string; + readonly slug: string; + readonly icon: string; + readonly seconds: number; + readonly times_used: number; +} + +// From codersdk/templates.go +export interface TemplateAutostartRequirement { + readonly days_of_week: readonly string[]; +} + +// From codersdk/templates.go +export interface TemplateAutostopRequirement { + readonly days_of_week: readonly string[]; + readonly weeks: number; +} + +// From codersdk/templates.go +export type TemplateBuildTimeStats = Record< + WorkspaceTransition, + TransitionStats +>; + +// From codersdk/templates.go +export interface TemplateExample { + readonly id: string; + readonly url: string; + readonly name: string; + readonly description: string; + readonly icon: string; + readonly tags: readonly string[]; + readonly markdown: string; +} + +// From codersdk/templates.go +export interface TemplateGroup extends Group { + readonly role: TemplateRole; +} + +// From codersdk/insights.go +export interface TemplateInsightsIntervalReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly interval: InsightsReportInterval; + readonly active_users: number; +} + +// From codersdk/insights.go +export interface TemplateInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly active_users: number; + readonly apps_usage: readonly TemplateAppUsage[]; + readonly parameters_usage: readonly TemplateParameterUsage[]; +} + +// From codersdk/insights.go +export interface TemplateInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly interval: InsightsReportInterval; + readonly sections: readonly TemplateInsightsSection[]; +} + +// From codersdk/insights.go +export interface TemplateInsightsResponse { + readonly report?: TemplateInsightsReport; + readonly interval_reports?: readonly TemplateInsightsIntervalReport[]; +} + +// From codersdk/insights.go +export interface TemplateParameterUsage { + readonly template_ids: readonly string[]; + readonly display_name: string; + readonly name: string; + readonly type: string; + readonly description: string; + readonly options?: readonly TemplateVersionParameterOption[]; + readonly values: readonly TemplateParameterValue[]; +} + +// From codersdk/insights.go +export interface TemplateParameterValue { + readonly value: string; + readonly count: number; +} + +// From codersdk/templates.go +export interface TemplateUser extends User { + readonly role: TemplateRole; +} + +// From codersdk/templateversions.go +export interface TemplateVersion { + readonly id: string; + readonly template_id?: string; + readonly organization_id?: string; + readonly created_at: string; + readonly updated_at: string; + readonly name: string; + readonly message: string; + readonly job: ProvisionerJob; + readonly readme: string; + readonly created_by: MinimalUser; + readonly archived: boolean; + readonly warnings?: readonly TemplateVersionWarning[]; +} + +// From codersdk/templateversions.go +export interface TemplateVersionExternalAuth { + readonly id: string; + readonly type: string; + readonly display_name: string; + readonly display_icon: string; + readonly authenticate_url: string; + readonly authenticated: boolean; + readonly optional?: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameter { + readonly name: string; + readonly display_name?: string; + readonly description: string; + readonly description_plaintext: string; + readonly type: string; + readonly mutable: boolean; + readonly default_value: string; + readonly icon: string; + readonly options: readonly TemplateVersionParameterOption[]; + readonly validation_error?: string; + readonly validation_regex?: string; + readonly validation_min?: number; + readonly validation_max?: number; + readonly validation_monotonic?: ValidationMonotonicOrder; + readonly required: boolean; + readonly ephemeral: boolean; +} + +// From codersdk/templateversions.go +export interface TemplateVersionParameterOption { + readonly name: string; + readonly description: string; + readonly value: string; + readonly icon: string; +} + +// From codersdk/templateversions.go +export interface TemplateVersionVariable { + readonly name: string; + readonly description: string; + readonly type: string; + readonly value: string; + readonly default_value: string; + readonly required: boolean; + readonly sensitive: boolean; +} + +// From codersdk/templates.go +export interface TemplateVersionsByTemplateRequest extends Pagination { + readonly template_id: string; + readonly include_archived: boolean; +} + +// From codersdk/apikey.go +export interface TokenConfig { + readonly max_token_lifetime: number; +} + +// From codersdk/apikey.go +export interface TokensFilter { + readonly include_all: boolean; +} + +// From codersdk/deployment.go +export interface TraceConfig { + readonly enable: boolean; + readonly honeycomb_api_key: string; + readonly capture_logs: boolean; + readonly data_dog: boolean; +} + +// From codersdk/templates.go +export interface TransitionStats { + readonly P50?: number; + readonly P95?: number; +} + +// From codersdk/templates.go +export interface UpdateActiveTemplateVersion { + readonly id: string; +} + +// From codersdk/deployment.go +export interface UpdateAppearanceConfig { + readonly application_name: string; + readonly logo_url: string; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; +} + +// From codersdk/updatecheck.go +export interface UpdateCheckResponse { + readonly current: boolean; + readonly version: string; + readonly url: string; +} + +// From codersdk/organizations.go +export interface UpdateOrganizationRequest { + readonly name: string; +} + +// From codersdk/users.go +export interface UpdateRoles { + readonly roles: readonly string[]; +} + +// From codersdk/templates.go +export interface UpdateTemplateACL { + readonly user_perms?: Record; + readonly group_perms?: Record; +} + +// From codersdk/templates.go +export interface UpdateTemplateMeta { + readonly name?: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly default_ttl_ms?: number; + readonly activity_bump_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly autostart_requirement?: TemplateAutostartRequirement; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly failure_ttl_ms?: number; + readonly time_til_dormant_ms?: number; + readonly time_til_dormant_autodelete_ms?: number; + readonly update_workspace_last_used_at: boolean; + readonly update_workspace_dormant_at: boolean; + readonly require_active_version?: boolean; + readonly deprecation_message?: string; + readonly disable_everyone_group_access: boolean; + readonly max_port_share_level?: WorkspaceAgentPortShareLevel; +} + +// From codersdk/users.go +export interface UpdateUserAppearanceSettingsRequest { + readonly theme_preference: string; +} + +// From codersdk/users.go +export interface UpdateUserPasswordRequest { + readonly old_password: string; + readonly password: string; +} + +// From codersdk/users.go +export interface UpdateUserProfileRequest { + readonly username: string; + readonly name: string; +} + +// From codersdk/users.go +export interface UpdateUserQuietHoursScheduleRequest { + readonly schedule: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutomaticUpdatesRequest { + readonly automatic_updates: AutomaticUpdates; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceAutostartRequest { + readonly schedule?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceDormancy { + readonly dormant: boolean; +} + +// From codersdk/workspaceproxy.go +export interface UpdateWorkspaceProxyResponse { + readonly proxy: WorkspaceProxy; + readonly proxy_token: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceRequest { + readonly name?: string; +} + +// From codersdk/workspaces.go +export interface UpdateWorkspaceTTLRequest { + readonly ttl_ms?: number; +} + +// From codersdk/files.go +export interface UploadResponse { + readonly hash: string; +} + +// From codersdk/workspaceagentportshare.go +export interface UpsertWorkspaceAgentPortShareRequest { + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/users.go +export interface User extends ReducedUser { + readonly organization_ids: readonly string[]; + readonly roles: readonly SlimRole[]; +} + +// From codersdk/insights.go +export interface UserActivity { + readonly template_ids: readonly string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly seconds: number; +} + +// From codersdk/insights.go +export interface UserActivityInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly users: readonly UserActivity[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsResponse { + readonly report: UserActivityInsightsReport; +} + +// From codersdk/insights.go +export interface UserLatency { + readonly template_ids: readonly string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly latency_ms: ConnectionLatency; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; + readonly users: readonly UserLatency[]; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: readonly string[]; +} + +// From codersdk/insights.go +export interface UserLatencyInsightsResponse { + readonly report: UserLatencyInsightsReport; +} + +// From codersdk/users.go +export interface UserLoginType { + readonly login_type: LoginType; +} + +// From codersdk/users.go +export interface UserParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/deployment.go +export interface UserQuietHoursScheduleConfig { + readonly default_schedule: string; + readonly allow_user_custom: boolean; +} + +// From codersdk/users.go +export interface UserQuietHoursScheduleResponse { + readonly raw_schedule: string; + readonly user_set: boolean; + readonly user_can_set: boolean; + readonly time: string; + readonly timezone: string; + readonly next: string; +} + +// From codersdk/users.go +export interface UserRoles { + readonly roles: readonly string[]; + readonly organization_roles: Record; +} + +// From codersdk/users.go +export interface UsersRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/client.go +export interface ValidationError { + readonly field: string; + readonly detail: string; +} + +// From codersdk/organizations.go +export interface VariableValue { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface Workspace { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly owner_id: string; + readonly owner_name: string; + readonly owner_avatar_url: string; + readonly organization_id: string; + readonly template_id: string; + readonly template_name: string; + readonly template_display_name: string; + readonly template_icon: string; + readonly template_allow_user_cancel_workspace_jobs: boolean; + readonly template_active_version_id: string; + readonly template_require_active_version: boolean; + readonly latest_build: WorkspaceBuild; + readonly outdated: boolean; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly last_used_at: string; + readonly deleting_at?: string; + readonly dormant_at?: string; + readonly health: WorkspaceHealth; + readonly automatic_updates: AutomaticUpdates; + readonly allow_renames: boolean; + readonly favorite: boolean; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgent { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly first_connected_at?: string; + readonly last_connected_at?: string; + readonly disconnected_at?: string; + readonly started_at?: string; + readonly ready_at?: string; + readonly status: WorkspaceAgentStatus; + readonly lifecycle_state: WorkspaceAgentLifecycle; + readonly name: string; + readonly resource_id: string; + readonly instance_id?: string; + readonly architecture: string; + readonly environment_variables: Record; + readonly operating_system: string; + readonly logs_length: number; + readonly logs_overflowed: boolean; + readonly directory?: string; + readonly expanded_directory?: string; + readonly version: string; + readonly api_version: string; + readonly apps: readonly WorkspaceApp[]; + readonly latency?: Record; + readonly connection_timeout_seconds: number; + readonly troubleshooting_url: string; + readonly subsystems: readonly AgentSubsystem[]; + readonly health: WorkspaceAgentHealth; + readonly display_apps: readonly DisplayApp[]; + readonly log_sources: readonly WorkspaceAgentLogSource[]; + readonly scripts: readonly WorkspaceAgentScript[]; + readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentHealth { + readonly healthy: boolean; + readonly reason?: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPort { + readonly process_name: string; + readonly network: string; + readonly port: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentListeningPortsResponse { + readonly ports: readonly WorkspaceAgentListeningPort[]; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLog { + readonly id: number; + readonly created_at: string; + readonly output: string; + readonly level: LogLevel; + readonly source_id: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentLogSource { + readonly workspace_agent_id: string; + readonly id: string; + readonly created_at: string; + readonly display_name: string; + readonly icon: string; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadata { + readonly result: WorkspaceAgentMetadataResult; + readonly description: WorkspaceAgentMetadataDescription; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataDescription { + readonly display_name: string; + readonly key: string; + readonly script: string; + readonly interval: number; + readonly timeout: number; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentMetadataResult { + readonly collected_at: string; + readonly age: number; + readonly value: string; + readonly error: string; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShare { + readonly workspace_id: string; + readonly agent_name: string; + readonly port: number; + readonly share_level: WorkspaceAgentPortShareLevel; + readonly protocol: WorkspaceAgentPortShareProtocol; +} + +// From codersdk/workspaceagentportshare.go +export interface WorkspaceAgentPortShares { + readonly shares: readonly WorkspaceAgentPortShare[]; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentScript { + readonly log_source_id: string; + readonly log_path: string; + readonly script: string; + readonly cron: string; + readonly run_on_start: boolean; + readonly run_on_stop: boolean; + readonly start_blocks_login: boolean; + readonly timeout: number; +} + +// From codersdk/workspaceapps.go +export interface WorkspaceApp { + readonly id: string; + readonly url: string; + readonly external: boolean; + readonly slug: string; + readonly display_name: string; + readonly command?: string; + readonly icon?: string; + readonly subdomain: boolean; + readonly subdomain_name?: string; + readonly sharing_level: WorkspaceAppSharingLevel; + readonly healthcheck: Healthcheck; + readonly health: WorkspaceAppHealth; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuild { + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly workspace_id: string; + readonly workspace_name: string; + readonly workspace_owner_id: string; + readonly workspace_owner_name: string; + readonly workspace_owner_avatar_url: string; + readonly template_version_id: string; + readonly template_version_name: string; + readonly build_number: number; + readonly transition: WorkspaceTransition; + readonly initiator_id: string; + readonly initiator_name: string; + readonly job: ProvisionerJob; + readonly reason: BuildReason; + readonly resources: readonly WorkspaceResource[]; + readonly deadline?: string; + readonly max_deadline?: string; + readonly status: WorkspaceStatus; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceBuildParameter { + readonly name: string; + readonly value: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceBuildsRequest extends Pagination { + readonly since?: string; +} + +// From codersdk/deployment.go +export interface WorkspaceConnectionLatencyMS { + readonly P50: number; + readonly P95: number; +} + +// From codersdk/deployment.go +export interface WorkspaceDeploymentStats { + readonly pending: number; + readonly building: number; + readonly running: number; + readonly failed: number; + readonly stopped: number; + readonly connection_latency_ms: WorkspaceConnectionLatencyMS; + readonly rx_bytes: number; + readonly tx_bytes: number; +} + +// From codersdk/workspaces.go +export interface WorkspaceFilter { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceHealth { + readonly healthy: boolean; + readonly failing_agents: readonly string[]; +} + +// From codersdk/workspaces.go +export interface WorkspaceOptions { + readonly include_deleted?: boolean; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxy extends Region { + readonly derp_enabled: boolean; + readonly derp_only: boolean; + readonly status?: WorkspaceProxyStatus; + readonly created_at: string; + readonly updated_at: string; + readonly deleted: boolean; + readonly version: string; +} + +// From codersdk/deployment.go +export interface WorkspaceProxyBuildInfo { + readonly workspace_proxy: boolean; + readonly dashboard_url: string; +} + +// From codersdk/workspaceproxy.go +export interface WorkspaceProxyStatus { + readonly status: ProxyHealthStatus; + readonly report?: ProxyHealthReport; + readonly checked_at: string; +} + +// From codersdk/workspaces.go +export interface WorkspaceQuota { + readonly credits_consumed: number; + readonly budget: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResource { + readonly id: string; + readonly created_at: string; + readonly job_id: string; + readonly workspace_transition: WorkspaceTransition; + readonly type: string; + readonly name: string; + readonly hide: boolean; + readonly icon: string; + readonly agents?: readonly WorkspaceAgent[]; + readonly metadata?: readonly WorkspaceResourceMetadata[]; + readonly daily_cost: number; +} + +// From codersdk/workspacebuilds.go +export interface WorkspaceResourceMetadata { + readonly key: string; + readonly value: string; + readonly sensitive: boolean; +} + +// From codersdk/workspaces.go +export interface WorkspacesRequest extends Pagination { + readonly q?: string; +} + +// From codersdk/workspaces.go +export interface WorkspacesResponse { + readonly workspaces: readonly Workspace[]; + readonly count: number; +} + +// From codersdk/apikey.go +export type APIKeyScope = 'all' | 'application_connect'; +export const APIKeyScopes: APIKeyScope[] = ['all', 'application_connect']; + +// From codersdk/workspaceagents.go +export type AgentSubsystem = 'envbox' | 'envbuilder' | 'exectrace'; +export const AgentSubsystems: AgentSubsystem[] = [ + 'envbox', + 'envbuilder', + 'exectrace', +]; + +// From codersdk/audit.go +export type AuditAction = + | 'create' + | 'delete' + | 'login' + | 'logout' + | 'register' + | 'start' + | 'stop' + | 'write'; +export const AuditActions: AuditAction[] = [ + 'create', + 'delete', + 'login', + 'logout', + 'register', + 'start', + 'stop', + 'write', +]; + +// From codersdk/workspaces.go +export type AutomaticUpdates = 'always' | 'never'; +export const AutomaticUpdateses: AutomaticUpdates[] = ['always', 'never']; + +// From codersdk/workspacebuilds.go +export type BuildReason = 'autostart' | 'autostop' | 'initiator'; +export const BuildReasons: BuildReason[] = [ + 'autostart', + 'autostop', + 'initiator', +]; + +// From codersdk/workspaceagents.go +export type DisplayApp = + | 'port_forwarding_helper' + | 'ssh_helper' + | 'vscode' + | 'vscode_insiders' + | 'web_terminal'; +export const DisplayApps: DisplayApp[] = [ + 'port_forwarding_helper', + 'ssh_helper', + 'vscode', + 'vscode_insiders', + 'web_terminal', +]; + +// From codersdk/externalauth.go +export type EnhancedExternalAuthProvider = + | 'azure-devops' + | 'azure-devops-entra' + | 'bitbucket-cloud' + | 'bitbucket-server' + | 'gitea' + | 'github' + | 'gitlab' + | 'jfrog' + | 'slack'; +export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = [ + 'azure-devops', + 'azure-devops-entra', + 'bitbucket-cloud', + 'bitbucket-server', + 'gitea', + 'github', + 'gitlab', + 'jfrog', + 'slack', +]; + +// From codersdk/deployment.go +export type Entitlement = 'entitled' | 'grace_period' | 'not_entitled'; +export const entitlements: Entitlement[] = [ + 'entitled', + 'grace_period', + 'not_entitled', +]; + +// From codersdk/deployment.go +export type Experiment = + | 'auto-fill-parameters' + | 'custom-roles' + | 'example' + | 'multi-organization'; +export const experiments: Experiment[] = [ + 'auto-fill-parameters', + 'custom-roles', + 'example', + 'multi-organization', +]; + +// From codersdk/deployment.go +export type FeatureName = + | 'access_control' + | 'advanced_template_scheduling' + | 'appearance' + | 'audit_log' + | 'browser_only' + | 'control_shared_ports' + | 'custom_roles' + | 'external_provisioner_daemons' + | 'external_token_encryption' + | 'high_availability' + | 'multiple_external_auth' + | 'scim' + | 'template_rbac' + | 'user_limit' + | 'user_role_management' + | 'workspace_batch_actions' + | 'workspace_proxy'; +export const FeatureNames: FeatureName[] = [ + 'access_control', + 'advanced_template_scheduling', + 'appearance', + 'audit_log', + 'browser_only', + 'control_shared_ports', + 'custom_roles', + 'external_provisioner_daemons', + 'external_token_encryption', + 'high_availability', + 'multiple_external_auth', + 'scim', + 'template_rbac', + 'user_limit', + 'user_role_management', + 'workspace_batch_actions', + 'workspace_proxy', +]; + +// From codersdk/groups.go +export type GroupSource = 'oidc' | 'user'; +export const GroupSources: GroupSource[] = ['oidc', 'user']; + +// From codersdk/insights.go +export type InsightsReportInterval = 'day' | 'week'; +export const InsightsReportIntervals: InsightsReportInterval[] = [ + 'day', + 'week', +]; + +// From codersdk/provisionerdaemons.go +export type JobErrorCode = 'REQUIRED_TEMPLATE_VARIABLES'; +export const JobErrorCodes: JobErrorCode[] = ['REQUIRED_TEMPLATE_VARIABLES']; + +// From codersdk/provisionerdaemons.go +export type LogLevel = 'debug' | 'error' | 'info' | 'trace' | 'warn'; +export const LogLevels: LogLevel[] = [ + 'debug', + 'error', + 'info', + 'trace', + 'warn', +]; + +// From codersdk/provisionerdaemons.go +export type LogSource = 'provisioner' | 'provisioner_daemon'; +export const LogSources: LogSource[] = ['provisioner', 'provisioner_daemon']; + +// From codersdk/apikey.go +export type LoginType = '' | 'github' | 'none' | 'oidc' | 'password' | 'token'; +export const LoginTypes: LoginType[] = [ + '', + 'github', + 'none', + 'oidc', + 'password', + 'token', +]; + +// From codersdk/oauth2.go +export type OAuth2ProviderGrantType = 'authorization_code' | 'refresh_token'; +export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = [ + 'authorization_code', + 'refresh_token', +]; + +// From codersdk/oauth2.go +export type OAuth2ProviderResponseType = 'code'; +export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = [ + 'code', +]; + +// From codersdk/deployment.go +export type PostgresAuth = 'awsiamrds' | 'password'; +export const PostgresAuths: PostgresAuth[] = ['awsiamrds', 'password']; + +// From codersdk/provisionerdaemons.go +export type ProvisionerJobStatus = + | 'canceled' + | 'canceling' + | 'failed' + | 'pending' + | 'running' + | 'succeeded' + | 'unknown'; +export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ + 'canceled', + 'canceling', + 'failed', + 'pending', + 'running', + 'succeeded', + 'unknown', +]; + +// From codersdk/workspaces.go +export type ProvisionerLogLevel = 'debug'; +export const ProvisionerLogLevels: ProvisionerLogLevel[] = ['debug']; + +// From codersdk/organizations.go +export type ProvisionerStorageMethod = 'file'; +export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ['file']; + +// From codersdk/organizations.go +export type ProvisionerType = 'echo' | 'terraform'; +export const ProvisionerTypes: ProvisionerType[] = ['echo', 'terraform']; + +// From codersdk/workspaceproxy.go +export type ProxyHealthStatus = + | 'ok' + | 'unhealthy' + | 'unreachable' + | 'unregistered'; +export const ProxyHealthStatuses: ProxyHealthStatus[] = [ + 'ok', + 'unhealthy', + 'unreachable', + 'unregistered', +]; + +// From codersdk/rbacresources_gen.go +export type RBACAction = + | 'application_connect' + | 'assign' + | 'create' + | 'delete' + | 'read' + | 'read_personal' + | 'ssh' + | 'start' + | 'stop' + | 'update' + | 'update_personal' + | 'use' + | 'view_insights'; +export const RBACActions: RBACAction[] = [ + 'application_connect', + 'assign', + 'create', + 'delete', + 'read', + 'read_personal', + 'ssh', + 'start', + 'stop', + 'update', + 'update_personal', + 'use', + 'view_insights', +]; + +// From codersdk/rbacresources_gen.go +export type RBACResource = + | '*' + | 'api_key' + | 'assign_org_role' + | 'assign_role' + | 'audit_log' + | 'debug_info' + | 'deployment_config' + | 'deployment_stats' + | 'file' + | 'group' + | 'license' + | 'oauth2_app' + | 'oauth2_app_code_token' + | 'oauth2_app_secret' + | 'organization' + | 'organization_member' + | 'provisioner_daemon' + | 'replicas' + | 'system' + | 'tailnet_coordinator' + | 'template' + | 'user' + | 'workspace' + | 'workspace_dormant' + | 'workspace_proxy'; +export const RBACResources: RBACResource[] = [ + '*', + 'api_key', + 'assign_org_role', + 'assign_role', + 'audit_log', + 'debug_info', + 'deployment_config', + 'deployment_stats', + 'file', + 'group', + 'license', + 'oauth2_app', + 'oauth2_app_code_token', + 'oauth2_app_secret', + 'organization', + 'organization_member', + 'provisioner_daemon', + 'replicas', + 'system', + 'tailnet_coordinator', + 'template', + 'user', + 'workspace', + 'workspace_dormant', + 'workspace_proxy', +]; + +// From codersdk/audit.go +export type ResourceType = + | 'api_key' + | 'convert_login' + | 'git_ssh_key' + | 'group' + | 'health_settings' + | 'license' + | 'oauth2_provider_app' + | 'oauth2_provider_app_secret' + | 'organization' + | 'template' + | 'template_version' + | 'user' + | 'workspace' + | 'workspace_build' + | 'workspace_proxy'; +export const ResourceTypes: ResourceType[] = [ + 'api_key', + 'convert_login', + 'git_ssh_key', + 'group', + 'health_settings', + 'license', + 'oauth2_provider_app', + 'oauth2_provider_app_secret', + 'organization', + 'template', + 'template_version', + 'user', + 'workspace', + 'workspace_build', + 'workspace_proxy', +]; + +// From codersdk/serversentevents.go +export type ServerSentEventType = 'data' | 'error' | 'ping'; +export const ServerSentEventTypes: ServerSentEventType[] = [ + 'data', + 'error', + 'ping', +]; + +// From codersdk/insights.go +export type TemplateAppsType = 'app' | 'builtin'; +export const TemplateAppsTypes: TemplateAppsType[] = ['app', 'builtin']; + +// From codersdk/insights.go +export type TemplateInsightsSection = 'interval_reports' | 'report'; +export const TemplateInsightsSections: TemplateInsightsSection[] = [ + 'interval_reports', + 'report', +]; + +// From codersdk/templates.go +export type TemplateRole = '' | 'admin' | 'use'; +export const TemplateRoles: TemplateRole[] = ['', 'admin', 'use']; + +// From codersdk/templateversions.go +export type TemplateVersionWarning = 'UNSUPPORTED_WORKSPACES'; +export const TemplateVersionWarnings: TemplateVersionWarning[] = [ + 'UNSUPPORTED_WORKSPACES', +]; + +// From codersdk/users.go +export type UserStatus = 'active' | 'dormant' | 'suspended'; +export const UserStatuses: UserStatus[] = ['active', 'dormant', 'suspended']; + +// From codersdk/templateversions.go +export type ValidationMonotonicOrder = 'decreasing' | 'increasing'; +export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [ + 'decreasing', + 'increasing', +]; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentLifecycle = + | 'created' + | 'off' + | 'ready' + | 'shutdown_error' + | 'shutdown_timeout' + | 'shutting_down' + | 'start_error' + | 'start_timeout' + | 'starting'; +export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ + 'created', + 'off', + 'ready', + 'shutdown_error', + 'shutdown_timeout', + 'shutting_down', + 'start_error', + 'start_timeout', + 'starting', +]; + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareLevel = 'authenticated' | 'owner' | 'public'; +export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = [ + 'authenticated', + 'owner', + 'public', +]; + +// From codersdk/workspaceagentportshare.go +export type WorkspaceAgentPortShareProtocol = 'http' | 'https'; +export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] = + ['http', 'https']; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStartupScriptBehavior = 'blocking' | 'non-blocking'; +export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = + ['blocking', 'non-blocking']; + +// From codersdk/workspaceagents.go +export type WorkspaceAgentStatus = + | 'connected' + | 'connecting' + | 'disconnected' + | 'timeout'; +export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [ + 'connected', + 'connecting', + 'disconnected', + 'timeout', +]; + +// From codersdk/workspaceapps.go +export type WorkspaceAppHealth = + | 'disabled' + | 'healthy' + | 'initializing' + | 'unhealthy'; +export const WorkspaceAppHealths: WorkspaceAppHealth[] = [ + 'disabled', + 'healthy', + 'initializing', + 'unhealthy', +]; + +// From codersdk/workspaceapps.go +export type WorkspaceAppSharingLevel = 'authenticated' | 'owner' | 'public'; +export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = [ + 'authenticated', + 'owner', + 'public', +]; + +// From codersdk/workspacebuilds.go +export type WorkspaceStatus = + | 'canceled' + | 'canceling' + | 'deleted' + | 'deleting' + | 'failed' + | 'pending' + | 'running' + | 'starting' + | 'stopped' + | 'stopping'; +export const WorkspaceStatuses: WorkspaceStatus[] = [ + 'canceled', + 'canceling', + 'deleted', + 'deleting', + 'failed', + 'pending', + 'running', + 'starting', + 'stopped', + 'stopping', +]; + +// From codersdk/workspacebuilds.go +export type WorkspaceTransition = 'delete' | 'start' | 'stop'; +export const WorkspaceTransitions: WorkspaceTransition[] = [ + 'delete', + 'start', + 'stop', +]; + +// From codersdk/workspaceproxy.go +export type RegionTypes = Region | WorkspaceProxy; + +// The code below is generated from codersdk/healthsdk. + +// From healthsdk/healthsdk.go +export interface AccessURLReport extends BaseReport { + readonly healthy: boolean; + readonly access_url: string; + readonly reachable: boolean; + readonly status_code: number; + readonly healthz_response: string; +} + +// From healthsdk/healthsdk.go +export interface BaseReport { + readonly error?: string; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly dismissed: boolean; +} + +// From healthsdk/healthsdk.go +export interface DERPHealthReport extends BaseReport { + readonly healthy: boolean; + readonly regions: Record; + // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly netcheck?: any; + readonly netcheck_err?: string; + readonly netcheck_logs: readonly string[]; +} + +// From healthsdk/healthsdk.go +export interface DERPNodeReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly error?: string; + // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node?: any; + // Named type "tailscale.com/derp.ServerInfoMessage" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly node_info: any; + readonly can_exchange_messages: boolean; + readonly round_trip_ping: string; + readonly round_trip_ping_ms: number; + readonly uses_websocket: boolean; + readonly client_logs: readonly (readonly string[])[]; + readonly client_errs: readonly (readonly string[])[]; + readonly stun: STUNReport; +} + +// From healthsdk/healthsdk.go +export interface DERPRegionReport { + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly warnings: readonly HealthMessage[]; + readonly error?: string; + // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type + readonly region?: any; + readonly node_reports: readonly DERPNodeReport[]; +} + +// From healthsdk/healthsdk.go +export interface DatabaseReport extends BaseReport { + readonly healthy: boolean; + readonly reachable: boolean; + readonly latency: string; + readonly latency_ms: number; + readonly threshold_ms: number; +} + +// From healthsdk/healthsdk.go +export interface HealthSettings { + readonly dismissed_healthchecks: readonly HealthSection[]; +} + +// From healthsdk/healthsdk.go +export interface HealthcheckReport { + readonly time: string; + readonly healthy: boolean; + readonly severity: HealthSeverity; + readonly failing_sections: readonly HealthSection[]; + readonly derp: DERPHealthReport; + readonly access_url: AccessURLReport; + readonly websocket: WebsocketReport; + readonly database: DatabaseReport; + readonly workspace_proxy: WorkspaceProxyReport; + readonly provisioner_daemons: ProvisionerDaemonsReport; + readonly coder_version: string; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReport extends BaseReport { + readonly items: readonly ProvisionerDaemonsReportItem[]; +} + +// From healthsdk/healthsdk.go +export interface ProvisionerDaemonsReportItem { + readonly provisioner_daemon: ProvisionerDaemon; + readonly warnings: readonly HealthMessage[]; +} + +// From healthsdk/healthsdk.go +export interface STUNReport { + readonly Enabled: boolean; + readonly CanSTUN: boolean; + readonly Error?: string; +} + +// From healthsdk/healthsdk.go +export interface UpdateHealthSettings { + readonly dismissed_healthchecks: readonly HealthSection[]; +} + +// From healthsdk/healthsdk.go +export interface WebsocketReport extends BaseReport { + readonly healthy: boolean; + readonly body: string; + readonly code: number; +} + +// From healthsdk/healthsdk.go +export interface WorkspaceProxyReport extends BaseReport { + readonly healthy: boolean; + readonly workspace_proxies: RegionsResponse; +} + +// From healthsdk/healthsdk.go +export type HealthSection = + | 'AccessURL' + | 'DERP' + | 'Database' + | 'ProvisionerDaemons' + | 'Websocket' + | 'WorkspaceProxy'; +export const HealthSections: HealthSection[] = [ + 'AccessURL', + 'DERP', + 'Database', + 'ProvisionerDaemons', + 'Websocket', + 'WorkspaceProxy', +]; + +// The code below is generated from coderd/healthcheck/health. + +// From health/model.go +export interface HealthMessage { + readonly code: HealthCode; + readonly message: string; +} + +// From health/model.go +export type HealthCode = + | 'EACS01' + | 'EACS02' + | 'EACS03' + | 'EACS04' + | 'EDB01' + | 'EDB02' + | 'EDERP01' + | 'EDERP02' + | 'EPD01' + | 'EPD02' + | 'EPD03' + | 'EUNKNOWN' + | 'EWP01' + | 'EWP02' + | 'EWP04' + | 'EWS01' + | 'EWS02' + | 'EWS03'; +export const HealthCodes: HealthCode[] = [ + 'EACS01', + 'EACS02', + 'EACS03', + 'EACS04', + 'EDB01', + 'EDB02', + 'EDERP01', + 'EDERP02', + 'EPD01', + 'EPD02', + 'EPD03', + 'EUNKNOWN', + 'EWP01', + 'EWP02', + 'EWP04', + 'EWS01', + 'EWS02', + 'EWS03', +]; + +// From health/model.go +export type HealthSeverity = 'error' | 'ok' | 'warning'; +export const HealthSeveritys: HealthSeverity[] = ['error', 'ok', 'warning']; + +// The code below is generated from github.com/coder/serpent. + +// From serpent/serpent.go +export type SerpentAnnotations = Record; + +// From serpent/serpent.go +export interface SerpentGroup { + readonly parent?: SerpentGroup; + readonly name?: string; + readonly yaml?: string; + readonly description?: string; +} + +// From serpent/option.go +export interface SerpentOption { + readonly name?: string; + readonly description?: string; + readonly required?: boolean; + readonly flag?: string; + readonly flag_shorthand?: string; + readonly env?: string; + readonly yaml?: string; + readonly default?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Golang interface, unable to resolve type. + readonly value?: any; + readonly annotations?: SerpentAnnotations; + readonly group?: SerpentGroup; + readonly use_instead?: readonly SerpentOption[]; + readonly hidden?: boolean; + readonly value_source?: SerpentValueSource; +} + +// From serpent/option.go +export type SerpentOptionSet = readonly SerpentOption[]; + +// From serpent/option.go +export type SerpentValueSource = '' | 'default' | 'env' | 'flag' | 'yaml'; +export const SerpentValueSources: SerpentValueSource[] = [ + '', + 'default', + 'env', + 'flag', + 'yaml', +]; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts new file mode 100644 index 00000000..df99ae32 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -0,0 +1,8 @@ +/** + * Right now the file is doing barrel exports. But if something more + * sophisticated is needed down the line, those changes should be handled in + * this file, to provide some degree of insulation between the vendored files + * and the rest of the plugin logic. + */ +export * from './api/api'; +export type * from './api/typesGenerated'; diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts new file mode 100644 index 00000000..b915a7fb --- /dev/null +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/utils/delay.ts @@ -0,0 +1,4 @@ +export const delay = (ms: number): Promise => + new Promise(res => { + setTimeout(res, ms); + }); diff --git a/yarn.lock b/yarn.lock index e7553d7d..c287f84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8919,6 +8919,11 @@ dependencies: "@types/node" "*" +"@types/ua-parser-js@^0.7.39": + version "0.7.39" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz#832c58e460c9435e4e34bb866e85e9146e12cdbb" + integrity sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg== + "@types/unist@^2", "@types/unist@^2.0.0": version "2.0.10" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" @@ -11859,6 +11864,11 @@ dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.11.11: + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== + debounce@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -20248,14 +20258,6 @@ react-dom@^18.0.2: loose-envify "^1.1.0" scheduler "^0.23.0" -react-dom@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - react-double-scrollbar@0.0.15: version "0.0.15" resolved "https://registry.yarnpkg.com/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4" @@ -21188,13 +21190,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -22953,6 +22948,11 @@ typescript@~5.2.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +ua-parser-js@^1.0.37: + version "1.0.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" + integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" From 9c958d37009efda139f55772601d12177ede1c4f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 16:05:22 +0000 Subject: [PATCH 02/22] chore: update CoderClient class to use new SDK --- .../src/api/CoderClient.ts | 80 +++++++------------ .../src/api/vendoredSdk/index.ts | 8 +- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 7c09f72c..ecf1d67e 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -1,19 +1,18 @@ -import globalAxios, { +import { AxiosError, - type AxiosInstance, type InternalAxiosRequestConfig as RequestConfig, } from 'axios'; import { type IdentityApi, createApiRef } from '@backstage/core-plugin-api'; -import { - type Workspace, - CODER_API_REF_ID_PREFIX, - WorkspacesRequest, - WorkspacesResponse, - User, -} from '../typesConstants'; +import { CODER_API_REF_ID_PREFIX } from '../typesConstants'; import type { UrlSync } from './UrlSync'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; -import { CoderSdk } from './MockCoderSdk'; +import { + type User, + type Workspace, + type WorkspacesRequest, + type WorkspacesResponse, + CoderSdk, +} from './vendoredSdk'; export const CODER_AUTH_HEADER_KEY = 'Coder-Session-Token'; const DEFAULT_REQUEST_TIMEOUT_MS = 20_000; @@ -39,11 +38,6 @@ type CoderClientApi = Readonly<{ * Return value indicates whether the token is valid. */ syncToken: (newToken: string) => Promise; - - /** - * Cleans up a client instance, removing its links to all external systems. - */ - cleanupClient: () => void; }>; const sharedCleanupAbortReason = new DOMException( @@ -59,19 +53,30 @@ export const disabledClientError = new Error( ); type ConstructorInputs = Readonly<{ + /** + * initialToken is strictly for testing, and is basically limited to making it + * easier to test API logic. + * + * If trying to test UI logic that depends on CoderClient, it's probably + * better to interact with CoderClient indirectly through the auth components, + * so that React state is aware of everything. + */ initialToken?: string; - requestTimeoutMs?: number; + requestTimeoutMs?: number; apis: Readonly<{ urlSync: UrlSync; identityApi: IdentityApi; }>; }>; +type RequestInterceptor = ( + config: RequestConfig, +) => RequestConfig | Promise; + export class CoderClient implements CoderClientApi { private readonly urlSync: UrlSync; private readonly identityApi: IdentityApi; - private readonly axios: AxiosInstance; private readonly requestTimeoutMs: number; private readonly cleanupController: AbortController; @@ -82,33 +87,28 @@ export class CoderClient implements CoderClientApi { constructor(inputs: ConstructorInputs) { const { - apis, initialToken, + apis: { urlSync, identityApi }, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, } = inputs; - const { urlSync, identityApi } = apis; this.urlSync = urlSync; this.identityApi = identityApi; - this.axios = globalAxios.create(); - this.loadedSessionToken = initialToken; this.requestTimeoutMs = requestTimeoutMs; - this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.getBackstageCoderSdk(this.axios); + this.sdk = this.getBackstageCoderSdk(); this.addBaseRequestInterceptors(); } private addRequestInterceptor( - requestInterceptor: ( - config: RequestConfig, - ) => RequestConfig | Promise, + requestInterceptor: RequestInterceptor, errorInterceptor?: (error: unknown) => unknown, ): number { - const ejectionId = this.axios.interceptors.request.use( + const axios = this.sdk.getAxiosInstance(); + const ejectionId = axios.interceptors.request.use( requestInterceptor, errorInterceptor, ); @@ -120,7 +120,8 @@ export class CoderClient implements CoderClientApi { private removeRequestInterceptorById(ejectionId: number): boolean { // Even if we somehow pass in an ID that hasn't been associated with the // Axios instance, that's a noop. No harm in calling method no matter what - this.axios.interceptors.request.eject(ejectionId); + const axios = this.sdk.getAxiosInstance(); + axios.interceptors.request.eject(ejectionId); if (!this.trackedEjectionIds.has(ejectionId)) { return false; @@ -179,10 +180,8 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private getBackstageCoderSdk( - axiosInstance: AxiosInstance, - ): BackstageCoderSdk { - const baseSdk = new CoderSdk(axiosInstance); + private getBackstageCoderSdk(): BackstageCoderSdk { + const baseSdk = new CoderSdk(); const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { const workspacesRes = await baseSdk.getWorkspaces(request); @@ -335,23 +334,6 @@ export class CoderClient implements CoderClientApi { this.removeRequestInterceptorById(validationId); } }; - - cleanupClient = (): void => { - this.trackedEjectionIds.forEach(id => { - this.axios.interceptors.request.eject(id); - }); - - this.trackedEjectionIds.clear(); - this.cleanupController.abort(sharedCleanupAbortReason); - this.loadedSessionToken = undefined; - - // Not using this.addRequestInterceptor, because we don't want to track this - // interceptor at all. It should never be ejected once the client has been - // disabled - this.axios.interceptors.request.use(() => { - throw disabledClientError; - }); - }; } function appendParamToQuery( diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index df99ae32..4f70e40c 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -4,5 +4,11 @@ * this file, to provide some degree of insulation between the vendored files * and the rest of the plugin logic. */ -export * from './api/api'; export type * from './api/typesGenerated'; +export { + type DeleteWorkspaceOptions, + type GetLicensesResponse, + type InsightsParams, + type InsightsTemplateParams, + Api as CoderSdk, +} from './api/api'; From 4979067d3a7e1707c60cfcba9c76a3ae236e1793 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 16:09:07 +0000 Subject: [PATCH 03/22] chore: delete mock SDK --- .../src/api/MockCoderSdk.ts | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts diff --git a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts b/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts deleted file mode 100644 index 3100242b..00000000 --- a/plugins/backstage-plugin-coder/src/api/MockCoderSdk.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file This is a temporary (and significantly limited) implementation of the - * "Coder SDK" that will eventually be imported from Coder core - * - * @todo Replace this with a full, proper implementation, and then expose it to - * plugin users. - */ -import globalAxios, { type AxiosInstance } from 'axios'; -import { - type User, - type WorkspacesRequest, - type WorkspacesResponse, -} from '../typesConstants'; - -type CoderSdkApi = { - getAuthenticatedUser: () => Promise; - getWorkspaces: (request: WorkspacesRequest) => Promise; -}; - -export class CoderSdk implements CoderSdkApi { - private readonly axios: AxiosInstance; - - constructor(axiosInstance?: AxiosInstance) { - this.axios = axiosInstance ?? globalAxios.create(); - } - - getWorkspaces = async ( - request: WorkspacesRequest, - ): Promise => { - const urlParams = new URLSearchParams({ - q: request.q ?? '', - limit: String(request.limit || 0), - after_id: request.after_id ?? '', - offset: String(request.offset || 0), - }); - - const response = await this.axios.get( - `/workspaces?${urlParams.toString()}`, - ); - - return response.data; - }; - - getAuthenticatedUser = async (): Promise => { - const response = await this.axios.get('/users/me'); - return response.data; - }; -} From 5e7e01f464ad29ac750fb936b05a6c04da16b4b0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 17:57:39 +0000 Subject: [PATCH 04/22] fix: improve data hiding for CoderSdk --- .../src/api/vendoredSdk/index.ts | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index df99ae32..bcacd351 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -1,8 +1,31 @@ -/** - * Right now the file is doing barrel exports. But if something more - * sophisticated is needed down the line, those changes should be handled in - * this file, to provide some degree of insulation between the vendored files - * and the rest of the plugin logic. - */ -export * from './api/api'; export type * from './api/typesGenerated'; +export type { + DeleteWorkspaceOptions, + GetLicensesResponse, + InsightsParams, + InsightsTemplateParams, +} from './api/api'; +import { Api } from './api/api'; + +// Union of all API properties that won't ever be relevant to Backstage users. +// Not a huge deal that they still exist at runtime; mainly concerned about +// whether they pollute Intellisense when someone is using the SDK. Most of +// these properties don't deal with APIs and are mainly helpers in Core +type PropertyToHide = + | 'getJFrogXRayScan' + | 'getCsrfToken' + | 'setSessionToken' + | 'setHost' + | 'getAvailableExperiments' + | 'login' + | 'logout'; + +// Wanted to have a CoderSdk class (mainly re-exporting the Api class as itself +// with the extra properties omitted). But because classes are wonky and exist +// as both runtime values and times, it didn't seem possible, even with things +// like class declarations. Making a new function is good enough for now, though +export type CoderSdk = Omit; +export function makeCoderSdk(): CoderSdk { + const api = new Api(); + return api as CoderSdk; +} From 937f6f51572dbbca45b9172e92159b8b976a081a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 17:59:45 +0000 Subject: [PATCH 05/22] docs: update typo --- plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index bcacd351..b64f8419 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -22,7 +22,7 @@ type PropertyToHide = // Wanted to have a CoderSdk class (mainly re-exporting the Api class as itself // with the extra properties omitted). But because classes are wonky and exist -// as both runtime values and times, it didn't seem possible, even with things +// as both runtime values and types, it didn't seem possible, even with things // like class declarations. Making a new function is good enough for now, though export type CoderSdk = Omit; export function makeCoderSdk(): CoderSdk { From 294572d858e4ea05e1e70cdfd0ca4625f77f72f0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 18:54:19 +0000 Subject: [PATCH 06/22] wip: commit progress on updating Coder client --- .../src/api/queryOptions.ts | 2 +- .../src/testHelpers/coderEntities.ts | 3440 +++++++++++++++++ .../src/testHelpers/mockCoderAppData.ts | 12 +- .../src/testHelpers/server.ts | 13 +- .../src/typesConstants.ts | 17 - 5 files changed, 3455 insertions(+), 29 deletions(-) create mode 100644 plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index b10ecfe2..d15d6ce3 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -1,5 +1,5 @@ import type { UseQueryOptions } from '@tanstack/react-query'; -import type { Workspace, WorkspacesRequest } from '../typesConstants'; +import type { Workspace, WorkspacesRequest } from './vendoredSdk'; import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import type { BackstageCoderSdk } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts new file mode 100644 index 00000000..868daaaf --- /dev/null +++ b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts @@ -0,0 +1,3440 @@ +/** + * @file This is a subset of the mock data from the Coder OSS repo. No values + * are modified; if any values should be for Backstage, those should be updated + * in the mockCoderPluginData.ts file. + * + * @see {@link https://github.com/coder/coder/blob/main/site/src/testHelpers/entities.ts} + */ +import type * as TypesGen from '../api/vendoredSdk'; + +export const MockOrganization: TypesGen.Organization = { + id: 'fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0', + name: 'Test Organization', + created_at: '', + updated_at: '', + is_default: true, +}; + +export const MockOwnerRole: TypesGen.Role = { + name: 'owner', + display_name: 'Owner', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +export const MockUser: TypesGen.User = { + id: 'test-user', + username: 'TestUser', + email: 'test@coder.com', + created_at: '', + status: 'active', + organization_ids: [MockOrganization.id], + roles: [MockOwnerRole], + avatar_url: 'https://avatars.githubusercontent.com/u/95932066?s=200&v=4', + last_seen_at: '', + login_type: 'password', + theme_preference: '', + name: '', +}; + +export const MockTemplateDAUResponse: TypesGen.DAUsResponse = { + tz_hour_offset: 0, + entries: [ + { date: '2022-08-27', amount: 1 }, + { date: '2022-08-29', amount: 2 }, + { date: '2022-08-30', amount: 1 }, + ], +}; +export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = { + tz_hour_offset: 0, + entries: [ + { date: '2022-08-27', amount: 10 }, + { date: '2022-08-29', amount: 22 }, + { date: '2022-08-30', amount: 14 }, + ], +}; +export const MockSessionToken: TypesGen.LoginWithPasswordResponse = { + session_token: 'my-session-token', +}; + +export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = { + key: 'my-api-key', +}; + +export const MockToken: TypesGen.APIKeyWithOwner = { + id: 'tBoVE3dqLl', + user_id: 'f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b', + last_used: '0001-01-01T00:00:00Z', + expires_at: '2023-01-15T20:10:45.637438Z', + created_at: '2022-12-16T20:10:45.637452Z', + updated_at: '2022-12-16T20:10:45.637452Z', + login_type: 'token', + scope: 'all', + lifetime_seconds: 2592000, + token_name: 'token-one', + username: 'admin', +}; + +export const MockTokens: TypesGen.APIKeyWithOwner[] = [ + MockToken, + { + id: 'tBoVE3dqLl', + user_id: 'f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b', + last_used: '0001-01-01T00:00:00Z', + expires_at: '2023-01-15T20:10:45.637438Z', + created_at: '2022-12-16T20:10:45.637452Z', + updated_at: '2022-12-16T20:10:45.637452Z', + login_type: 'token', + scope: 'all', + lifetime_seconds: 2592000, + token_name: 'token-two', + username: 'admin', + }, +]; + +export const MockPrimaryWorkspaceProxy: TypesGen.WorkspaceProxy = { + id: '4aa23000-526a-481f-a007-0f20b98b1e12', + name: 'primary', + display_name: 'Default', + icon_url: '/emojis/1f60e.png', + healthy: true, + path_app_url: 'https://coder.com', + wildcard_hostname: '*.coder.com', + derp_enabled: true, + derp_only: false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + version: 'v2.34.5-test+primary', + deleted: false, + status: { + status: 'ok', + checked_at: new Date().toISOString(), + }, +}; + +export const MockHealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { + id: '5e2c1ab7-479b-41a9-92ce-aa85625de52c', + name: 'haswildcard', + display_name: 'Subdomain Supported', + icon_url: '/emojis/1f319.png', + healthy: true, + path_app_url: 'https://external.com', + wildcard_hostname: '*.external.com', + derp_enabled: true, + derp_only: false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + deleted: false, + version: 'v2.34.5-test+haswildcard', + status: { + status: 'ok', + checked_at: new Date().toISOString(), + }, +}; + +export const MockUnhealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { + id: '8444931c-0247-4171-842a-569d9f9cbadb', + name: 'unhealthy', + display_name: 'Unhealthy', + icon_url: '/emojis/1f92e.png', + healthy: false, + path_app_url: 'https://unhealthy.coder.com', + wildcard_hostname: '*unhealthy..coder.com', + derp_enabled: true, + derp_only: true, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + version: 'v2.34.5-test+unhealthy', + deleted: false, + status: { + status: 'unhealthy', + report: { + errors: ['This workspace proxy is manually marked as unhealthy.'], + warnings: ['This is a manual warning for this workspace proxy.'], + }, + checked_at: new Date().toISOString(), + }, +}; + +export const MockWorkspaceProxies: TypesGen.WorkspaceProxy[] = [ + MockPrimaryWorkspaceProxy, + MockHealthyWildWorkspaceProxy, + MockUnhealthyWildWorkspaceProxy, + { + id: '26e84c16-db24-4636-a62d-aa1a4232b858', + name: 'nowildcard', + display_name: 'No wildcard', + icon_url: '/emojis/1f920.png', + healthy: true, + path_app_url: 'https://cowboy.coder.com', + wildcard_hostname: '', + derp_enabled: false, + derp_only: false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + deleted: false, + version: 'v2.34.5-test+nowildcard', + status: { + status: 'ok', + checked_at: new Date().toISOString(), + }, + }, +]; + +export const MockProxyLatencies: Record = { + ...MockWorkspaceProxies.reduce((acc, proxy) => { + if (!proxy.healthy) { + return acc; + } + acc[proxy.id] = { + // Make one of them inaccurate. + accurate: proxy.id !== '26e84c16-db24-4636-a62d-aa1a4232b858', + // This is a deterministic way to generate a latency to for each proxy. + // It will be the same for each run as long as the IDs don't change. + latencyMS: + (Number( + Array.from(proxy.id).reduce( + // Multiply each char code by some large prime number to increase the + // size of the number and allow use to get some decimal points. + (acc, char) => acc + char.charCodeAt(0) * 37, + 0, + ), + ) / + // Cap at 250ms + 100) % + 250, + at: new Date(), + }; + return acc; + }, {} as Record), +}; + +export const MockBuildInfo: TypesGen.BuildInfoResponse = { + agent_api_version: '1.0', + external_url: 'file:///mock-url', + version: 'v99.999.9999+c9cdf14', + dashboard_url: 'https:///mock-url', + workspace_proxy: false, + upgrade_message: 'My custom upgrade message', + deployment_id: '510d407f-e521-4180-b559-eab4a6d802b8', +}; + +export const MockSupportLinks: TypesGen.LinkConfig[] = [ + { + name: 'First link', + target: 'http://first-link', + icon: 'chat', + }, + { + name: 'Second link', + target: 'http://second-link', + icon: 'docs', + }, + { + name: 'Third link', + target: + 'https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}', + icon: '', + }, +]; + +export const MockUpdateCheck: TypesGen.UpdateCheckResponse = { + current: true, + url: 'file:///mock-url', + version: 'v99.999.9999+c9cdf14', +}; + +export const MockUserAdminRole: TypesGen.Role = { + name: 'user_admin', + display_name: 'User Admin', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +export const MockTemplateAdminRole: TypesGen.Role = { + name: 'template_admin', + display_name: 'Template Admin', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +export const MockMemberRole: TypesGen.SlimRole = { + name: 'member', + display_name: 'Member', +}; + +export const MockAuditorRole: TypesGen.Role = { + name: 'auditor', + display_name: 'Auditor', + site_permissions: [], + organization_permissions: {}, + user_permissions: [], + organization_id: '', +}; + +// assignableRole takes a role and a boolean. The boolean implies if the +// actor can assign (add/remove) the role from other users. +export function assignableRole( + role: TypesGen.Role, + assignable: boolean, +): TypesGen.AssignableRoles { + return { + ...role, + assignable: assignable, + built_in: true, + }; +} + +export const MockSiteRoles = [MockUserAdminRole, MockAuditorRole]; +export const MockAssignableSiteRoles = [ + assignableRole(MockUserAdminRole, true), + assignableRole(MockAuditorRole, true), +]; + +export const MockMemberPermissions = { + viewAuditLog: false, +}; + +export const MockUserAdmin: TypesGen.User = { + id: 'test-user', + username: 'TestUser', + email: 'test@coder.com', + created_at: '', + status: 'active', + organization_ids: [MockOrganization.id], + roles: [MockUserAdminRole], + avatar_url: '', + last_seen_at: '', + login_type: 'password', + theme_preference: '', + name: '', +}; + +export const MockUser2: TypesGen.User = { + id: 'test-user-2', + username: 'TestUser2', + email: 'test2@coder.com', + created_at: '', + status: 'active', + organization_ids: [MockOrganization.id], + roles: [], + avatar_url: '', + last_seen_at: '2022-09-14T19:12:21Z', + login_type: 'oidc', + theme_preference: '', + name: 'Mock User The Second', +}; + +export const SuspendedMockUser: TypesGen.User = { + id: 'suspended-mock-user', + username: 'SuspendedMockUser', + email: 'iamsuspendedsad!@coder.com', + created_at: '', + status: 'suspended', + organization_ids: [MockOrganization.id], + roles: [], + avatar_url: '', + last_seen_at: '', + login_type: 'password', + theme_preference: '', + name: '', +}; + +export const MockProvisioner: TypesGen.ProvisionerDaemon = { + created_at: '2022-05-17T17:39:01.382927298Z', + id: 'test-provisioner', + name: 'Test Provisioner', + provisioners: ['echo'], + tags: { scope: 'organization' }, + version: 'v2.34.5', + api_version: '1.0', +}; + +export const MockUserProvisioner: TypesGen.ProvisionerDaemon = { + created_at: '2022-05-17T17:39:01.382927298Z', + id: 'test-user-provisioner', + name: 'Test User Provisioner', + provisioners: ['echo'], + tags: { scope: 'user', owner: '12345678-abcd-1234-abcd-1234567890abcd' }, + version: 'v2.34.5', + api_version: '1.0', +}; + +export const MockProvisionerJob: TypesGen.ProvisionerJob = { + created_at: '', + id: 'test-provisioner-job', + status: 'succeeded', + file_id: MockOrganization.id, + completed_at: '2022-05-17T17:39:01.382927298Z', + tags: { + scope: 'organization', + owner: '', + wowzers: 'whatatag', + isCapable: 'false', + department: 'engineering', + dreaming: 'true', + }, + queue_position: 0, + queue_size: 0, +}; + +export const MockFailedProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: 'failed', +}; + +export const MockCancelingProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: 'canceling', +}; +export const MockCanceledProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: 'canceled', +}; +export const MockRunningProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: 'running', +}; +export const MockPendingProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: 'pending', + queue_position: 2, + queue_size: 4, +}; +export const MockTemplateVersion: TypesGen.TemplateVersion = { + id: 'test-template-version', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-17T17:39:01.382927298Z', + template_id: 'test-template', + job: MockProvisionerJob, + name: 'test-version', + message: 'first version', + readme: `--- +name:Template test +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, + archived: false, +}; + +export const MockTemplateVersion2: TypesGen.TemplateVersion = { + id: 'test-template-version-2', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-17T17:39:01.382927298Z', + template_id: 'test-template', + job: MockProvisionerJob, + name: 'test-version-2', + message: 'first version', + readme: `--- +name:Template test 2 +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, + archived: false, +}; + +export const MockTemplateVersionWithMarkdownMessage: TypesGen.TemplateVersion = + { + ...MockTemplateVersion, + message: ` +# Abiding Grace +## Enchantment +At the beginning of your end step, choose one — + +- You gain 1 life. + +- Return target creature card with mana value 1 from your graveyard to the battlefield. +`, + }; + +export const MockTemplate: TypesGen.Template = { + id: 'test-template', + created_at: '2022-05-17T17:39:01.382927298Z', + updated_at: '2022-05-18T17:39:01.382927298Z', + organization_id: MockOrganization.id, + name: 'test-template', + display_name: 'Test Template', + provisioner: MockProvisioner.provisioners[0], + active_version_id: MockTemplateVersion.id, + active_user_count: 1, + build_time_stats: { + start: { + P50: 1000, + P95: 1500, + }, + stop: { + P50: 1000, + P95: 1500, + }, + delete: { + P50: 1000, + P95: 1500, + }, + }, + description: 'This is a test description.', + default_ttl_ms: 24 * 60 * 60 * 1000, + activity_bump_ms: 1 * 60 * 60 * 1000, + autostop_requirement: { + days_of_week: ['sunday'], + weeks: 1, + }, + autostart_requirement: { + days_of_week: [ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ], + }, + created_by_id: 'test-creator-id', + created_by_name: 'test_creator', + icon: '/icon/code.svg', + allow_user_cancel_workspace_jobs: true, + failure_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, + allow_user_autostart: true, + allow_user_autostop: true, + require_active_version: false, + deprecated: false, + deprecation_message: '', + max_port_share_level: 'public', +}; + +export const MockTemplateVersionFiles: TemplateVersionFiles = { + 'README.md': '# Example\n\nThis is an example template.', + 'main.tf': `// Provides info about the workspace. +data "coder_workspace" "me" {} + +// Provides the startup script used to download +// the agent and communicate with Coder. +resource "coder_agent" "dev" { +os = "linux" +arch = "amd64" +} + +resource "kubernetes_pod" "main" { +// Ensures that the Pod dies when the workspace shuts down! +count = data.coder_workspace.me.start_count +metadata { + name = "dev-\${data.coder_workspace.me.id}" +} +spec { + container { + image = "ubuntu" + command = ["sh", "-c", coder_agent.main.init_script] + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + } +} +} +`, +}; + +export const MockWorkspaceApp: TypesGen.WorkspaceApp = { + id: 'test-app', + slug: 'test-app', + display_name: 'Test App', + icon: '', + subdomain: false, + health: 'disabled', + external: false, + url: '', + sharing_level: 'owner', + healthcheck: { + url: '', + interval: 0, + threshold: 0, + }, +}; + +export const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { + created_at: '2023-05-04T11:30:41.402072Z', + id: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', + display_name: 'Startup Script', + icon: '', + workspace_agent_id: '', +}; + +export const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = { + log_source_id: MockWorkspaceAgentLogSource.id, + cron: '', + log_path: '', + run_on_start: true, + run_on_stop: false, + script: "echo 'hello world'", + start_blocks_login: false, + timeout: 0, +}; + +export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { + apps: [MockWorkspaceApp], + architecture: 'amd64', + created_at: '', + environment_variables: {}, + id: 'test-workspace-agent', + name: 'a-workspace-agent', + operating_system: 'linux', + resource_id: '', + status: 'connected', + updated_at: '', + version: MockBuildInfo.version, + api_version: '1.0', + latency: { + 'Coder Embedded DERP': { + latency_ms: 32.55, + preferred: true, + }, + }, + connection_timeout_seconds: 120, + troubleshooting_url: 'https://coder.com/troubleshoot', + lifecycle_state: 'starting', + logs_length: 0, + logs_overflowed: false, + log_sources: [MockWorkspaceAgentLogSource], + scripts: [MockWorkspaceAgentScript], + startup_script_behavior: 'non-blocking', + subsystems: ['envbox', 'exectrace'], + health: { + healthy: true, + }, + display_apps: [ + 'ssh_helper', + 'port_forwarding_helper', + 'vscode', + 'vscode_insiders', + 'web_terminal', + ], +}; + +export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-2', + name: 'another-workspace-agent', + status: 'disconnected', + version: '', + latency: {}, + lifecycle_state: 'ready', + health: { + healthy: false, + reason: 'agent is not connected', + }, +}; + +export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-3', + name: 'an-outdated-workspace-agent', + version: 'v99.999.9998+abcdef', + operating_system: 'Windows', + latency: { + ...MockWorkspaceAgent.latency, + Chicago: { + preferred: false, + latency_ms: 95.11, + }, + 'San Francisco': { + preferred: false, + latency_ms: 111.55, + }, + Paris: { + preferred: false, + latency_ms: 221.66, + }, + }, + lifecycle_state: 'ready', +}; + +export const MockWorkspaceAgentDeprecated: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-3', + name: 'an-outdated-workspace-agent', + version: 'v99.999.9998+abcdef', + api_version: '1.99', + operating_system: 'Windows', + latency: { + ...MockWorkspaceAgent.latency, + Chicago: { + preferred: false, + latency_ms: 95.11, + }, + 'San Francisco': { + preferred: false, + latency_ms: 111.55, + }, + Paris: { + preferred: false, + latency_ms: 221.66, + }, + }, + lifecycle_state: 'ready', +}; + +export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-connecting', + name: 'another-workspace-agent', + status: 'connecting', + version: '', + latency: {}, + lifecycle_state: 'created', +}; + +export const MockWorkspaceAgentTimeout: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-timeout', + name: 'a-timed-out-workspace-agent', + status: 'timeout', + version: '', + latency: {}, + lifecycle_state: 'created', + health: { + healthy: false, + reason: 'agent is taking too long to connect', + }, +}; + +export const MockWorkspaceAgentStarting: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-starting', + name: 'a-starting-workspace-agent', + lifecycle_state: 'starting', +}; + +export const MockWorkspaceAgentReady: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-ready', + name: 'a-ready-workspace-agent', + lifecycle_state: 'ready', +}; + +export const MockWorkspaceAgentStartTimeout: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-start-timeout', + name: 'a-workspace-agent-timed-out-while-running-startup-script', + lifecycle_state: 'start_timeout', +}; + +export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-start-error', + name: 'a-workspace-agent-errored-while-running-startup-script', + lifecycle_state: 'start_error', + health: { + healthy: false, + reason: 'agent startup script failed', + }, +}; + +export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-shutting-down', + name: 'a-shutting-down-workspace-agent', + lifecycle_state: 'shutting_down', + health: { + healthy: false, + reason: 'agent is shutting down', + }, +}; + +export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-shutdown-timeout', + name: 'a-workspace-agent-timed-out-while-running-shutdownup-script', + lifecycle_state: 'shutdown_timeout', + health: { + healthy: false, + reason: 'agent is shutting down', + }, +}; + +export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-shutdown-error', + name: 'a-workspace-agent-errored-while-running-shutdownup-script', + lifecycle_state: 'shutdown_error', + health: { + healthy: false, + reason: 'agent is shutting down', + }, +}; + +export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-off', + name: 'a-workspace-agent-is-shut-down', + lifecycle_state: 'off', + health: { + healthy: false, + reason: 'agent is shutting down', + }, +}; + +export const MockWorkspaceResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-resource', + name: 'a-workspace-resource', + agents: [MockWorkspaceAgent], + created_at: '', + job_id: '', + type: 'google_compute_disk', + workspace_transition: 'start', + hide: false, + icon: '', + metadata: [{ key: 'size', value: '32GB', sensitive: false }], + daily_cost: 10, +}; + +export const MockWorkspaceResourceSensitive: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: 'test-workspace-resource-sensitive', + name: 'workspace-resource-sensitive', + metadata: [{ key: 'api_key', value: '12345678', sensitive: true }], +}; + +export const MockWorkspaceResourceMultipleAgents: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: 'test-workspace-resource-multiple-agents', + name: 'workspace-resource-multiple-agents', + agents: [ + MockWorkspaceAgent, + MockWorkspaceAgentDisconnected, + MockWorkspaceAgentOutdated, + ], +}; + +export const MockWorkspaceResourceHidden: TypesGen.WorkspaceResource = { + ...MockWorkspaceResource, + id: 'test-workspace-resource-hidden', + name: 'workspace-resource-hidden', + hide: true, +}; + +export const MockWorkspaceVolumeResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-volume-resource', + created_at: '', + job_id: '', + workspace_transition: 'start', + type: 'docker_volume', + name: 'home_volume', + hide: false, + icon: '', + daily_cost: 0, +}; + +export const MockWorkspaceImageResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-image-resource', + created_at: '', + job_id: '', + workspace_transition: 'start', + type: 'docker_image', + name: 'main', + hide: false, + icon: '', + daily_cost: 0, +}; + +export const MockWorkspaceContainerResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-container-resource', + created_at: '', + job_id: '', + workspace_transition: 'start', + type: 'docker_container', + name: 'workspace', + hide: false, + icon: '', + daily_cost: 0, +}; + +export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = + { + schedule: '', + }; + +export const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = + { + // Runs at 9:30am Monday through Friday using Canada/Eastern + // (America/Toronto) time + schedule: 'CRON_TZ=Canada/Eastern 30 9 * * 1-5', + }; + +export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: 'start', + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'initiator', + resources: [MockWorkspaceResource], + status: 'running', + daily_cost: 20, +}; + +export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: 'start', + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'autostart', + resources: [MockWorkspaceResource], + status: 'running', + daily_cost: 20, +}; + +export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: 'start', + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'autostop', + resources: [MockWorkspaceResource], + status: 'running', + daily_cost: 20, +}; + +export const MockFailedWorkspaceBuild = ( + transition: TypesGen.WorkspaceTransition = 'start', +): TypesGen.WorkspaceBuild => ({ + build_number: 1, + created_at: '2022-05-17T17:39:01.382927298Z', + id: '1', + initiator_id: MockUser.id, + initiator_name: MockUser.username, + job: MockFailedProvisionerJob, + template_version_id: MockTemplateVersion.id, + template_version_name: MockTemplateVersion.name, + transition: transition, + updated_at: '2022-05-17T17:39:01.382927298Z', + workspace_name: 'test-workspace', + workspace_owner_id: MockUser.id, + workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, + workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', + deadline: '2022-05-17T23:39:00.00Z', + reason: 'initiator', + resources: [], + status: 'failed', + daily_cost: 20, +}); + +export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = { + ...MockWorkspaceBuild, + id: '2', + transition: 'stop', +}; + +export const MockWorkspaceBuildDelete: TypesGen.WorkspaceBuild = { + ...MockWorkspaceBuild, + id: '3', + transition: 'delete', +}; + +export const MockBuilds = [ + { ...MockWorkspaceBuild, id: '1' }, + { ...MockWorkspaceBuildAutostart, id: '2' }, + { ...MockWorkspaceBuildAutostop, id: '3' }, + { ...MockWorkspaceBuildStop, id: '4' }, + { ...MockWorkspaceBuildDelete, id: '5' }, +]; + +export const MockWorkspace: TypesGen.Workspace = { + id: 'test-workspace', + name: 'Test-Workspace', + created_at: '', + updated_at: '', + template_id: MockTemplate.id, + template_name: MockTemplate.name, + template_icon: MockTemplate.icon, + template_display_name: MockTemplate.display_name, + template_allow_user_cancel_workspace_jobs: + MockTemplate.allow_user_cancel_workspace_jobs, + template_active_version_id: MockTemplate.active_version_id, + template_require_active_version: MockTemplate.require_active_version, + outdated: false, + owner_id: MockUser.id, + organization_id: MockOrganization.id, + owner_name: MockUser.username, + owner_avatar_url: 'https://avatars.githubusercontent.com/u/7122116?v=4', + autostart_schedule: MockWorkspaceAutostartEnabled.schedule, + ttl_ms: 2 * 60 * 60 * 1000, + latest_build: MockWorkspaceBuild, + last_used_at: '2022-05-16T15:29:10.302441433Z', + health: { + healthy: true, + failing_agents: [], + }, + automatic_updates: 'never', + allow_renames: true, + favorite: false, +}; + +export const MockFavoriteWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-favorite-workspace', + favorite: true, +}; + +export const MockStoppedWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-stopped-workspace', + latest_build: { ...MockWorkspaceBuildStop, status: 'stopped' }, +}; +export const MockStoppingWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-stopping-workspace', + latest_build: { + ...MockWorkspaceBuildStop, + job: MockRunningProvisionerJob, + status: 'stopping', + }, +}; +export const MockStartingWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-starting-workspace', + latest_build: { + ...MockWorkspaceBuild, + job: MockRunningProvisionerJob, + transition: 'start', + status: 'starting', + }, +}; +export const MockCancelingWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-canceling-workspace', + latest_build: { + ...MockWorkspaceBuild, + job: MockCancelingProvisionerJob, + status: 'canceling', + }, +}; +export const MockCanceledWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-canceled-workspace', + latest_build: { + ...MockWorkspaceBuild, + job: MockCanceledProvisionerJob, + status: 'canceled', + }, +}; +export const MockFailedWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-failed-workspace', + latest_build: { + ...MockWorkspaceBuild, + job: MockFailedProvisionerJob, + status: 'failed', + }, +}; +export const MockDeletingWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-deleting-workspace', + latest_build: { + ...MockWorkspaceBuildDelete, + job: MockRunningProvisionerJob, + status: 'deleting', + }, +}; + +export const MockWorkspaceWithDeletion = { + ...MockStoppedWorkspace, + deleting_at: new Date().toISOString(), +}; + +export const MockDeletedWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-deleted-workspace', + latest_build: { ...MockWorkspaceBuildDelete, status: 'deleted' }, +}; + +export const MockOutdatedWorkspace: TypesGen.Workspace = { + ...MockFailedWorkspace, + id: 'test-outdated-workspace', + outdated: true, +}; + +export const MockRunningOutdatedWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-running-outdated-workspace', + outdated: true, +}; + +export const MockDormantWorkspace: TypesGen.Workspace = { + ...MockStoppedWorkspace, + id: 'test-dormant-workspace', + dormant_at: new Date().toISOString(), +}; + +export const MockDormantOutdatedWorkspace: TypesGen.Workspace = { + ...MockStoppedWorkspace, + id: 'test-dormant-outdated-workspace', + name: 'Dormant-Workspace', + outdated: true, + dormant_at: new Date().toISOString(), +}; + +export const MockOutdatedRunningWorkspaceRequireActiveVersion: TypesGen.Workspace = + { + ...MockWorkspace, + id: 'test-outdated-workspace-require-active-version', + outdated: true, + template_require_active_version: true, + latest_build: { + ...MockWorkspaceBuild, + status: 'running', + }, + }; + +export const MockOutdatedRunningWorkspaceAlwaysUpdate: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-outdated-workspace-always-update', + outdated: true, + automatic_updates: 'always', + latest_build: { + ...MockWorkspaceBuild, + status: 'running', + }, +}; + +export const MockOutdatedStoppedWorkspaceRequireActiveVersion: TypesGen.Workspace = + { + ...MockOutdatedRunningWorkspaceRequireActiveVersion, + latest_build: { + ...MockWorkspaceBuild, + status: 'stopped', + }, + }; + +export const MockOutdatedStoppedWorkspaceAlwaysUpdate: TypesGen.Workspace = { + ...MockOutdatedRunningWorkspaceAlwaysUpdate, + latest_build: { + ...MockWorkspaceBuild, + status: 'stopped', + }, +}; + +export const MockPendingWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: 'test-pending-workspace', + latest_build: { + ...MockWorkspaceBuild, + job: MockPendingProvisionerJob, + transition: 'start', + status: 'pending', + }, +}; + +// just over one page of workspaces +export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { + workspaces: range(1, 27).map((id: number) => ({ + ...MockWorkspace, + id: id.toString(), + name: `${MockWorkspace.name}${id}`, + })), + count: 26, +}; + +export const MockWorkspacesResponseWithDeletions = { + workspaces: [...MockWorkspacesResponse.workspaces, MockWorkspaceWithDeletion], + count: MockWorkspacesResponse.count + 1, +}; + +export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = + { + name: 'first_parameter', + type: 'string', + description: 'This is first parameter', + description_plaintext: 'Markdown: This is first parameter', + default_value: 'abc', + mutable: true, + icon: '/icon/folder.svg', + options: [], + required: true, + ephemeral: false, + }; + +export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = + { + name: 'second_parameter', + type: 'number', + description: 'This is second parameter', + description_plaintext: 'Markdown: This is second parameter', + default_value: '2', + mutable: true, + icon: '/icon/folder.svg', + options: [], + validation_min: 1, + validation_max: 3, + validation_monotonic: 'increasing', + required: true, + ephemeral: false, + }; + +export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = + { + name: 'third_parameter', + type: 'string', + description: 'This is third parameter', + description_plaintext: 'Markdown: This is third parameter', + default_value: 'aaa', + mutable: true, + icon: '/icon/database.svg', + options: [], + validation_error: 'No way!', + validation_regex: '^[a-z]{3}$', + required: true, + ephemeral: false, + }; + +export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter = + { + name: 'fourth_parameter', + type: 'string', + description: 'This is fourth parameter', + description_plaintext: 'Markdown: This is fourth parameter', + default_value: 'def', + mutable: false, + icon: '/icon/database.svg', + options: [], + required: true, + ephemeral: false, + }; + +export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = + { + name: 'fifth_parameter', + type: 'number', + description: 'This is fifth parameter', + description_plaintext: 'Markdown: This is fifth parameter', + default_value: '5', + mutable: true, + icon: '/icon/folder.svg', + options: [], + validation_min: 1, + validation_max: 10, + validation_monotonic: 'decreasing', + required: true, + ephemeral: false, + }; + +export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = { + name: 'first_variable', + description: 'This is first variable.', + type: 'string', + value: '', + default_value: 'abc', + required: false, + sensitive: false, +}; + +export const MockTemplateVersionVariable2: TypesGen.TemplateVersionVariable = { + name: 'second_variable', + description: 'This is second variable.', + type: 'number', + value: '5', + default_value: '3', + required: false, + sensitive: false, +}; + +export const MockTemplateVersionVariable3: TypesGen.TemplateVersionVariable = { + name: 'third_variable', + description: 'This is third variable.', + type: 'bool', + value: '', + default_value: 'false', + required: false, + sensitive: false, +}; + +export const MockTemplateVersionVariable4: TypesGen.TemplateVersionVariable = { + name: 'fourth_variable', + description: 'This is fourth variable.', + type: 'string', + value: 'defghijk', + default_value: '', + required: true, + sensitive: true, +}; + +export const MockTemplateVersionVariable5: TypesGen.TemplateVersionVariable = { + name: 'fifth_variable', + description: 'This is fifth variable.', + type: 'string', + value: '', + default_value: '', + required: true, + sensitive: false, +}; + +export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { + name: 'test', + template_version_id: 'test-template-version', + rich_parameter_values: [], +}; + +export const MockWorkspaceRichParametersRequest: TypesGen.CreateWorkspaceRequest = + { + name: 'test', + template_version_id: 'test-template-version', + rich_parameter_values: [ + { + name: MockTemplateVersionParameter1.name, + value: MockTemplateVersionParameter1.default_value, + }, + ], + }; + +export const MockUserAgent = { + browser: 'Chrome 99.0.4844', + device: 'Other', + ip_address: '11.22.33.44', + os: 'Windows 10', +}; + +export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = { + password: { enabled: true }, + github: { enabled: false }, + oidc: { enabled: false, signInText: '', iconUrl: '' }, +}; + +export const MockAuthMethodsPasswordTermsOfService: TypesGen.AuthMethods = { + terms_of_service_url: 'https://www.youtube.com/watch?v=C2f37Vb2NAE', + password: { enabled: true }, + github: { enabled: false }, + oidc: { enabled: false, signInText: '', iconUrl: '' }, +}; + +export const MockAuthMethodsExternal: TypesGen.AuthMethods = { + password: { enabled: false }, + github: { enabled: true }, + oidc: { + enabled: true, + signInText: 'Google', + iconUrl: '/icon/google.svg', + }, +}; + +export const MockAuthMethodsAll: TypesGen.AuthMethods = { + password: { enabled: true }, + github: { enabled: true }, + oidc: { + enabled: true, + signInText: 'Google', + iconUrl: '/icon/google.svg', + }, +}; + +export const MockGitSSHKey: TypesGen.GitSSHKey = { + user_id: '1fa0200f-7331-4524-a364-35770666caa7', + created_at: '2022-05-16T14:30:34.148205897Z', + updated_at: '2022-05-16T15:29:10.302441433Z', + public_key: + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJOQRIM7kE30rOzrfy+/+R+nQGCk7S9pioihy+2ARbq', +}; + +export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ + { + id: 1, + created_at: '2022-05-19T16:45:31.005Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Setting up', + output: '', + }, + { + id: 2, + created_at: '2022-05-19T16:45:31.006Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Starting workspace', + output: '', + }, + { + id: 3, + created_at: '2022-05-19T16:45:31.072Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '', + }, + { + id: 4, + created_at: '2022-05-19T16:45:31.073Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: 'Initializing the backend...', + }, + { + id: 5, + created_at: '2022-05-19T16:45:31.077Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '', + }, + { + id: 6, + created_at: '2022-05-19T16:45:31.078Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: 'Initializing provider plugins...', + }, + { + id: 7, + created_at: '2022-05-19T16:45:31.078Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '- Finding hashicorp/google versions matching "~\u003e 4.15"...', + }, + { + id: 8, + created_at: '2022-05-19T16:45:31.123Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '- Finding coder/coder versions matching "0.3.4"...', + }, + { + id: 9, + created_at: '2022-05-19T16:45:31.137Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '- Using hashicorp/google v4.21.0 from the shared cache directory', + }, + { + id: 10, + created_at: '2022-05-19T16:45:31.344Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '- Using coder/coder v0.3.4 from the shared cache directory', + }, + { + id: 11, + created_at: '2022-05-19T16:45:31.388Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '', + }, + { + id: 12, + created_at: '2022-05-19T16:45:31.388Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: + 'Terraform has created a lock file .terraform.lock.hcl to record the provider', + }, + { + id: 13, + created_at: '2022-05-19T16:45:31.389Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: + 'selections it made above. Include this file in your version control repository', + }, + { + id: 14, + created_at: '2022-05-19T16:45:31.389Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: + 'so that Terraform can guarantee to make the same selections by default when', + }, + { + id: 15, + created_at: '2022-05-19T16:45:31.39Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: 'you run "terraform init" in the future.', + }, + { + id: 16, + created_at: '2022-05-19T16:45:31.39Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: '', + }, + { + id: 17, + created_at: '2022-05-19T16:45:31.391Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Starting workspace', + output: 'Terraform has been successfully initialized!', + }, + { + id: 18, + created_at: '2022-05-19T16:45:31.42Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'Terraform 1.1.9', + }, + { + id: 19, + created_at: '2022-05-19T16:45:33.537Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'coder_agent.dev: Plan to create', + }, + { + id: 20, + created_at: '2022-05-19T16:45:33.537Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_disk.root: Plan to create', + }, + { + id: 21, + created_at: '2022-05-19T16:45:33.538Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_instance.dev[0]: Plan to create', + }, + { + id: 22, + created_at: '2022-05-19T16:45:33.539Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'Plan: 3 to add, 0 to change, 0 to destroy.', + }, + { + id: 23, + created_at: '2022-05-19T16:45:33.712Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'coder_agent.dev: Creating...', + }, + { + id: 24, + created_at: '2022-05-19T16:45:33.719Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: + 'coder_agent.dev: Creation complete after 0s [id=d07f5bdc-4a8d-4919-9cdb-0ac6ba9e64d6]', + }, + { + id: 25, + created_at: '2022-05-19T16:45:34.139Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_disk.root: Creating...', + }, + { + id: 26, + created_at: '2022-05-19T16:45:44.14Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_disk.root: Still creating... [10s elapsed]', + }, + { + id: 27, + created_at: '2022-05-19T16:45:47.106Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: + 'google_compute_disk.root: Creation complete after 13s [id=projects/bruno-coder-v2/zones/europe-west4-b/disks/coder-developer-bruno-dev-123-root]', + }, + { + id: 28, + created_at: '2022-05-19T16:45:47.118Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_instance.dev[0]: Creating...', + }, + { + id: 29, + created_at: '2022-05-19T16:45:57.122Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'google_compute_instance.dev[0]: Still creating... [10s elapsed]', + }, + { + id: 30, + created_at: '2022-05-19T16:46:00.837Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: + 'google_compute_instance.dev[0]: Creation complete after 14s [id=projects/bruno-coder-v2/zones/europe-west4-b/instances/coder-developer-bruno-dev-123]', + }, + { + id: 31, + created_at: '2022-05-19T16:46:00.846Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'Apply complete! Resources: 3 added, 0 changed, 0 destroyed.', + }, + { + id: 32, + created_at: '2022-05-19T16:46:00.847Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Starting workspace', + output: 'Outputs: 0', + }, + { + id: 33, + created_at: '2022-05-19T16:46:02.283Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Cleaning Up', + output: '', + }, +]; + +export const MockWorkspaceExtendedBuildLogs: TypesGen.ProvisionerJobLog[] = [ + { + id: 938494, + created_at: '2023-08-25T19:07:43.331Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Setting up', + output: '', + }, + { + id: 938495, + created_at: '2023-08-25T19:07:43.331Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Parsing template parameters', + output: '', + }, + { + id: 938496, + created_at: '2023-08-25T19:07:43.339Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Detecting persistent resources', + output: '', + }, + { + id: 938497, + created_at: '2023-08-25T19:07:44.15Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'Initializing the backend...', + }, + { + id: 938498, + created_at: '2023-08-25T19:07:44.215Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'Initializing provider plugins...', + }, + { + id: 938499, + created_at: '2023-08-25T19:07:44.216Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: '- Finding coder/coder versions matching "~> 0.11.0"...', + }, + { + id: 938500, + created_at: '2023-08-25T19:07:44.668Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: '- Finding kreuzwerker/docker versions matching "~> 3.0.1"...', + }, + { + id: 938501, + created_at: '2023-08-25T19:07:44.722Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: '- Using coder/coder v0.11.1 from the shared cache directory', + }, + { + id: 938502, + created_at: '2023-08-25T19:07:44.857Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: '- Using kreuzwerker/docker v3.0.2 from the shared cache directory', + }, + { + id: 938503, + created_at: '2023-08-25T19:07:45.081Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'Terraform has created a lock file .terraform.lock.hcl to record the provider', + }, + { + id: 938504, + created_at: '2023-08-25T19:07:45.081Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'selections it made above. Include this file in your version control repository', + }, + { + id: 938505, + created_at: '2023-08-25T19:07:45.081Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'so that Terraform can guarantee to make the same selections by default when', + }, + { + id: 938506, + created_at: '2023-08-25T19:07:45.082Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'you run "terraform init" in the future.', + }, + { + id: 938507, + created_at: '2023-08-25T19:07:45.083Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'Terraform has been successfully initialized!', + }, + { + id: 938508, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'You may now begin working with Terraform. Try running "terraform plan" to see', + }, + { + id: 938509, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'any changes that are required for your infrastructure. All Terraform commands', + }, + { + id: 938510, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'should now work.', + }, + { + id: 938511, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'If you ever set or change modules or backend configuration for Terraform,', + }, + { + id: 938512, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: + 'rerun this command to reinitialize your working directory. If you forget, other', + }, + { + id: 938513, + created_at: '2023-08-25T19:07:45.084Z', + log_source: 'provisioner', + log_level: 'debug', + stage: 'Detecting persistent resources', + output: 'commands will detect it and remind you to do so if necessary.', + }, + { + id: 938514, + created_at: '2023-08-25T19:07:45.143Z', + log_source: 'provisioner', + log_level: 'info', + stage: 'Detecting persistent resources', + output: 'Terraform 1.1.9', + }, + { + id: 938515, + created_at: '2023-08-25T19:07:46.297Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: 'Warning: Argument is deprecated', + }, + { + id: 938516, + created_at: '2023-08-25T19:07:46.297Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938517, + created_at: '2023-08-25T19:07:46.297Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: ' 15: feature_use_managed_variables = true', + }, + { + id: 938518, + created_at: '2023-08-25T19:07:46.297Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: '', + }, + { + id: 938519, + created_at: '2023-08-25T19:07:46.297Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: + 'Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.', + }, + { + id: 938520, + created_at: '2023-08-25T19:07:46.3Z', + log_source: 'provisioner', + log_level: 'error', + stage: 'Detecting persistent resources', + output: 'Error: ephemeral parameter requires the default property', + }, + { + id: 938521, + created_at: '2023-08-25T19:07:46.3Z', + log_source: 'provisioner', + log_level: 'error', + stage: 'Detecting persistent resources', + output: + 'on devcontainer-on-docker.tf line 27, in data "coder_parameter" "another_one":', + }, + { + id: 938522, + created_at: '2023-08-25T19:07:46.3Z', + log_source: 'provisioner', + log_level: 'error', + stage: 'Detecting persistent resources', + output: ' 27: data "coder_parameter" "another_one" {', + }, + { + id: 938523, + created_at: '2023-08-25T19:07:46.301Z', + log_source: 'provisioner', + log_level: 'error', + stage: 'Detecting persistent resources', + output: '', + }, + { + id: 938524, + created_at: '2023-08-25T19:07:46.301Z', + log_source: 'provisioner', + log_level: 'error', + stage: 'Detecting persistent resources', + output: '', + }, + { + id: 938525, + created_at: '2023-08-25T19:07:46.303Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: 'Warning: Argument is deprecated', + }, + { + id: 938526, + created_at: '2023-08-25T19:07:46.303Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', + }, + { + id: 938527, + created_at: '2023-08-25T19:07:46.303Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: ' 15: feature_use_managed_variables = true', + }, + { + id: 938528, + created_at: '2023-08-25T19:07:46.303Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: '', + }, + { + id: 938529, + created_at: '2023-08-25T19:07:46.303Z', + log_source: 'provisioner', + log_level: 'warn', + stage: 'Detecting persistent resources', + output: + 'Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.', + }, + { + id: 938530, + created_at: '2023-08-25T19:07:46.311Z', + log_source: 'provisioner_daemon', + log_level: 'info', + stage: 'Cleaning Up', + output: '', + }, +]; + +export const MockCancellationMessage = { + message: 'Job successfully canceled', +}; + +type MockAPIInput = { + message?: string; + detail?: string; + validations?: FieldError[]; +}; + +type MockAPIOutput = { + isAxiosError: true; + response: { + data: { + message: string; + detail: string | undefined; + validations: FieldError[] | undefined; + }; + }; +}; + +export const mockApiError = ({ + message = 'Something went wrong.', + detail, + validations, +}: MockAPIInput): MockAPIOutput => ({ + // This is how axios can check if it is an axios error when calling isAxiosError + isAxiosError: true, + response: { + data: { + message, + detail, + validations, + }, + }, +}); + +export const MockEntitlements: TypesGen.Entitlements = { + errors: [], + warnings: [], + has_license: false, + features: withDefaultFeatures({ + workspace_batch_actions: { + enabled: true, + entitlement: 'entitled', + }, + }), + require_telemetry: false, + trial: false, + refreshed_at: '2022-05-20T16:45:57.122Z', +}; + +export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { + errors: [], + warnings: ['You are over your active user limit.', 'And another thing.'], + has_license: true, + trial: false, + require_telemetry: false, + refreshed_at: '2022-05-20T16:45:57.122Z', + features: withDefaultFeatures({ + user_limit: { + enabled: true, + entitlement: 'grace_period', + limit: 100, + actual: 102, + }, + audit_log: { + enabled: true, + entitlement: 'entitled', + }, + browser_only: { + enabled: true, + entitlement: 'entitled', + }, + }), +}; + +export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { + errors: [], + warnings: [], + has_license: true, + require_telemetry: false, + trial: false, + refreshed_at: '2022-05-20T16:45:57.122Z', + features: withDefaultFeatures({ + audit_log: { + enabled: true, + entitlement: 'entitled', + }, + }), +}; + +export const MockEntitlementsWithScheduling: TypesGen.Entitlements = { + errors: [], + warnings: [], + has_license: true, + require_telemetry: false, + trial: false, + refreshed_at: '2022-05-20T16:45:57.122Z', + features: withDefaultFeatures({ + advanced_template_scheduling: { + enabled: true, + entitlement: 'entitled', + }, + }), +}; + +export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = { + errors: [], + warnings: [], + has_license: true, + require_telemetry: false, + trial: false, + refreshed_at: '2022-05-20T16:45:57.122Z', + features: withDefaultFeatures({ + user_limit: { + enabled: true, + entitlement: 'entitled', + limit: 25, + }, + }), +}; + +export const MockExperiments: TypesGen.Experiment[] = []; + +export const MockAuditLog: TypesGen.AuditLog = { + id: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', + request_id: '53bded77-7b9d-4e82-8771-991a34d759f9', + time: '2022-05-19T16:45:57.122Z', + organization_id: MockOrganization.id, + ip: '127.0.0.1', + user_agent: + '"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"', + resource_type: 'workspace', + resource_id: 'ef8d1cf4-82de-4fd9-8980-047dad6d06b5', + resource_target: 'bruno-dev', + resource_icon: '', + action: 'create', + diff: { + ttl: { + old: 0, + new: 3600000000000, + secret: false, + }, + }, + status_code: 200, + additional_fields: {}, + description: '{user} created workspace {target}', + user: MockUser, + resource_link: '/@admin/bruno-dev', + is_deleted: false, +}; + +export const MockAuditLog2: TypesGen.AuditLog = { + ...MockAuditLog, + id: '53bded77-7b9d-4e82-8771-991a34d759f9', + action: 'write', + time: '2022-05-20T16:45:57.122Z', + description: '{user} updated workspace {target}', + diff: { + workspace_name: { + old: 'old-workspace-name', + new: MockWorkspace.name, + secret: false, + }, + workspace_auto_off: { + old: true, + new: false, + secret: false, + }, + template_version_id: { + old: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', + new: '53bded77-7b9d-4e82-8771-991a34d759f9', + secret: false, + }, + roles: { + old: null, + new: ['admin', 'auditor'], + secret: false, + }, + }, +}; + +export const MockWorkspaceCreateAuditLogForDifferentOwner = { + ...MockAuditLog, + additional_fields: { + workspace_owner: 'Member', + }, +}; + +export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { + ...MockAuditLog, + id: 'f90995bf-4a2b-4089-b597-e66e025e523e', + request_id: '61555889-2875-475c-8494-f7693dd5d75b', + action: 'stop', + resource_type: 'workspace_build', + description: '{user} stopped build for workspace {target}', + additional_fields: { + workspace_name: 'test2', + }, +}; + +export const MockAuditLogWithDeletedResource: TypesGen.AuditLog = { + ...MockAuditLog, + is_deleted: true, +}; + +export const MockAuditLogGitSSH: TypesGen.AuditLog = { + ...MockAuditLog, + diff: { + private_key: { + old: '', + new: '', + secret: true, + }, + public_key: { + old: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRUPjBSNtOAnL22+r07OSu9t3Lnm8/5OX8bRHECKS9g\n', + new: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwoUPJPMekuSzMZyV0rA82TGGNzw/Uj/dhLbwiczTpV\n', + secret: false, + }, + }, +}; + +export const MockAuditOauthConvert: TypesGen.AuditLog = { + ...MockAuditLog, + resource_type: 'convert_login', + resource_target: 'oidc', + action: 'create', + status_code: 201, + description: '{user} created login type conversion to {target}}', + diff: { + created_at: { + old: '0001-01-01T00:00:00Z', + new: '2023-06-20T20:44:54.243019Z', + secret: false, + }, + expires_at: { + old: '0001-01-01T00:00:00Z', + new: '2023-06-20T20:49:54.243019Z', + secret: false, + }, + state_string: { + old: '', + new: '', + secret: true, + }, + to_type: { + old: '', + new: 'oidc', + secret: false, + }, + user_id: { + old: '', + new: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', + secret: false, + }, + }, +}; + +export const MockAuditLogSuccessfulLogin: TypesGen.AuditLog = { + ...MockAuditLog, + resource_type: 'api_key', + resource_target: '', + action: 'login', + status_code: 201, + description: '{user} logged in', +}; + +export const MockAuditLogUnsuccessfulLoginKnownUser: TypesGen.AuditLog = { + ...MockAuditLogSuccessfulLogin, + status_code: 401, +}; + +export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { + credits_consumed: 0, + budget: 100, +}; + +export const MockGroup: TypesGen.Group = { + id: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', + name: 'Front-End', + display_name: 'Front-End', + avatar_url: 'https://example.com', + organization_id: MockOrganization.id, + members: [MockUser, MockUser2], + quota_allowance: 5, + source: 'user', +}; + +const everyOneGroup = (organizationId: string): TypesGen.Group => ({ + id: organizationId, + name: 'Everyone', + display_name: '', + organization_id: organizationId, + members: [], + avatar_url: '', + quota_allowance: 0, + source: 'user', +}); + +export const MockTemplateACL: TypesGen.TemplateACL = { + group: [ + { ...everyOneGroup(MockOrganization.id), role: 'use' }, + { ...MockGroup, role: 'admin' }, + ], + users: [{ ...MockUser, role: 'use' }], +}; + +export const MockTemplateACLEmpty: TypesGen.TemplateACL = { + group: [], + users: [], +}; + +export const MockTemplateExample: TypesGen.TemplateExample = { + id: 'aws-windows', + url: 'https://github.com/coder/coder/tree/main/examples/templates/aws-windows', + name: 'Develop in an ECS-hosted container', + description: 'Get started with Linux development on AWS ECS.', + markdown: + '\n# aws-ecs\n\nThis is a sample template for running a Coder workspace on ECS. It assumes there\nis a pre-existing ECS cluster with EC2-based compute to host the workspace.\n\n## Architecture\n\nThis workspace is built using the following AWS resources:\n\n- Task definition - the container definition, includes the image, command, volume(s)\n- ECS service - manages the task definition\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n', + icon: '/icon/aws.svg', + tags: ['aws', 'cloud'], +}; + +export const MockTemplateExample2: TypesGen.TemplateExample = { + id: 'aws-linux', + url: 'https://github.com/coder/coder/tree/main/examples/templates/aws-linux', + name: 'Develop in Linux on AWS EC2', + description: 'Get started with Linux development on AWS EC2.', + markdown: + '\n# aws-linux\n\nTo get started, run `coder templates init`. When prompted, select this template.\nFollow the on-screen instructions to proceed.\n\n## Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith AWS. For example, run `aws configure import` to import credentials on the\nsystem and user running coderd. For other ways to authenticate [consult the\nTerraform docs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Sid": "VisualEditor0",\n "Effect": "Allow",\n "Action": [\n "ec2:GetDefaultCreditSpecification",\n "ec2:DescribeIamInstanceProfileAssociations",\n "ec2:DescribeTags",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:DescribeInstanceCreditSpecifications",\n "ec2:DescribeImages",\n "ec2:ModifyDefaultCreditSpecification",\n "ec2:DescribeVolumes"\n ],\n "Resource": "*"\n },\n {\n "Sid": "CoderResources",\n "Effect": "Allow",\n "Action": [\n "ec2:DescribeInstances",\n "ec2:DescribeInstanceAttribute",\n "ec2:UnmonitorInstances",\n "ec2:TerminateInstances",\n "ec2:StartInstances",\n "ec2:StopInstances",\n "ec2:DeleteTags",\n "ec2:MonitorInstances",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:ModifyInstanceAttribute",\n "ec2:ModifyInstanceCreditSpecification"\n ],\n "Resource": "arn:aws:ec2:*:*:instance/*",\n "Condition": {\n "StringEquals": {\n "aws:ResourceTag/Coder_Provisioned": "true"\n }\n }\n }\n ]\n}\n```\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n', + icon: '/icon/aws.svg', + tags: ['aws', 'cloud'], +}; + +export const MockPermissions: Permissions = { + createGroup: true, + createTemplates: true, + createUser: true, + deleteTemplates: true, + updateTemplates: true, + readAllUsers: true, + updateUsers: true, + viewAuditLog: true, + viewDeploymentValues: true, + viewUpdateCheck: true, + viewDeploymentStats: true, + viewExternalAuthConfig: true, + editWorkspaceProxies: true, +}; + +export const MockDeploymentConfig: DeploymentConfig = { + config: { + enable_terraform_debug_mode: true, + }, + options: [], +}; + +export const MockAppearanceConfig: TypesGen.AppearanceConfig = { + application_name: '', + logo_url: '', + service_banner: { + enabled: false, + }, + notification_banners: [], +}; + +export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter1.name, + value: 'mock-abc', +}; + +export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter2.name, + value: '3', +}; + +export const MockWorkspaceBuildParameter3: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter3.name, + value: 'my-database', +}; + +export const MockWorkspaceBuildParameter4: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter4.name, + value: 'immutable-value', +}; + +export const MockWorkspaceBuildParameter5: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter5.name, + value: '5', +}; + +export const MockTemplateVersionExternalAuthGithub: TypesGen.TemplateVersionExternalAuth = + { + id: 'github', + type: 'github', + authenticate_url: 'https://example.com/external-auth/github', + authenticated: false, + display_icon: '/icon/github.svg', + display_name: 'GitHub', + }; + +export const MockTemplateVersionExternalAuthGithubAuthenticated: TypesGen.TemplateVersionExternalAuth = + { + id: 'github', + type: 'github', + authenticate_url: 'https://example.com/external-auth/github', + authenticated: true, + display_icon: '/icon/github.svg', + display_name: 'GitHub', + }; + +export const MockDeploymentStats: TypesGen.DeploymentStats = { + aggregated_from: '2023-03-06T19:08:55.211625Z', + collected_at: '2023-03-06T19:12:55.211625Z', + next_update_at: '2023-03-06T19:20:55.211625Z', + session_count: { + vscode: 128, + jetbrains: 5, + ssh: 32, + reconnecting_pty: 15, + }, + workspaces: { + building: 15, + failed: 12, + pending: 5, + running: 32, + stopped: 16, + connection_latency_ms: { + P50: 32.56, + P95: 15.23, + }, + rx_bytes: 15613513253, + tx_bytes: 36113513253, + }, +}; + +export const MockDeploymentSSH: TypesGen.SSHConfigResponse = { + hostname_prefix: ' coder.', + ssh_config_options: {}, +}; + +export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [ + { + id: 166663, + created_at: '2023-05-04T11:30:41.402072Z', + output: '+ curl -fsSL https://code-server.dev/install.sh', + level: 'info', + source_id: MockWorkspaceAgentLogSource.id, + }, + { + id: 166664, + created_at: '2023-05-04T11:30:41.40228Z', + output: + '+ sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3', + level: 'info', + source_id: MockWorkspaceAgentLogSource.id, + }, + { + id: 166665, + created_at: '2023-05-04T11:30:42.590731Z', + output: 'Ubuntu 22.04.2 LTS', + level: 'info', + source_id: MockWorkspaceAgentLogSource.id, + }, + { + id: 166666, + created_at: '2023-05-04T11:30:42.593686Z', + output: 'Installing v4.8.3 of the amd64 release from GitHub.', + level: 'info', + source_id: MockWorkspaceAgentLogSource.id, + }, +]; + +export const MockLicenseResponse: GetLicensesResponse[] = [ + { + id: 1, + uploaded_at: '1660104000', + expires_at: '3420244800', // expires on 5/20/2078 + uuid: '1', + claims: { + trial: false, + all_features: true, + version: 1, + features: {}, + license_expires: 3420244800, + }, + }, + { + id: 1, + uploaded_at: '1660104000', + expires_at: '1660104000', // expired on 8/10/2022 + uuid: '1', + claims: { + trial: false, + all_features: true, + version: 1, + features: {}, + license_expires: 1660104000, + }, + }, + { + id: 1, + uploaded_at: '1682346425', + expires_at: '1682346425', // expired on 4/24/2023 + uuid: '1', + claims: { + trial: false, + all_features: true, + version: 1, + features: {}, + license_expires: 1682346425, + }, + }, +]; + +export const MockHealth: TypesGen.HealthcheckReport = { + time: '2023-08-01T16:51:03.29792825Z', + healthy: true, + severity: 'ok', + failing_sections: [], + derp: { + healthy: true, + severity: 'ok', + warnings: [], + dismissed: false, + regions: { + '999': { + healthy: true, + severity: 'ok', + warnings: [], + region: { + EmbeddedRelay: true, + RegionID: 999, + RegionCode: 'coder', + RegionName: 'Council Bluffs, Iowa', + Nodes: [ + { + Name: '999stun0', + RegionID: 999, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: '999b', + RegionID: 999, + HostName: 'dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '999stun0', + RegionID: 999, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: '0', + round_trip_ping_ms: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + stun: { + Enabled: true, + CanSTUN: true, + }, + }, + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '999b', + RegionID: 999, + HostName: 'dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: '7674330', + round_trip_ping_ms: 7674330, + uses_websocket: false, + client_logs: [ + [ + 'derphttp.Client.Connect: connecting to https://dev.coder.com/derp', + ], + [ + 'derphttp.Client.Connect: connecting to https://dev.coder.com/derp', + ], + ], + client_errs: [ + ['recv derp message: derphttp.Client closed'], + [ + 'connect to derp: derphttp.Client.Connect connect to : context deadline exceeded: read tcp 10.44.1.150:59546->149.248.214.149:443: use of closed network connection', + 'connect to derp: derphttp.Client closed', + 'connect to derp: derphttp.Client closed', + 'connect to derp: derphttp.Client closed', + 'connect to derp: derphttp.Client closed', + "couldn't connect after 5 tries, last error: couldn't connect after 5 tries, last error: derphttp.Client closed", + ], + ], + stun: { + Enabled: false, + CanSTUN: false, + }, + }, + ], + }, + '10007': { + healthy: true, + severity: 'ok', + warnings: [], + region: { + EmbeddedRelay: false, + RegionID: 10007, + RegionCode: 'coder_sydney', + RegionName: 'sydney', + Nodes: [ + { + Name: '10007stun0', + RegionID: 10007, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: '10007a', + RegionID: 10007, + HostName: 'sydney.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10007stun0', + RegionID: 10007, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: '0', + round_trip_ping_ms: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + stun: { + Enabled: true, + CanSTUN: true, + }, + }, + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10007a', + RegionID: 10007, + HostName: 'sydney.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: '170527034', + round_trip_ping_ms: 170527034, + uses_websocket: false, + client_logs: [ + [ + 'derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp', + ], + [ + 'derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp', + ], + ], + client_errs: [[], []], + stun: { + Enabled: false, + CanSTUN: false, + }, + }, + ], + }, + '10008': { + healthy: true, + severity: 'ok', + warnings: [], + region: { + EmbeddedRelay: false, + RegionID: 10008, + RegionCode: 'coder_europe-frankfurt', + RegionName: 'europe-frankfurt', + Nodes: [ + { + Name: '10008stun0', + RegionID: 10008, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: '10008a', + RegionID: 10008, + HostName: 'europe.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10008stun0', + RegionID: 10008, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: '0', + round_trip_ping_ms: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + stun: { + Enabled: true, + CanSTUN: true, + }, + }, + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10008a', + RegionID: 10008, + HostName: 'europe.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: '111329690', + round_trip_ping_ms: 111329690, + uses_websocket: false, + client_logs: [ + [ + 'derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp', + ], + [ + 'derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp', + ], + ], + client_errs: [[], []], + stun: { + Enabled: false, + CanSTUN: false, + }, + }, + ], + }, + '10009': { + healthy: true, + severity: 'ok', + warnings: [], + region: { + EmbeddedRelay: false, + RegionID: 10009, + RegionCode: 'coder_brazil-saopaulo', + RegionName: 'brazil-saopaulo', + Nodes: [ + { + Name: '10009stun0', + RegionID: 10009, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + { + Name: '10009a', + RegionID: 10009, + HostName: 'brazil.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + ], + }, + node_reports: [ + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10009stun0', + RegionID: 10009, + HostName: 'stun.l.google.com', + STUNPort: 19302, + STUNOnly: true, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: false, + round_trip_ping: '0', + round_trip_ping_ms: 0, + uses_websocket: false, + client_logs: [], + client_errs: [], + stun: { + Enabled: true, + CanSTUN: true, + }, + }, + { + healthy: true, + severity: 'ok', + warnings: [], + node: { + Name: '10009a', + RegionID: 10009, + HostName: 'brazil.dev.coder.com', + STUNPort: -1, + DERPPort: 443, + }, + node_info: { + TokenBucketBytesPerSecond: 0, + TokenBucketBytesBurst: 0, + }, + can_exchange_messages: true, + round_trip_ping: '138185506', + round_trip_ping_ms: 138185506, + uses_websocket: false, + client_logs: [ + [ + 'derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp', + ], + [ + 'derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp', + ], + ], + client_errs: [[], []], + stun: { + Enabled: false, + CanSTUN: false, + }, + }, + ], + }, + }, + netcheck: { + UDP: true, + IPv6: false, + IPv4: true, + IPv6CanSend: false, + IPv4CanSend: true, + OSHasIPv6: true, + ICMPv4: false, + MappingVariesByDestIP: false, + HairPinning: null, + UPnP: false, + PMP: false, + PCP: false, + PreferredDERP: 999, + RegionLatency: { + '999': 1638180, + '10007': 174853022, + '10008': 112142029, + '10009': 138855606, + }, + RegionV4Latency: { + '999': 1638180, + '10007': 174853022, + '10008': 112142029, + '10009': 138855606, + }, + RegionV6Latency: {}, + GlobalV4: '34.71.26.24:55368', + GlobalV6: '', + CaptivePortal: null, + }, + netcheck_logs: [ + 'netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms', + 'netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms', + 'netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms', + 'netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms', + 'netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms', + 'netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms', + 'netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms', + 'netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms', + 'netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted', + 'netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms', + ], + }, + access_url: { + healthy: true, + severity: 'ok', + warnings: [], + dismissed: false, + access_url: 'https://dev.coder.com', + reachable: true, + status_code: 200, + healthz_response: 'OK', + }, + websocket: { + healthy: true, + severity: 'ok', + warnings: [], + dismissed: false, + body: '', + code: 101, + }, + database: { + healthy: true, + severity: 'ok', + warnings: [], + dismissed: false, + reachable: true, + latency: '92570', + latency_ms: 92570, + threshold_ms: 92570, + }, + workspace_proxy: { + healthy: true, + severity: 'warning', + warnings: [ + { + code: 'EWP04', + message: + 'unhealthy: request to proxy failed: Get "http://127.0.0.1:3001/healthz-report": dial tcp 127.0.0.1:3001: connect: connection refused', + }, + ], + dismissed: false, + error: undefined, + workspace_proxies: { + regions: [ + { + id: '1a3e5eb8-d785-4f7d-9188-2eeab140cd06', + name: 'primary', + display_name: 'Council Bluffs, Iowa', + icon_url: '/emojis/1f3e1.png', + healthy: true, + path_app_url: 'https://dev.coder.com', + wildcard_hostname: '*--apps.dev.coder.com', + derp_enabled: false, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.829032482Z', + }, + created_at: '0001-01-01T00:00:00Z', + updated_at: '0001-01-01T00:00:00Z', + deleted: false, + version: '', + }, + { + id: '2876ab4d-bcee-4643-944f-d86323642840', + name: 'sydney', + display_name: 'Sydney GCP', + icon_url: '/emojis/1f1e6-1f1fa.png', + healthy: true, + path_app_url: 'https://sydney.dev.coder.com', + wildcard_hostname: '*--apps.sydney.dev.coder.com', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-05-01T19:15:56.606593Z', + updated_at: '2023-12-05T14:13:36.647535Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + { + id: '9d786ce0-55b1-4ace-8acc-a4672ff8d41f', + name: 'europe-frankfurt', + display_name: 'Europe GCP (Frankfurt)', + icon_url: '/emojis/1f1e9-1f1ea.png', + healthy: true, + path_app_url: 'https://europe.dev.coder.com', + wildcard_hostname: '*--apps.europe.dev.coder.com', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-05-01T20:34:11.114005Z', + updated_at: '2023-12-05T14:13:45.941716Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + { + id: '2e209786-73b1-4838-ba78-e01c9334450a', + name: 'brazil-saopaulo', + display_name: 'Brazil GCP (Sao Paulo)', + icon_url: '/emojis/1f1e7-1f1f7.png', + healthy: true, + path_app_url: 'https://brazil.dev.coder.com', + wildcard_hostname: '*--apps.brazil.dev.coder.com', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-05-01T20:41:02.76448Z', + updated_at: '2023-12-05T14:13:41.968568Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + { + id: 'c272e80c-0cce-49d6-9782-1b5cf90398e8', + name: 'unregistered', + display_name: 'UnregisteredProxy', + icon_url: '/emojis/274c.png', + healthy: false, + path_app_url: '', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'unregistered', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-07-10T14:51:11.539222Z', + updated_at: '2023-07-10T14:51:11.539223Z', + deleted: false, + version: '', + }, + { + id: 'a3efbff1-587b-4677-80a4-dc4f892fed3e', + name: 'unhealthy', + display_name: 'Unhealthy', + icon_url: '/emojis/1f92e.png', + healthy: false, + path_app_url: 'http://127.0.0.1:3001', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'unreachable', + report: { + errors: [ + 'request to proxy failed: Get "http://127.0.0.1:3001/healthz-report": dial tcp 127.0.0.1:3001: connect: connection refused', + ], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-07-10T14:51:48.407017Z', + updated_at: '2023-07-10T14:51:57.993682Z', + deleted: false, + version: '', + }, + { + id: 'b6cefb69-cb6f-46e2-9c9c-39c089fb7e42', + name: 'paris-coder', + display_name: 'Europe (Paris)', + icon_url: '/emojis/1f1eb-1f1f7.png', + healthy: true, + path_app_url: 'https://paris-coder.fly.dev', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-12-01T09:21:15.996267Z', + updated_at: '2023-12-05T14:13:59.663174Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + { + id: '72649dc9-03c7-46a8-bc95-96775e93ddc1', + name: 'sydney-coder', + display_name: 'Australia (Sydney)', + icon_url: '/emojis/1f1e6-1f1fa.png', + healthy: true, + path_app_url: 'https://sydney-coder.fly.dev', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-12-01T09:23:44.505529Z', + updated_at: '2023-12-05T14:13:55.769058Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + { + id: '1f78398f-e5ae-4c38-aa89-30222181d443', + name: 'sao-paulo-coder', + display_name: 'Brazil (Sau Paulo)', + icon_url: '/emojis/1f1e7-1f1f7.png', + healthy: true, + path_app_url: 'https://sao-paulo-coder.fly.dev', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'ok', + report: { + errors: [], + warnings: [], + }, + checked_at: '2023-12-05T14:14:05.250322277Z', + }, + created_at: '2023-12-01T09:36:00.231252Z', + updated_at: '2023-12-05T14:13:47.015031Z', + deleted: false, + version: 'v2.5.0-devel+5fad61102', + }, + ], + }, + }, + provisioner_daemons: { + severity: 'ok', + warnings: [ + { + message: 'Something is wrong!', + code: 'EUNKNOWN', + }, + { + message: 'This is also bad.', + code: 'EPD01', + }, + ], + dismissed: false, + items: [ + { + provisioner_daemon: { + id: 'e455b582-ac04-4323-9ad6-ab71301fa006', + created_at: '2024-01-04T15:53:03.21563Z', + last_seen_at: '2024-01-04T16:05:03.967551Z', + name: 'ok', + version: 'v2.3.4-devel+abcd1234', + api_version: '1.0', + provisioners: ['echo', 'terraform'], + tags: { + owner: '', + scope: 'organization', + tag_value: 'value', + tag_true: 'true', + tag_1: '1', + tag_yes: 'yes', + }, + }, + warnings: [], + }, + { + provisioner_daemon: { + id: '00000000-0000-0000-000000000000', + created_at: '2024-01-04T15:53:03.21563Z', + last_seen_at: '2024-01-04T16:05:03.967551Z', + name: 'user-scoped', + version: 'v2.34-devel+abcd1234', + api_version: '1.0', + provisioners: ['echo', 'terraform'], + tags: { + owner: '12345678-1234-1234-1234-12345678abcd', + scope: 'user', + tag_VALUE: 'VALUE', + tag_TRUE: 'TRUE', + tag_1: '1', + tag_YES: 'YES', + }, + }, + warnings: [], + }, + { + provisioner_daemon: { + id: 'e455b582-ac04-4323-9ad6-ab71301fa006', + created_at: '2024-01-04T15:53:03.21563Z', + last_seen_at: '2024-01-04T16:05:03.967551Z', + name: 'unhappy', + version: 'v0.0.1', + api_version: '0.1', + provisioners: ['echo', 'terraform'], + tags: { + owner: '', + scope: 'organization', + tag_string: 'value', + tag_false: 'false', + tag_0: '0', + tag_no: 'no', + }, + }, + warnings: [ + { + message: 'Something specific is wrong with this daemon.', + code: 'EUNKNOWN', + }, + { + message: 'And now for something completely different.', + code: 'EUNKNOWN', + }, + ], + }, + ], + }, + coder_version: 'v2.5.0-devel+5fad61102', +}; + +export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = + { + ports: [ + { process_name: 'webb', network: '', port: 30000 }, + { process_name: 'gogo', network: '', port: 8080 }, + { process_name: '', network: '', port: 8081 }, + ], + }; + +export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = { + shares: [ + { + workspace_id: MockWorkspace.id, + agent_name: 'a-workspace-agent', + port: 4000, + share_level: 'authenticated', + protocol: 'http', + }, + { + workspace_id: MockWorkspace.id, + agent_name: 'a-workspace-agent', + port: 65535, + share_level: 'authenticated', + protocol: 'https', + }, + { + workspace_id: MockWorkspace.id, + agent_name: 'a-workspace-agent', + port: 8081, + share_level: 'public', + protocol: 'http', + }, + ], +}; + +export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { + healthy: false, + severity: 'ok', + failing_sections: [], // apparently this property is not used at all? + time: '2023-10-12T23:15:00.000000000Z', + coder_version: 'v2.3.0-devel+8cca4915a', + access_url: { + healthy: true, + severity: 'ok', + warnings: [], + dismissed: false, + access_url: '', + healthz_response: '', + reachable: true, + status_code: 0, + }, + database: { + healthy: false, + severity: 'ok', + warnings: [], + dismissed: false, + latency: '', + latency_ms: 0, + reachable: true, + threshold_ms: 92570, + }, + derp: { + healthy: false, + severity: 'ok', + warnings: [], + dismissed: false, + regions: [], + netcheck_logs: [], + }, + websocket: { + healthy: false, + severity: 'ok', + warnings: [], + dismissed: false, + body: '', + code: 0, + }, + workspace_proxy: { + healthy: false, + error: 'some error', + severity: 'error', + warnings: [], + dismissed: false, + workspace_proxies: { + regions: [ + { + id: 'df7e4b2b-2d40-47e5-a021-e5d08b219c77', + name: 'unhealthy', + display_name: 'unhealthy', + icon_url: '/emojis/1f5fa.png', + healthy: false, + path_app_url: 'http://127.0.0.1:3001', + wildcard_hostname: '', + derp_enabled: true, + derp_only: false, + status: { + status: 'unreachable', + report: { + errors: ['some error'], + warnings: [], + }, + checked_at: '2023-11-24T12:14:05.743303497Z', + }, + created_at: '2023-11-23T15:37:25.513213Z', + updated_at: '2023-11-23T18:09:19.734747Z', + deleted: false, + version: 'v2.5.0-devel+89bae7eff', + }, + ], + }, + }, + provisioner_daemons: { + severity: 'error', + error: 'something went wrong', + warnings: [ + { + message: 'this is a message', + code: 'EUNKNOWN', + }, + ], + dismissed: false, + items: [ + { + provisioner_daemon: { + id: 'e455b582-ac04-4323-9ad6-ab71301fa006', + created_at: '2024-01-04T15:53:03.21563Z', + last_seen_at: '2024-01-04T16:05:03.967551Z', + name: 'vvuurrkk-2', + version: 'v2.6.0-devel+965ad5e96', + api_version: '1.0', + provisioners: ['echo', 'terraform'], + tags: { + owner: '', + scope: 'organization', + }, + }, + warnings: [ + { + message: 'this is a specific message for this thing', + code: 'EUNKNOWN', + }, + ], + }, + ], + }, +}; + +export const MockHealthSettings: TypesGen.HealthSettings = { + dismissed_healthchecks: [], +}; + +export const MockGithubExternalProvider: TypesGen.ExternalAuthLinkProvider = { + id: 'github', + type: 'github', + device: false, + display_icon: '/icon/github.svg', + display_name: 'GitHub', + allow_refresh: true, + allow_validate: true, +}; + +export const MockGithubAuthLink: TypesGen.ExternalAuthLink = { + provider_id: 'github', + created_at: '', + updated_at: '', + has_refresh_token: true, + expires: '', + authenticated: true, + validate_error: '', +}; + +export const MockOAuth2ProviderApps: TypesGen.OAuth2ProviderApp[] = [ + { + id: '1', + name: 'foo', + callback_url: 'http://localhost:3001', + icon: '/icon/github.svg', + endpoints: { + authorization: 'http://localhost:3001/oauth2/authorize', + token: 'http://localhost:3001/oauth2/token', + device_authorization: '', + }, + }, +]; + +export const MockOAuth2ProviderAppSecrets: TypesGen.OAuth2ProviderAppSecret[] = + [ + { + id: '1', + client_secret_truncated: 'foo', + }, + { + id: '1', + last_used_at: '2022-12-16T20:10:45.637452Z', + client_secret_truncated: 'foo', + }, + ]; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts index 412e0e05..df137d69 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts @@ -1,5 +1,15 @@ +import { User } from '../api/vendoredSdk'; import type { Workspace } from '../typesConstants'; -import { mockBackstageApiEndpoint } from './mockBackstageData'; +import { MockUser } from './coderEntities'; +import { + mockBackstageApiEndpoint, + mockBackstageAssetsEndpoint, +} from './mockBackstageData'; + +export const mockUserWithProxyUrls: User = { + ...MockUser, + avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, +}; /** * The main mock for a workspace whose repo URL matches cleanedRepoUrl diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 69fe816a..b68f48f2 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -11,11 +11,11 @@ import { setupServer } from 'msw/node'; /* eslint-enable @backstage/no-undeclared-imports */ import { + mockUserWithProxyUrls, mockWorkspacesList, mockWorkspacesListForRepoSearch, } from './mockCoderAppData'; import { - mockBackstageAssetsEndpoint, mockBearerToken, mockCoderAuthToken, mockCoderWorkspacesConfig, @@ -23,7 +23,7 @@ import { } from './mockBackstageData'; import type { WorkspacesResponse } from '../typesConstants'; import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient'; -import { User } from '../typesConstants'; +import type { User } from '../api/vendoredSdk'; type RestResolver = ResponseResolver< RestRequest, @@ -129,14 +129,7 @@ const mainTestHandlers: readonly RestHandler[] = [ // This is the dummy request used to verify a user's auth status wrappedGet(mockServerEndpoints.authenticatedUser, (_, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - id: '1', - avatar_url: `${mockBackstageAssetsEndpoint}/blueberry.png`, - username: 'blueberry', - }), - ); + return res(ctx.status(200), ctx.json(mockUserWithProxyUrls)); }), ]; diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index 76551f89..5ab133c2 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -87,23 +87,6 @@ export type WorkspaceBuild = Output; export type Workspace = Output; export type WorkspacesResponse = Output; -export type WorkspacesRequest = Readonly<{ - after_id?: string; - limit?: number; - offset?: number; - q?: string; -}>; - -// This is actually the MinimalUser type from Coder core (User extends from -// ReducedUser, which extends from MinimalUser). Don't need all the properties -// until we roll out full SDK support, so going with the least privileged -// type definition for now -export type User = Readonly<{ - id: string; - username: string; - avatar_url: string; -}>; - /** * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to * retrying a failed API request 3 times before exposing an error to the UI From d9626a0e6164a21c54ef642e0dedb74aed35915f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 19:00:14 +0000 Subject: [PATCH 07/22] wip: commit more progress on updating types --- .../src/api/CoderClient.test.ts | 5 ++--- .../CoderWorkspacesCard/WorkspacesListItem.tsx | 3 ++- .../backstage-plugin-coder/src/testHelpers/server.ts | 3 +-- plugins/backstage-plugin-coder/src/typesConstants.ts | 12 ------------ .../backstage-plugin-coder/src/utils/workspaces.ts | 2 +- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 9addcd1a..4cdf3738 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -13,7 +13,7 @@ import { mockWorkspacesList, mockWorkspacesListForRepoSearch, } from '../testHelpers/mockCoderAppData'; -import type { Workspace, WorkspacesResponse } from '../typesConstants'; +import type { Workspace, WorkspacesResponse } from './vendoredSdk'; import { getMockConfigApi, getMockDiscoveryApi, @@ -103,7 +103,6 @@ describe(`${CoderClient.name}`, () => { describe('cleanupClient functionality', () => { it('Will prevent any new SDK requests from going through', async () => { const client = new CoderClient({ apis: getConstructorApis() }); - client.cleanupClient(); // Request should fail, even though token is valid await expect(() => { @@ -139,7 +138,7 @@ describe(`${CoderClient.name}`, () => { q: 'owner:me', limit: 0, }); - client.cleanupClient(); + await expect(() => workspacesPromise2).rejects.toThrow(); }); }); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx index f7292e51..004d77aa 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx @@ -11,7 +11,8 @@ import { useId } from '../../hooks/hookPolyfills'; import { useCoderAppConfig } from '../CoderProvider'; import { getWorkspaceAgentStatuses } from '../../utils/workspaces'; -import type { Workspace, WorkspaceStatus } from '../../typesConstants'; +import type { WorkspaceStatus } from '../../api/vendoredSdk'; +import type { Workspace } from '../../typesConstants'; import { WorkspacesListIcon } from './WorkspacesListIcon'; import { VisuallyHidden } from '../VisuallyHidden'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index b68f48f2..9ab2dae2 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -21,9 +21,8 @@ import { mockCoderWorkspacesConfig, mockBackstageApiEndpoint as root, } from './mockBackstageData'; -import type { WorkspacesResponse } from '../typesConstants'; import { CODER_AUTH_HEADER_KEY } from '../api/CoderClient'; -import type { User } from '../api/vendoredSdk'; +import type { User, WorkspacesResponse } from '../api/vendoredSdk'; type RestResolver = ResponseResolver< RestRequest, diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index 5ab133c2..ff226101 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -1,7 +1,6 @@ import { type Output, array, - number, object, string, union, @@ -74,18 +73,7 @@ export const workspaceSchema = object({ latest_build: workspaceBuildSchema, }); -export const workspacesResponseSchema = object({ - count: number(), - workspaces: array(workspaceSchema), -}); - -export type WorkspaceAgentStatus = Output; -export type WorkspaceAgent = Output; -export type WorkspaceResource = Output; -export type WorkspaceStatus = Output; -export type WorkspaceBuild = Output; export type Workspace = Output; -export type WorkspacesResponse = Output; /** * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to diff --git a/plugins/backstage-plugin-coder/src/utils/workspaces.ts b/plugins/backstage-plugin-coder/src/utils/workspaces.ts index c36b6d4b..f9317a97 100644 --- a/plugins/backstage-plugin-coder/src/utils/workspaces.ts +++ b/plugins/backstage-plugin-coder/src/utils/workspaces.ts @@ -1,4 +1,4 @@ -import type { Workspace, WorkspaceAgentStatus } from '../typesConstants'; +import { Workspace, WorkspaceAgentStatus } from '../api/vendoredSdk'; export function getWorkspaceAgentStatuses( workspace: Workspace, From 1dcc13b6fbcf7e0dacf9f6cd4b24e319740f6022 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 19:07:41 +0000 Subject: [PATCH 08/22] chore: remove valibot type definitions from global constants file --- .../ReminderAccordion.test.tsx | 2 +- .../components/CoderWorkspacesCard/Root.tsx | 2 +- .../WorkspacesList.test.tsx | 2 +- .../CoderWorkspacesCard/WorkspacesList.tsx | 2 +- .../WorkspacesListItem.test.tsx | 2 +- .../WorkspacesListItem.tsx | 2 +- .../src/testHelpers/mockCoderAppData.ts | 2 +- .../src/typesConstants.ts | 56 ------------------- 8 files changed, 7 insertions(+), 63 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx index 0ae1d918..5be7284b 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ReminderAccordion.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { type WorkspacesCardContext, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 0866d95a..452f0a9c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -15,7 +15,7 @@ import { useCoderWorkspacesConfig, type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx index 50bc1de1..ccb60d47 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx @@ -4,7 +4,7 @@ import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { screen } from '@testing-library/react'; type RenderInputs = Readonly<{ diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index 1e47b08a..9301d6a4 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -1,7 +1,7 @@ import React, { type HTMLAttributes, type ReactNode, Fragment } from 'react'; import { type Theme, makeStyles } from '@material-ui/core'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { useWorkspacesCardContext } from './Root'; import { WorkspacesListItem } from './WorkspacesListItem'; import { Placeholder } from './Placeholder'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx index 03ff2623..36922919 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListItem } from './WorkspacesListItem'; type RenderInput = Readonly<{ diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx index 004d77aa..a5a588ae 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.tsx @@ -12,7 +12,7 @@ import { useCoderAppConfig } from '../CoderProvider'; import { getWorkspaceAgentStatuses } from '../../utils/workspaces'; import type { WorkspaceStatus } from '../../api/vendoredSdk'; -import type { Workspace } from '../../typesConstants'; +import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListIcon } from './WorkspacesListIcon'; import { VisuallyHidden } from '../VisuallyHidden'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts index df137d69..ea5f46ad 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts @@ -1,5 +1,5 @@ import { User } from '../api/vendoredSdk'; -import type { Workspace } from '../typesConstants'; +import type { Workspace } from '../api/vendoredSdk'; import { MockUser } from './coderEntities'; import { mockBackstageApiEndpoint, diff --git a/plugins/backstage-plugin-coder/src/typesConstants.ts b/plugins/backstage-plugin-coder/src/typesConstants.ts index ff226101..986696bd 100644 --- a/plugins/backstage-plugin-coder/src/typesConstants.ts +++ b/plugins/backstage-plugin-coder/src/typesConstants.ts @@ -1,13 +1,3 @@ -import { - type Output, - array, - object, - string, - union, - literal, - optional, -} from 'valibot'; - export type ReadonlyJsonValue = | string | number @@ -29,52 +19,6 @@ export const CODER_API_REF_ID_PREFIX = 'backstage-plugin-coder'; export const DEFAULT_CODER_DOCS_LINK = 'https://coder.com/docs/v2/latest'; -export const workspaceAgentStatusSchema = union([ - literal('connected'), - literal('connecting'), - literal('disconnected'), - literal('timeout'), -]); - -export const workspaceAgentSchema = object({ - id: string(), - status: workspaceAgentStatusSchema, -}); - -export const workspaceResourceSchema = object({ - id: string(), - agents: optional(array(workspaceAgentSchema)), -}); - -export const workspaceStatusSchema = union([ - literal('canceled'), - literal('canceling'), - literal('deleted'), - literal('deleting'), - literal('failed'), - literal('pending'), - literal('running'), - literal('starting'), - literal('stopped'), - literal('stopping'), -]); - -export const workspaceBuildSchema = object({ - id: string(), - resources: array(workspaceResourceSchema), - status: workspaceStatusSchema, -}); - -export const workspaceSchema = object({ - id: string(), - name: string(), - template_icon: string(), - owner_name: string(), - latest_build: workspaceBuildSchema, -}); - -export type Workspace = Output; - /** * 2024-05-22 - While this isn't documented anywhere, TanStack Query defaults to * retrying a failed API request 3 times before exposing an error to the UI From 692a763016c23d69fa0bd0b3c156318fde3106e6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 19:08:24 +0000 Subject: [PATCH 09/22] chore: rename mocks file --- plugins/backstage-plugin-coder/src/api/CoderClient.test.ts | 2 +- .../components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx | 2 +- .../src/components/CoderWorkspacesCard/WorkspacesList.test.tsx | 2 +- .../components/CoderWorkspacesCard/WorkspacesListItem.test.tsx | 2 +- .../src/hooks/useCoderWorkspacesQuery.test.ts | 2 +- .../testHelpers/{mockCoderAppData.ts => mockCoderPluginData.ts} | 0 plugins/backstage-plugin-coder/src/testHelpers/server.ts | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename plugins/backstage-plugin-coder/src/testHelpers/{mockCoderAppData.ts => mockCoderPluginData.ts} (100%) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 4cdf3738..113d29d9 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -12,7 +12,7 @@ import { delay } from '../utils/time'; import { mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from '../testHelpers/mockCoderAppData'; +} from '../testHelpers/mockCoderPluginData'; import type { Workspace, WorkspacesResponse } from './vendoredSdk'; import { getMockConfigApi, diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx index a8cbef6c..8acc04a1 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.test.tsx @@ -10,7 +10,7 @@ import { mockWorkspaceNoParameters, mockWorkspaceWithMatch2, mockWorkspacesList, -} from '../../testHelpers/mockCoderAppData'; +} from '../../testHelpers/mockCoderPluginData'; import { type CoderAuthStatus } from '../CoderProvider'; import { CoderWorkspacesCard } from './CoderWorkspacesCard'; import userEvent from '@testing-library/user-event'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx index ccb60d47..bc7e0273 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx @@ -3,7 +3,7 @@ import { type WorkspacesListProps, WorkspacesList } from './WorkspacesList'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root'; import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; import type { Workspace } from '../../api/vendoredSdk'; import { screen } from '@testing-library/react'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx index 36922919..3d9d7b87 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { renderInCoderEnvironment } from '../../testHelpers/setup'; -import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData'; +import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListItem } from './WorkspacesListItem'; diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts index d29e64a5..49535619 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts @@ -6,7 +6,7 @@ import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData'; import { mockWorkspaceNoParameters, mockWorkspacesList, -} from '../testHelpers/mockCoderAppData'; +} from '../testHelpers/mockCoderPluginData'; beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts similarity index 100% rename from plugins/backstage-plugin-coder/src/testHelpers/mockCoderAppData.ts rename to plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 9ab2dae2..1031bffd 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -14,7 +14,7 @@ import { mockUserWithProxyUrls, mockWorkspacesList, mockWorkspacesListForRepoSearch, -} from './mockCoderAppData'; +} from './mockCoderPluginData'; import { mockBearerToken, mockCoderAuthToken, From 28accc8c75b07159d4a83070a3d854effe2ff4e3 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 19:40:29 +0000 Subject: [PATCH 10/22] fix: update type mismatches --- .../components/CoderErrorBoundary/CoderErrorBoundary.tsx | 2 +- plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx index c1f2bc61..5843a180 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderErrorBoundary/CoderErrorBoundary.tsx @@ -39,7 +39,7 @@ class ErrorBoundaryCore extends Component< render() { const { children, fallbackUi } = this.props; - return this.state.hasError ? fallbackUi : children; + return <>{this.state.hasError ? fallbackUi : children}; } } diff --git a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts index 3b777c5e..ce15f948 100644 --- a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts +++ b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.ts @@ -25,5 +25,11 @@ function useIdPolyfill(): string { return readonlyId; } +const ReactWithNewerHooks = React as typeof React & { + useId?: () => string; +}; + export const useId = - typeof React.useId === 'undefined' ? useIdPolyfill : React.useId; + typeof ReactWithNewerHooks.useId === 'undefined' + ? useIdPolyfill + : ReactWithNewerHooks.useId; From d032768c6b761b1bb9ef294dcd99e2ff2fa8c4aa Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 20:22:51 +0000 Subject: [PATCH 11/22] wip: commit more update progress --- .../src/api/CoderClient.test.ts | 49 +- .../WorkspacesListItem.test.tsx | 6 + .../src/testHelpers/coderEntities.ts | 3341 +---------------- .../src/testHelpers/mockCoderPluginData.ts | 13 +- .../src/testHelpers/server.ts | 1 - 5 files changed, 120 insertions(+), 3290 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index 113d29d9..f807adb7 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -1,8 +1,4 @@ -import { - CODER_AUTH_HEADER_KEY, - CoderClient, - disabledClientError, -} from './CoderClient'; +import { CODER_AUTH_HEADER_KEY, CoderClient } from './CoderClient'; import type { IdentityApi } from '@backstage/core-plugin-api'; import { UrlSync } from './UrlSync'; import { rest } from 'msw'; @@ -100,49 +96,6 @@ describe(`${CoderClient.name}`, () => { }); }); - describe('cleanupClient functionality', () => { - it('Will prevent any new SDK requests from going through', async () => { - const client = new CoderClient({ apis: getConstructorApis() }); - - // Request should fail, even though token is valid - await expect(() => { - return client.syncToken(mockCoderAuthToken); - }).rejects.toThrow(disabledClientError); - - await expect(() => { - return client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - }).rejects.toThrow(disabledClientError); - }); - - it('Will abort any pending requests', async () => { - const client = new CoderClient({ - initialToken: mockCoderAuthToken, - apis: getConstructorApis(), - }); - - // Sanity check to ensure that request can still go through normally - const workspacesPromise1 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - - await expect(workspacesPromise1).resolves.toEqual({ - workspaces: mockWorkspacesList, - count: mockWorkspacesList.length, - }); - - const workspacesPromise2 = client.sdk.getWorkspaces({ - q: 'owner:me', - limit: 0, - }); - - await expect(() => workspacesPromise2).rejects.toThrow(); - }); - }); - // Eventually the Coder SDK is going to get too big to test every single // function. Focus tests on the functionality specifically being patched in // for Backstage diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx index 3d9d7b87..471d3356 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx @@ -4,6 +4,10 @@ import { renderInCoderEnvironment } from '../../testHelpers/setup'; import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderPluginData'; import type { Workspace } from '../../api/vendoredSdk'; import { WorkspacesListItem } from './WorkspacesListItem'; +import { + MockWorkspaceAgent, + MockWorkspaceResource, +} from '../../testHelpers/coderEntities'; type RenderInput = Readonly<{ isOnline?: boolean; @@ -19,9 +23,11 @@ async function renderListItem(inputs?: RenderInput) { status: isOnline ? 'running' : 'stopped', resources: [ { + ...MockWorkspaceResource, id: '1', agents: [ { + ...MockWorkspaceAgent, id: '2', status: isOnline ? 'connected' : 'disconnected', }, diff --git a/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts index 868daaaf..b5cf5abf 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/coderEntities.ts @@ -1,13 +1,13 @@ /** * @file This is a subset of the mock data from the Coder OSS repo. No values - * are modified; if any values should be for Backstage, those should be updated - * in the mockCoderPluginData.ts file. + * are modified; if any values should be patched for Backstage testing, those + * should be updated in the mockCoderPluginData.ts file. * * @see {@link https://github.com/coder/coder/blob/main/site/src/testHelpers/entities.ts} */ import type * as TypesGen from '../api/vendoredSdk'; -export const MockOrganization: TypesGen.Organization = { +const MockOrganization: TypesGen.Organization = { id: 'fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0', name: 'Test Organization', created_at: '', @@ -15,7 +15,7 @@ export const MockOrganization: TypesGen.Organization = { is_default: true, }; -export const MockOwnerRole: TypesGen.Role = { +const MockOwnerRole: TypesGen.Role = { name: 'owner', display_name: 'Owner', site_permissions: [], @@ -39,334 +39,7 @@ export const MockUser: TypesGen.User = { name: '', }; -export const MockTemplateDAUResponse: TypesGen.DAUsResponse = { - tz_hour_offset: 0, - entries: [ - { date: '2022-08-27', amount: 1 }, - { date: '2022-08-29', amount: 2 }, - { date: '2022-08-30', amount: 1 }, - ], -}; -export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = { - tz_hour_offset: 0, - entries: [ - { date: '2022-08-27', amount: 10 }, - { date: '2022-08-29', amount: 22 }, - { date: '2022-08-30', amount: 14 }, - ], -}; -export const MockSessionToken: TypesGen.LoginWithPasswordResponse = { - session_token: 'my-session-token', -}; - -export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = { - key: 'my-api-key', -}; - -export const MockToken: TypesGen.APIKeyWithOwner = { - id: 'tBoVE3dqLl', - user_id: 'f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b', - last_used: '0001-01-01T00:00:00Z', - expires_at: '2023-01-15T20:10:45.637438Z', - created_at: '2022-12-16T20:10:45.637452Z', - updated_at: '2022-12-16T20:10:45.637452Z', - login_type: 'token', - scope: 'all', - lifetime_seconds: 2592000, - token_name: 'token-one', - username: 'admin', -}; - -export const MockTokens: TypesGen.APIKeyWithOwner[] = [ - MockToken, - { - id: 'tBoVE3dqLl', - user_id: 'f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b', - last_used: '0001-01-01T00:00:00Z', - expires_at: '2023-01-15T20:10:45.637438Z', - created_at: '2022-12-16T20:10:45.637452Z', - updated_at: '2022-12-16T20:10:45.637452Z', - login_type: 'token', - scope: 'all', - lifetime_seconds: 2592000, - token_name: 'token-two', - username: 'admin', - }, -]; - -export const MockPrimaryWorkspaceProxy: TypesGen.WorkspaceProxy = { - id: '4aa23000-526a-481f-a007-0f20b98b1e12', - name: 'primary', - display_name: 'Default', - icon_url: '/emojis/1f60e.png', - healthy: true, - path_app_url: 'https://coder.com', - wildcard_hostname: '*.coder.com', - derp_enabled: true, - derp_only: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - version: 'v2.34.5-test+primary', - deleted: false, - status: { - status: 'ok', - checked_at: new Date().toISOString(), - }, -}; - -export const MockHealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { - id: '5e2c1ab7-479b-41a9-92ce-aa85625de52c', - name: 'haswildcard', - display_name: 'Subdomain Supported', - icon_url: '/emojis/1f319.png', - healthy: true, - path_app_url: 'https://external.com', - wildcard_hostname: '*.external.com', - derp_enabled: true, - derp_only: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - deleted: false, - version: 'v2.34.5-test+haswildcard', - status: { - status: 'ok', - checked_at: new Date().toISOString(), - }, -}; - -export const MockUnhealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { - id: '8444931c-0247-4171-842a-569d9f9cbadb', - name: 'unhealthy', - display_name: 'Unhealthy', - icon_url: '/emojis/1f92e.png', - healthy: false, - path_app_url: 'https://unhealthy.coder.com', - wildcard_hostname: '*unhealthy..coder.com', - derp_enabled: true, - derp_only: true, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - version: 'v2.34.5-test+unhealthy', - deleted: false, - status: { - status: 'unhealthy', - report: { - errors: ['This workspace proxy is manually marked as unhealthy.'], - warnings: ['This is a manual warning for this workspace proxy.'], - }, - checked_at: new Date().toISOString(), - }, -}; - -export const MockWorkspaceProxies: TypesGen.WorkspaceProxy[] = [ - MockPrimaryWorkspaceProxy, - MockHealthyWildWorkspaceProxy, - MockUnhealthyWildWorkspaceProxy, - { - id: '26e84c16-db24-4636-a62d-aa1a4232b858', - name: 'nowildcard', - display_name: 'No wildcard', - icon_url: '/emojis/1f920.png', - healthy: true, - path_app_url: 'https://cowboy.coder.com', - wildcard_hostname: '', - derp_enabled: false, - derp_only: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - deleted: false, - version: 'v2.34.5-test+nowildcard', - status: { - status: 'ok', - checked_at: new Date().toISOString(), - }, - }, -]; - -export const MockProxyLatencies: Record = { - ...MockWorkspaceProxies.reduce((acc, proxy) => { - if (!proxy.healthy) { - return acc; - } - acc[proxy.id] = { - // Make one of them inaccurate. - accurate: proxy.id !== '26e84c16-db24-4636-a62d-aa1a4232b858', - // This is a deterministic way to generate a latency to for each proxy. - // It will be the same for each run as long as the IDs don't change. - latencyMS: - (Number( - Array.from(proxy.id).reduce( - // Multiply each char code by some large prime number to increase the - // size of the number and allow use to get some decimal points. - (acc, char) => acc + char.charCodeAt(0) * 37, - 0, - ), - ) / - // Cap at 250ms - 100) % - 250, - at: new Date(), - }; - return acc; - }, {} as Record), -}; - -export const MockBuildInfo: TypesGen.BuildInfoResponse = { - agent_api_version: '1.0', - external_url: 'file:///mock-url', - version: 'v99.999.9999+c9cdf14', - dashboard_url: 'https:///mock-url', - workspace_proxy: false, - upgrade_message: 'My custom upgrade message', - deployment_id: '510d407f-e521-4180-b559-eab4a6d802b8', -}; - -export const MockSupportLinks: TypesGen.LinkConfig[] = [ - { - name: 'First link', - target: 'http://first-link', - icon: 'chat', - }, - { - name: 'Second link', - target: 'http://second-link', - icon: 'docs', - }, - { - name: 'Third link', - target: - 'https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}', - icon: '', - }, -]; - -export const MockUpdateCheck: TypesGen.UpdateCheckResponse = { - current: true, - url: 'file:///mock-url', - version: 'v99.999.9999+c9cdf14', -}; - -export const MockUserAdminRole: TypesGen.Role = { - name: 'user_admin', - display_name: 'User Admin', - site_permissions: [], - organization_permissions: {}, - user_permissions: [], - organization_id: '', -}; - -export const MockTemplateAdminRole: TypesGen.Role = { - name: 'template_admin', - display_name: 'Template Admin', - site_permissions: [], - organization_permissions: {}, - user_permissions: [], - organization_id: '', -}; - -export const MockMemberRole: TypesGen.SlimRole = { - name: 'member', - display_name: 'Member', -}; - -export const MockAuditorRole: TypesGen.Role = { - name: 'auditor', - display_name: 'Auditor', - site_permissions: [], - organization_permissions: {}, - user_permissions: [], - organization_id: '', -}; - -// assignableRole takes a role and a boolean. The boolean implies if the -// actor can assign (add/remove) the role from other users. -export function assignableRole( - role: TypesGen.Role, - assignable: boolean, -): TypesGen.AssignableRoles { - return { - ...role, - assignable: assignable, - built_in: true, - }; -} - -export const MockSiteRoles = [MockUserAdminRole, MockAuditorRole]; -export const MockAssignableSiteRoles = [ - assignableRole(MockUserAdminRole, true), - assignableRole(MockAuditorRole, true), -]; - -export const MockMemberPermissions = { - viewAuditLog: false, -}; - -export const MockUserAdmin: TypesGen.User = { - id: 'test-user', - username: 'TestUser', - email: 'test@coder.com', - created_at: '', - status: 'active', - organization_ids: [MockOrganization.id], - roles: [MockUserAdminRole], - avatar_url: '', - last_seen_at: '', - login_type: 'password', - theme_preference: '', - name: '', -}; - -export const MockUser2: TypesGen.User = { - id: 'test-user-2', - username: 'TestUser2', - email: 'test2@coder.com', - created_at: '', - status: 'active', - organization_ids: [MockOrganization.id], - roles: [], - avatar_url: '', - last_seen_at: '2022-09-14T19:12:21Z', - login_type: 'oidc', - theme_preference: '', - name: 'Mock User The Second', -}; - -export const SuspendedMockUser: TypesGen.User = { - id: 'suspended-mock-user', - username: 'SuspendedMockUser', - email: 'iamsuspendedsad!@coder.com', - created_at: '', - status: 'suspended', - organization_ids: [MockOrganization.id], - roles: [], - avatar_url: '', - last_seen_at: '', - login_type: 'password', - theme_preference: '', - name: '', -}; - -export const MockProvisioner: TypesGen.ProvisionerDaemon = { - created_at: '2022-05-17T17:39:01.382927298Z', - id: 'test-provisioner', - name: 'Test Provisioner', - provisioners: ['echo'], - tags: { scope: 'organization' }, - version: 'v2.34.5', - api_version: '1.0', -}; - -export const MockUserProvisioner: TypesGen.ProvisionerDaemon = { - created_at: '2022-05-17T17:39:01.382927298Z', - id: 'test-user-provisioner', - name: 'Test User Provisioner', - provisioners: ['echo'], - tags: { scope: 'user', owner: '12345678-abcd-1234-abcd-1234567890abcd' }, - version: 'v2.34.5', - api_version: '1.0', -}; - -export const MockProvisionerJob: TypesGen.ProvisionerJob = { +const MockProvisionerJob: TypesGen.ProvisionerJob = { created_at: '', id: 'test-provisioner-job', status: 'succeeded', @@ -384,30 +57,17 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = { queue_size: 0, }; -export const MockFailedProvisionerJob: TypesGen.ProvisionerJob = { - ...MockProvisionerJob, - status: 'failed', +const MockProvisioner: TypesGen.ProvisionerDaemon = { + created_at: '2022-05-17T17:39:01.382927298Z', + id: 'test-provisioner', + name: 'Test Provisioner', + provisioners: ['echo'], + tags: { scope: 'organization' }, + version: 'v2.34.5', + api_version: '1.0', }; -export const MockCancelingProvisionerJob: TypesGen.ProvisionerJob = { - ...MockProvisionerJob, - status: 'canceling', -}; -export const MockCanceledProvisionerJob: TypesGen.ProvisionerJob = { - ...MockProvisionerJob, - status: 'canceled', -}; -export const MockRunningProvisionerJob: TypesGen.ProvisionerJob = { - ...MockProvisionerJob, - status: 'running', -}; -export const MockPendingProvisionerJob: TypesGen.ProvisionerJob = { - ...MockProvisionerJob, - status: 'pending', - queue_position: 2, - queue_size: 4, -}; -export const MockTemplateVersion: TypesGen.TemplateVersion = { +const MockTemplateVersion: TypesGen.TemplateVersion = { id: 'test-template-version', created_at: '2022-05-17T17:39:01.382927298Z', updated_at: '2022-05-17T17:39:01.382927298Z', @@ -426,129 +86,25 @@ You can add instructions here archived: false, }; -export const MockTemplateVersion2: TypesGen.TemplateVersion = { - id: 'test-template-version-2', - created_at: '2022-05-17T17:39:01.382927298Z', - updated_at: '2022-05-17T17:39:01.382927298Z', - template_id: 'test-template', - job: MockProvisionerJob, - name: 'test-version-2', - message: 'first version', - readme: `--- -name:Template test 2 ---- -## Instructions -You can add instructions here - -[Some link info](https://coder.com)`, - created_by: MockUser, - archived: false, -}; - -export const MockTemplateVersionWithMarkdownMessage: TypesGen.TemplateVersion = - { - ...MockTemplateVersion, - message: ` -# Abiding Grace -## Enchantment -At the beginning of your end step, choose one — - -- You gain 1 life. - -- Return target creature card with mana value 1 from your graveyard to the battlefield. -`, - }; - -export const MockTemplate: TypesGen.Template = { - id: 'test-template', - created_at: '2022-05-17T17:39:01.382927298Z', - updated_at: '2022-05-18T17:39:01.382927298Z', - organization_id: MockOrganization.id, - name: 'test-template', - display_name: 'Test Template', - provisioner: MockProvisioner.provisioners[0], - active_version_id: MockTemplateVersion.id, - active_user_count: 1, - build_time_stats: { - start: { - P50: 1000, - P95: 1500, - }, - stop: { - P50: 1000, - P95: 1500, - }, - delete: { - P50: 1000, - P95: 1500, - }, - }, - description: 'This is a test description.', - default_ttl_ms: 24 * 60 * 60 * 1000, - activity_bump_ms: 1 * 60 * 60 * 1000, - autostop_requirement: { - days_of_week: ['sunday'], - weeks: 1, - }, - autostart_requirement: { - days_of_week: [ - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday', - ], - }, - created_by_id: 'test-creator-id', - created_by_name: 'test_creator', - icon: '/icon/code.svg', - allow_user_cancel_workspace_jobs: true, - failure_ttl_ms: 0, - time_til_dormant_ms: 0, - time_til_dormant_autodelete_ms: 0, - allow_user_autostart: true, - allow_user_autostop: true, - require_active_version: false, - deprecated: false, - deprecation_message: '', - max_port_share_level: 'public', +const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { + created_at: '2023-05-04T11:30:41.402072Z', + id: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', + display_name: 'Startup Script', + icon: '', + workspace_agent_id: '', }; -export const MockTemplateVersionFiles: TemplateVersionFiles = { - 'README.md': '# Example\n\nThis is an example template.', - 'main.tf': `// Provides info about the workspace. -data "coder_workspace" "me" {} - -// Provides the startup script used to download -// the agent and communicate with Coder. -resource "coder_agent" "dev" { -os = "linux" -arch = "amd64" -} - -resource "kubernetes_pod" "main" { -// Ensures that the Pod dies when the workspace shuts down! -count = data.coder_workspace.me.start_count -metadata { - name = "dev-\${data.coder_workspace.me.id}" -} -spec { - container { - image = "ubuntu" - command = ["sh", "-c", coder_agent.main.init_script] - env { - name = "CODER_AGENT_TOKEN" - value = coder_agent.main.token - } - } -} -} -`, +const MockBuildInfo: TypesGen.BuildInfoResponse = { + agent_api_version: '1.0', + external_url: 'file:///mock-url', + version: 'v99.999.9999+c9cdf14', + dashboard_url: 'https:///mock-url', + workspace_proxy: false, + upgrade_message: 'My custom upgrade message', + deployment_id: '510d407f-e521-4180-b559-eab4a6d802b8', }; -export const MockWorkspaceApp: TypesGen.WorkspaceApp = { +const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: 'test-app', slug: 'test-app', display_name: 'Test App', @@ -565,15 +121,7 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { }, }; -export const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { - created_at: '2023-05-04T11:30:41.402072Z', - id: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', - display_name: 'Startup Script', - icon: '', - workspace_agent_id: '', -}; - -export const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = { +const MockWorkspaceAgentScript: TypesGen.WorkspaceAgentScript = { log_source_id: MockWorkspaceAgentLogSource.id, cron: '', log_path: '', @@ -624,257 +172,21 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { ], }; -export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-2', - name: 'another-workspace-agent', - status: 'disconnected', - version: '', - latency: {}, - lifecycle_state: 'ready', - health: { - healthy: false, - reason: 'agent is not connected', - }, -}; - -export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-3', - name: 'an-outdated-workspace-agent', - version: 'v99.999.9998+abcdef', - operating_system: 'Windows', - latency: { - ...MockWorkspaceAgent.latency, - Chicago: { - preferred: false, - latency_ms: 95.11, - }, - 'San Francisco': { - preferred: false, - latency_ms: 111.55, - }, - Paris: { - preferred: false, - latency_ms: 221.66, - }, - }, - lifecycle_state: 'ready', -}; - -export const MockWorkspaceAgentDeprecated: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-3', - name: 'an-outdated-workspace-agent', - version: 'v99.999.9998+abcdef', - api_version: '1.99', - operating_system: 'Windows', - latency: { - ...MockWorkspaceAgent.latency, - Chicago: { - preferred: false, - latency_ms: 95.11, - }, - 'San Francisco': { - preferred: false, - latency_ms: 111.55, - }, - Paris: { - preferred: false, - latency_ms: 221.66, - }, - }, - lifecycle_state: 'ready', -}; - -export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-connecting', - name: 'another-workspace-agent', - status: 'connecting', - version: '', - latency: {}, - lifecycle_state: 'created', -}; - -export const MockWorkspaceAgentTimeout: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-timeout', - name: 'a-timed-out-workspace-agent', - status: 'timeout', - version: '', - latency: {}, - lifecycle_state: 'created', - health: { - healthy: false, - reason: 'agent is taking too long to connect', - }, +export const MockWorkspaceResource: TypesGen.WorkspaceResource = { + id: 'test-workspace-resource', + name: 'a-workspace-resource', + agents: [MockWorkspaceAgent], + created_at: '', + job_id: '', + type: 'google_compute_disk', + workspace_transition: 'start', + hide: false, + icon: '', + metadata: [{ key: 'size', value: '32GB', sensitive: false }], + daily_cost: 10, }; -export const MockWorkspaceAgentStarting: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-starting', - name: 'a-starting-workspace-agent', - lifecycle_state: 'starting', -}; - -export const MockWorkspaceAgentReady: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-ready', - name: 'a-ready-workspace-agent', - lifecycle_state: 'ready', -}; - -export const MockWorkspaceAgentStartTimeout: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-start-timeout', - name: 'a-workspace-agent-timed-out-while-running-startup-script', - lifecycle_state: 'start_timeout', -}; - -export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-start-error', - name: 'a-workspace-agent-errored-while-running-startup-script', - lifecycle_state: 'start_error', - health: { - healthy: false, - reason: 'agent startup script failed', - }, -}; - -export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-shutting-down', - name: 'a-shutting-down-workspace-agent', - lifecycle_state: 'shutting_down', - health: { - healthy: false, - reason: 'agent is shutting down', - }, -}; - -export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-shutdown-timeout', - name: 'a-workspace-agent-timed-out-while-running-shutdownup-script', - lifecycle_state: 'shutdown_timeout', - health: { - healthy: false, - reason: 'agent is shutting down', - }, -}; - -export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-shutdown-error', - name: 'a-workspace-agent-errored-while-running-shutdownup-script', - lifecycle_state: 'shutdown_error', - health: { - healthy: false, - reason: 'agent is shutting down', - }, -}; - -export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { - ...MockWorkspaceAgent, - id: 'test-workspace-agent-off', - name: 'a-workspace-agent-is-shut-down', - lifecycle_state: 'off', - health: { - healthy: false, - reason: 'agent is shutting down', - }, -}; - -export const MockWorkspaceResource: TypesGen.WorkspaceResource = { - id: 'test-workspace-resource', - name: 'a-workspace-resource', - agents: [MockWorkspaceAgent], - created_at: '', - job_id: '', - type: 'google_compute_disk', - workspace_transition: 'start', - hide: false, - icon: '', - metadata: [{ key: 'size', value: '32GB', sensitive: false }], - daily_cost: 10, -}; - -export const MockWorkspaceResourceSensitive: TypesGen.WorkspaceResource = { - ...MockWorkspaceResource, - id: 'test-workspace-resource-sensitive', - name: 'workspace-resource-sensitive', - metadata: [{ key: 'api_key', value: '12345678', sensitive: true }], -}; - -export const MockWorkspaceResourceMultipleAgents: TypesGen.WorkspaceResource = { - ...MockWorkspaceResource, - id: 'test-workspace-resource-multiple-agents', - name: 'workspace-resource-multiple-agents', - agents: [ - MockWorkspaceAgent, - MockWorkspaceAgentDisconnected, - MockWorkspaceAgentOutdated, - ], -}; - -export const MockWorkspaceResourceHidden: TypesGen.WorkspaceResource = { - ...MockWorkspaceResource, - id: 'test-workspace-resource-hidden', - name: 'workspace-resource-hidden', - hide: true, -}; - -export const MockWorkspaceVolumeResource: TypesGen.WorkspaceResource = { - id: 'test-workspace-volume-resource', - created_at: '', - job_id: '', - workspace_transition: 'start', - type: 'docker_volume', - name: 'home_volume', - hide: false, - icon: '', - daily_cost: 0, -}; - -export const MockWorkspaceImageResource: TypesGen.WorkspaceResource = { - id: 'test-workspace-image-resource', - created_at: '', - job_id: '', - workspace_transition: 'start', - type: 'docker_image', - name: 'main', - hide: false, - icon: '', - daily_cost: 0, -}; - -export const MockWorkspaceContainerResource: TypesGen.WorkspaceResource = { - id: 'test-workspace-container-resource', - created_at: '', - job_id: '', - workspace_transition: 'start', - type: 'docker_container', - name: 'workspace', - hide: false, - icon: '', - daily_cost: 0, -}; - -export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = - { - schedule: '', - }; - -export const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = - { - // Runs at 9:30am Monday through Friday using Canada/Eastern - // (America/Toronto) time - schedule: 'CRON_TZ=Canada/Eastern 30 9 * * 1-5', - }; - -export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { +const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { build_number: 1, created_at: '2022-05-17T17:39:01.382927298Z', id: '1', @@ -897,96 +209,69 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { daily_cost: 20, }; -export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { - build_number: 1, - created_at: '2022-05-17T17:39:01.382927298Z', - id: '1', - initiator_id: MockUser.id, - initiator_name: MockUser.username, - job: MockProvisionerJob, - template_version_id: MockTemplateVersion.id, - template_version_name: MockTemplateVersion.name, - transition: 'start', - updated_at: '2022-05-17T17:39:01.382927298Z', - workspace_name: 'test-workspace', - workspace_owner_id: MockUser.id, - workspace_owner_name: MockUser.username, - workspace_owner_avatar_url: MockUser.avatar_url, - workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', - deadline: '2022-05-17T23:39:00.00Z', - reason: 'autostart', - resources: [MockWorkspaceResource], - status: 'running', - daily_cost: 20, -}; - -export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { - build_number: 1, - created_at: '2022-05-17T17:39:01.382927298Z', - id: '1', - initiator_id: MockUser.id, - initiator_name: MockUser.username, - job: MockProvisionerJob, - template_version_id: MockTemplateVersion.id, - template_version_name: MockTemplateVersion.name, - transition: 'start', - updated_at: '2022-05-17T17:39:01.382927298Z', - workspace_name: 'test-workspace', - workspace_owner_id: MockUser.id, - workspace_owner_name: MockUser.username, - workspace_owner_avatar_url: MockUser.avatar_url, - workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', - deadline: '2022-05-17T23:39:00.00Z', - reason: 'autostop', - resources: [MockWorkspaceResource], - status: 'running', - daily_cost: 20, -}; - -export const MockFailedWorkspaceBuild = ( - transition: TypesGen.WorkspaceTransition = 'start', -): TypesGen.WorkspaceBuild => ({ - build_number: 1, +const MockTemplate: TypesGen.Template = { + id: 'test-template', created_at: '2022-05-17T17:39:01.382927298Z', - id: '1', - initiator_id: MockUser.id, - initiator_name: MockUser.username, - job: MockFailedProvisionerJob, - template_version_id: MockTemplateVersion.id, - template_version_name: MockTemplateVersion.name, - transition: transition, - updated_at: '2022-05-17T17:39:01.382927298Z', - workspace_name: 'test-workspace', - workspace_owner_id: MockUser.id, - workspace_owner_name: MockUser.username, - workspace_owner_avatar_url: MockUser.avatar_url, - workspace_id: '759f1d46-3174-453d-aa60-980a9c1442f3', - deadline: '2022-05-17T23:39:00.00Z', - reason: 'initiator', - resources: [], - status: 'failed', - daily_cost: 20, -}); - -export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = { - ...MockWorkspaceBuild, - id: '2', - transition: 'stop', -}; - -export const MockWorkspaceBuildDelete: TypesGen.WorkspaceBuild = { - ...MockWorkspaceBuild, - id: '3', - transition: 'delete', + updated_at: '2022-05-18T17:39:01.382927298Z', + organization_id: MockOrganization.id, + name: 'test-template', + display_name: 'Test Template', + provisioner: MockProvisioner.provisioners[0], + active_version_id: MockTemplateVersion.id, + active_user_count: 1, + build_time_stats: { + start: { + P50: 1000, + P95: 1500, + }, + stop: { + P50: 1000, + P95: 1500, + }, + delete: { + P50: 1000, + P95: 1500, + }, + }, + description: 'This is a test description.', + default_ttl_ms: 24 * 60 * 60 * 1000, + activity_bump_ms: 1 * 60 * 60 * 1000, + autostop_requirement: { + days_of_week: ['sunday'], + weeks: 1, + }, + autostart_requirement: { + days_of_week: [ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ], + }, + created_by_id: 'test-creator-id', + created_by_name: 'test_creator', + icon: '/icon/code.svg', + allow_user_cancel_workspace_jobs: true, + failure_ttl_ms: 0, + time_til_dormant_ms: 0, + time_til_dormant_autodelete_ms: 0, + allow_user_autostart: true, + allow_user_autostop: true, + require_active_version: false, + deprecated: false, + deprecation_message: '', + max_port_share_level: 'public', }; -export const MockBuilds = [ - { ...MockWorkspaceBuild, id: '1' }, - { ...MockWorkspaceBuildAutostart, id: '2' }, - { ...MockWorkspaceBuildAutostop, id: '3' }, - { ...MockWorkspaceBuildStop, id: '4' }, - { ...MockWorkspaceBuildDelete, id: '5' }, -]; +const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = + { + // Runs at 9:30am Monday through Friday using Canada/Eastern + // (America/Toronto) time + schedule: 'CRON_TZ=Canada/Eastern 30 9 * * 1-5', + }; export const MockWorkspace: TypesGen.Workspace = { id: 'test-workspace', @@ -1018,2423 +303,3 @@ export const MockWorkspace: TypesGen.Workspace = { allow_renames: true, favorite: false, }; - -export const MockFavoriteWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-favorite-workspace', - favorite: true, -}; - -export const MockStoppedWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-stopped-workspace', - latest_build: { ...MockWorkspaceBuildStop, status: 'stopped' }, -}; -export const MockStoppingWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-stopping-workspace', - latest_build: { - ...MockWorkspaceBuildStop, - job: MockRunningProvisionerJob, - status: 'stopping', - }, -}; -export const MockStartingWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-starting-workspace', - latest_build: { - ...MockWorkspaceBuild, - job: MockRunningProvisionerJob, - transition: 'start', - status: 'starting', - }, -}; -export const MockCancelingWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-canceling-workspace', - latest_build: { - ...MockWorkspaceBuild, - job: MockCancelingProvisionerJob, - status: 'canceling', - }, -}; -export const MockCanceledWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-canceled-workspace', - latest_build: { - ...MockWorkspaceBuild, - job: MockCanceledProvisionerJob, - status: 'canceled', - }, -}; -export const MockFailedWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-failed-workspace', - latest_build: { - ...MockWorkspaceBuild, - job: MockFailedProvisionerJob, - status: 'failed', - }, -}; -export const MockDeletingWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-deleting-workspace', - latest_build: { - ...MockWorkspaceBuildDelete, - job: MockRunningProvisionerJob, - status: 'deleting', - }, -}; - -export const MockWorkspaceWithDeletion = { - ...MockStoppedWorkspace, - deleting_at: new Date().toISOString(), -}; - -export const MockDeletedWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-deleted-workspace', - latest_build: { ...MockWorkspaceBuildDelete, status: 'deleted' }, -}; - -export const MockOutdatedWorkspace: TypesGen.Workspace = { - ...MockFailedWorkspace, - id: 'test-outdated-workspace', - outdated: true, -}; - -export const MockRunningOutdatedWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-running-outdated-workspace', - outdated: true, -}; - -export const MockDormantWorkspace: TypesGen.Workspace = { - ...MockStoppedWorkspace, - id: 'test-dormant-workspace', - dormant_at: new Date().toISOString(), -}; - -export const MockDormantOutdatedWorkspace: TypesGen.Workspace = { - ...MockStoppedWorkspace, - id: 'test-dormant-outdated-workspace', - name: 'Dormant-Workspace', - outdated: true, - dormant_at: new Date().toISOString(), -}; - -export const MockOutdatedRunningWorkspaceRequireActiveVersion: TypesGen.Workspace = - { - ...MockWorkspace, - id: 'test-outdated-workspace-require-active-version', - outdated: true, - template_require_active_version: true, - latest_build: { - ...MockWorkspaceBuild, - status: 'running', - }, - }; - -export const MockOutdatedRunningWorkspaceAlwaysUpdate: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-outdated-workspace-always-update', - outdated: true, - automatic_updates: 'always', - latest_build: { - ...MockWorkspaceBuild, - status: 'running', - }, -}; - -export const MockOutdatedStoppedWorkspaceRequireActiveVersion: TypesGen.Workspace = - { - ...MockOutdatedRunningWorkspaceRequireActiveVersion, - latest_build: { - ...MockWorkspaceBuild, - status: 'stopped', - }, - }; - -export const MockOutdatedStoppedWorkspaceAlwaysUpdate: TypesGen.Workspace = { - ...MockOutdatedRunningWorkspaceAlwaysUpdate, - latest_build: { - ...MockWorkspaceBuild, - status: 'stopped', - }, -}; - -export const MockPendingWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: 'test-pending-workspace', - latest_build: { - ...MockWorkspaceBuild, - job: MockPendingProvisionerJob, - transition: 'start', - status: 'pending', - }, -}; - -// just over one page of workspaces -export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { - workspaces: range(1, 27).map((id: number) => ({ - ...MockWorkspace, - id: id.toString(), - name: `${MockWorkspace.name}${id}`, - })), - count: 26, -}; - -export const MockWorkspacesResponseWithDeletions = { - workspaces: [...MockWorkspacesResponse.workspaces, MockWorkspaceWithDeletion], - count: MockWorkspacesResponse.count + 1, -}; - -export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = - { - name: 'first_parameter', - type: 'string', - description: 'This is first parameter', - description_plaintext: 'Markdown: This is first parameter', - default_value: 'abc', - mutable: true, - icon: '/icon/folder.svg', - options: [], - required: true, - ephemeral: false, - }; - -export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = - { - name: 'second_parameter', - type: 'number', - description: 'This is second parameter', - description_plaintext: 'Markdown: This is second parameter', - default_value: '2', - mutable: true, - icon: '/icon/folder.svg', - options: [], - validation_min: 1, - validation_max: 3, - validation_monotonic: 'increasing', - required: true, - ephemeral: false, - }; - -export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = - { - name: 'third_parameter', - type: 'string', - description: 'This is third parameter', - description_plaintext: 'Markdown: This is third parameter', - default_value: 'aaa', - mutable: true, - icon: '/icon/database.svg', - options: [], - validation_error: 'No way!', - validation_regex: '^[a-z]{3}$', - required: true, - ephemeral: false, - }; - -export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter = - { - name: 'fourth_parameter', - type: 'string', - description: 'This is fourth parameter', - description_plaintext: 'Markdown: This is fourth parameter', - default_value: 'def', - mutable: false, - icon: '/icon/database.svg', - options: [], - required: true, - ephemeral: false, - }; - -export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = - { - name: 'fifth_parameter', - type: 'number', - description: 'This is fifth parameter', - description_plaintext: 'Markdown: This is fifth parameter', - default_value: '5', - mutable: true, - icon: '/icon/folder.svg', - options: [], - validation_min: 1, - validation_max: 10, - validation_monotonic: 'decreasing', - required: true, - ephemeral: false, - }; - -export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = { - name: 'first_variable', - description: 'This is first variable.', - type: 'string', - value: '', - default_value: 'abc', - required: false, - sensitive: false, -}; - -export const MockTemplateVersionVariable2: TypesGen.TemplateVersionVariable = { - name: 'second_variable', - description: 'This is second variable.', - type: 'number', - value: '5', - default_value: '3', - required: false, - sensitive: false, -}; - -export const MockTemplateVersionVariable3: TypesGen.TemplateVersionVariable = { - name: 'third_variable', - description: 'This is third variable.', - type: 'bool', - value: '', - default_value: 'false', - required: false, - sensitive: false, -}; - -export const MockTemplateVersionVariable4: TypesGen.TemplateVersionVariable = { - name: 'fourth_variable', - description: 'This is fourth variable.', - type: 'string', - value: 'defghijk', - default_value: '', - required: true, - sensitive: true, -}; - -export const MockTemplateVersionVariable5: TypesGen.TemplateVersionVariable = { - name: 'fifth_variable', - description: 'This is fifth variable.', - type: 'string', - value: '', - default_value: '', - required: true, - sensitive: false, -}; - -export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { - name: 'test', - template_version_id: 'test-template-version', - rich_parameter_values: [], -}; - -export const MockWorkspaceRichParametersRequest: TypesGen.CreateWorkspaceRequest = - { - name: 'test', - template_version_id: 'test-template-version', - rich_parameter_values: [ - { - name: MockTemplateVersionParameter1.name, - value: MockTemplateVersionParameter1.default_value, - }, - ], - }; - -export const MockUserAgent = { - browser: 'Chrome 99.0.4844', - device: 'Other', - ip_address: '11.22.33.44', - os: 'Windows 10', -}; - -export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = { - password: { enabled: true }, - github: { enabled: false }, - oidc: { enabled: false, signInText: '', iconUrl: '' }, -}; - -export const MockAuthMethodsPasswordTermsOfService: TypesGen.AuthMethods = { - terms_of_service_url: 'https://www.youtube.com/watch?v=C2f37Vb2NAE', - password: { enabled: true }, - github: { enabled: false }, - oidc: { enabled: false, signInText: '', iconUrl: '' }, -}; - -export const MockAuthMethodsExternal: TypesGen.AuthMethods = { - password: { enabled: false }, - github: { enabled: true }, - oidc: { - enabled: true, - signInText: 'Google', - iconUrl: '/icon/google.svg', - }, -}; - -export const MockAuthMethodsAll: TypesGen.AuthMethods = { - password: { enabled: true }, - github: { enabled: true }, - oidc: { - enabled: true, - signInText: 'Google', - iconUrl: '/icon/google.svg', - }, -}; - -export const MockGitSSHKey: TypesGen.GitSSHKey = { - user_id: '1fa0200f-7331-4524-a364-35770666caa7', - created_at: '2022-05-16T14:30:34.148205897Z', - updated_at: '2022-05-16T15:29:10.302441433Z', - public_key: - 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJOQRIM7kE30rOzrfy+/+R+nQGCk7S9pioihy+2ARbq', -}; - -export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ - { - id: 1, - created_at: '2022-05-19T16:45:31.005Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Setting up', - output: '', - }, - { - id: 2, - created_at: '2022-05-19T16:45:31.006Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Starting workspace', - output: '', - }, - { - id: 3, - created_at: '2022-05-19T16:45:31.072Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '', - }, - { - id: 4, - created_at: '2022-05-19T16:45:31.073Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: 'Initializing the backend...', - }, - { - id: 5, - created_at: '2022-05-19T16:45:31.077Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '', - }, - { - id: 6, - created_at: '2022-05-19T16:45:31.078Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: 'Initializing provider plugins...', - }, - { - id: 7, - created_at: '2022-05-19T16:45:31.078Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '- Finding hashicorp/google versions matching "~\u003e 4.15"...', - }, - { - id: 8, - created_at: '2022-05-19T16:45:31.123Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '- Finding coder/coder versions matching "0.3.4"...', - }, - { - id: 9, - created_at: '2022-05-19T16:45:31.137Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '- Using hashicorp/google v4.21.0 from the shared cache directory', - }, - { - id: 10, - created_at: '2022-05-19T16:45:31.344Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '- Using coder/coder v0.3.4 from the shared cache directory', - }, - { - id: 11, - created_at: '2022-05-19T16:45:31.388Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '', - }, - { - id: 12, - created_at: '2022-05-19T16:45:31.388Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: - 'Terraform has created a lock file .terraform.lock.hcl to record the provider', - }, - { - id: 13, - created_at: '2022-05-19T16:45:31.389Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: - 'selections it made above. Include this file in your version control repository', - }, - { - id: 14, - created_at: '2022-05-19T16:45:31.389Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: - 'so that Terraform can guarantee to make the same selections by default when', - }, - { - id: 15, - created_at: '2022-05-19T16:45:31.39Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: 'you run "terraform init" in the future.', - }, - { - id: 16, - created_at: '2022-05-19T16:45:31.39Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: '', - }, - { - id: 17, - created_at: '2022-05-19T16:45:31.391Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Starting workspace', - output: 'Terraform has been successfully initialized!', - }, - { - id: 18, - created_at: '2022-05-19T16:45:31.42Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'Terraform 1.1.9', - }, - { - id: 19, - created_at: '2022-05-19T16:45:33.537Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'coder_agent.dev: Plan to create', - }, - { - id: 20, - created_at: '2022-05-19T16:45:33.537Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_disk.root: Plan to create', - }, - { - id: 21, - created_at: '2022-05-19T16:45:33.538Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_instance.dev[0]: Plan to create', - }, - { - id: 22, - created_at: '2022-05-19T16:45:33.539Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'Plan: 3 to add, 0 to change, 0 to destroy.', - }, - { - id: 23, - created_at: '2022-05-19T16:45:33.712Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'coder_agent.dev: Creating...', - }, - { - id: 24, - created_at: '2022-05-19T16:45:33.719Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: - 'coder_agent.dev: Creation complete after 0s [id=d07f5bdc-4a8d-4919-9cdb-0ac6ba9e64d6]', - }, - { - id: 25, - created_at: '2022-05-19T16:45:34.139Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_disk.root: Creating...', - }, - { - id: 26, - created_at: '2022-05-19T16:45:44.14Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_disk.root: Still creating... [10s elapsed]', - }, - { - id: 27, - created_at: '2022-05-19T16:45:47.106Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: - 'google_compute_disk.root: Creation complete after 13s [id=projects/bruno-coder-v2/zones/europe-west4-b/disks/coder-developer-bruno-dev-123-root]', - }, - { - id: 28, - created_at: '2022-05-19T16:45:47.118Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_instance.dev[0]: Creating...', - }, - { - id: 29, - created_at: '2022-05-19T16:45:57.122Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'google_compute_instance.dev[0]: Still creating... [10s elapsed]', - }, - { - id: 30, - created_at: '2022-05-19T16:46:00.837Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: - 'google_compute_instance.dev[0]: Creation complete after 14s [id=projects/bruno-coder-v2/zones/europe-west4-b/instances/coder-developer-bruno-dev-123]', - }, - { - id: 31, - created_at: '2022-05-19T16:46:00.846Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'Apply complete! Resources: 3 added, 0 changed, 0 destroyed.', - }, - { - id: 32, - created_at: '2022-05-19T16:46:00.847Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Starting workspace', - output: 'Outputs: 0', - }, - { - id: 33, - created_at: '2022-05-19T16:46:02.283Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Cleaning Up', - output: '', - }, -]; - -export const MockWorkspaceExtendedBuildLogs: TypesGen.ProvisionerJobLog[] = [ - { - id: 938494, - created_at: '2023-08-25T19:07:43.331Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Setting up', - output: '', - }, - { - id: 938495, - created_at: '2023-08-25T19:07:43.331Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Parsing template parameters', - output: '', - }, - { - id: 938496, - created_at: '2023-08-25T19:07:43.339Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Detecting persistent resources', - output: '', - }, - { - id: 938497, - created_at: '2023-08-25T19:07:44.15Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'Initializing the backend...', - }, - { - id: 938498, - created_at: '2023-08-25T19:07:44.215Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'Initializing provider plugins...', - }, - { - id: 938499, - created_at: '2023-08-25T19:07:44.216Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: '- Finding coder/coder versions matching "~> 0.11.0"...', - }, - { - id: 938500, - created_at: '2023-08-25T19:07:44.668Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: '- Finding kreuzwerker/docker versions matching "~> 3.0.1"...', - }, - { - id: 938501, - created_at: '2023-08-25T19:07:44.722Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: '- Using coder/coder v0.11.1 from the shared cache directory', - }, - { - id: 938502, - created_at: '2023-08-25T19:07:44.857Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: '- Using kreuzwerker/docker v3.0.2 from the shared cache directory', - }, - { - id: 938503, - created_at: '2023-08-25T19:07:45.081Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'Terraform has created a lock file .terraform.lock.hcl to record the provider', - }, - { - id: 938504, - created_at: '2023-08-25T19:07:45.081Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'selections it made above. Include this file in your version control repository', - }, - { - id: 938505, - created_at: '2023-08-25T19:07:45.081Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'so that Terraform can guarantee to make the same selections by default when', - }, - { - id: 938506, - created_at: '2023-08-25T19:07:45.082Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'you run "terraform init" in the future.', - }, - { - id: 938507, - created_at: '2023-08-25T19:07:45.083Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'Terraform has been successfully initialized!', - }, - { - id: 938508, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'You may now begin working with Terraform. Try running "terraform plan" to see', - }, - { - id: 938509, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'any changes that are required for your infrastructure. All Terraform commands', - }, - { - id: 938510, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'should now work.', - }, - { - id: 938511, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'If you ever set or change modules or backend configuration for Terraform,', - }, - { - id: 938512, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: - 'rerun this command to reinitialize your working directory. If you forget, other', - }, - { - id: 938513, - created_at: '2023-08-25T19:07:45.084Z', - log_source: 'provisioner', - log_level: 'debug', - stage: 'Detecting persistent resources', - output: 'commands will detect it and remind you to do so if necessary.', - }, - { - id: 938514, - created_at: '2023-08-25T19:07:45.143Z', - log_source: 'provisioner', - log_level: 'info', - stage: 'Detecting persistent resources', - output: 'Terraform 1.1.9', - }, - { - id: 938515, - created_at: '2023-08-25T19:07:46.297Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: 'Warning: Argument is deprecated', - }, - { - id: 938516, - created_at: '2023-08-25T19:07:46.297Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', - }, - { - id: 938517, - created_at: '2023-08-25T19:07:46.297Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: ' 15: feature_use_managed_variables = true', - }, - { - id: 938518, - created_at: '2023-08-25T19:07:46.297Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: '', - }, - { - id: 938519, - created_at: '2023-08-25T19:07:46.297Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: - 'Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.', - }, - { - id: 938520, - created_at: '2023-08-25T19:07:46.3Z', - log_source: 'provisioner', - log_level: 'error', - stage: 'Detecting persistent resources', - output: 'Error: ephemeral parameter requires the default property', - }, - { - id: 938521, - created_at: '2023-08-25T19:07:46.3Z', - log_source: 'provisioner', - log_level: 'error', - stage: 'Detecting persistent resources', - output: - 'on devcontainer-on-docker.tf line 27, in data "coder_parameter" "another_one":', - }, - { - id: 938522, - created_at: '2023-08-25T19:07:46.3Z', - log_source: 'provisioner', - log_level: 'error', - stage: 'Detecting persistent resources', - output: ' 27: data "coder_parameter" "another_one" {', - }, - { - id: 938523, - created_at: '2023-08-25T19:07:46.301Z', - log_source: 'provisioner', - log_level: 'error', - stage: 'Detecting persistent resources', - output: '', - }, - { - id: 938524, - created_at: '2023-08-25T19:07:46.301Z', - log_source: 'provisioner', - log_level: 'error', - stage: 'Detecting persistent resources', - output: '', - }, - { - id: 938525, - created_at: '2023-08-25T19:07:46.303Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: 'Warning: Argument is deprecated', - }, - { - id: 938526, - created_at: '2023-08-25T19:07:46.303Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: 'on devcontainer-on-docker.tf line 15, in provider "coder":', - }, - { - id: 938527, - created_at: '2023-08-25T19:07:46.303Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: ' 15: feature_use_managed_variables = true', - }, - { - id: 938528, - created_at: '2023-08-25T19:07:46.303Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: '', - }, - { - id: 938529, - created_at: '2023-08-25T19:07:46.303Z', - log_source: 'provisioner', - log_level: 'warn', - stage: 'Detecting persistent resources', - output: - 'Terraform variables are now exclusively utilized for template-wide variables after the removal of support for legacy parameters.', - }, - { - id: 938530, - created_at: '2023-08-25T19:07:46.311Z', - log_source: 'provisioner_daemon', - log_level: 'info', - stage: 'Cleaning Up', - output: '', - }, -]; - -export const MockCancellationMessage = { - message: 'Job successfully canceled', -}; - -type MockAPIInput = { - message?: string; - detail?: string; - validations?: FieldError[]; -}; - -type MockAPIOutput = { - isAxiosError: true; - response: { - data: { - message: string; - detail: string | undefined; - validations: FieldError[] | undefined; - }; - }; -}; - -export const mockApiError = ({ - message = 'Something went wrong.', - detail, - validations, -}: MockAPIInput): MockAPIOutput => ({ - // This is how axios can check if it is an axios error when calling isAxiosError - isAxiosError: true, - response: { - data: { - message, - detail, - validations, - }, - }, -}); - -export const MockEntitlements: TypesGen.Entitlements = { - errors: [], - warnings: [], - has_license: false, - features: withDefaultFeatures({ - workspace_batch_actions: { - enabled: true, - entitlement: 'entitled', - }, - }), - require_telemetry: false, - trial: false, - refreshed_at: '2022-05-20T16:45:57.122Z', -}; - -export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { - errors: [], - warnings: ['You are over your active user limit.', 'And another thing.'], - has_license: true, - trial: false, - require_telemetry: false, - refreshed_at: '2022-05-20T16:45:57.122Z', - features: withDefaultFeatures({ - user_limit: { - enabled: true, - entitlement: 'grace_period', - limit: 100, - actual: 102, - }, - audit_log: { - enabled: true, - entitlement: 'entitled', - }, - browser_only: { - enabled: true, - entitlement: 'entitled', - }, - }), -}; - -export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { - errors: [], - warnings: [], - has_license: true, - require_telemetry: false, - trial: false, - refreshed_at: '2022-05-20T16:45:57.122Z', - features: withDefaultFeatures({ - audit_log: { - enabled: true, - entitlement: 'entitled', - }, - }), -}; - -export const MockEntitlementsWithScheduling: TypesGen.Entitlements = { - errors: [], - warnings: [], - has_license: true, - require_telemetry: false, - trial: false, - refreshed_at: '2022-05-20T16:45:57.122Z', - features: withDefaultFeatures({ - advanced_template_scheduling: { - enabled: true, - entitlement: 'entitled', - }, - }), -}; - -export const MockEntitlementsWithUserLimit: TypesGen.Entitlements = { - errors: [], - warnings: [], - has_license: true, - require_telemetry: false, - trial: false, - refreshed_at: '2022-05-20T16:45:57.122Z', - features: withDefaultFeatures({ - user_limit: { - enabled: true, - entitlement: 'entitled', - limit: 25, - }, - }), -}; - -export const MockExperiments: TypesGen.Experiment[] = []; - -export const MockAuditLog: TypesGen.AuditLog = { - id: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', - request_id: '53bded77-7b9d-4e82-8771-991a34d759f9', - time: '2022-05-19T16:45:57.122Z', - organization_id: MockOrganization.id, - ip: '127.0.0.1', - user_agent: - '"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"', - resource_type: 'workspace', - resource_id: 'ef8d1cf4-82de-4fd9-8980-047dad6d06b5', - resource_target: 'bruno-dev', - resource_icon: '', - action: 'create', - diff: { - ttl: { - old: 0, - new: 3600000000000, - secret: false, - }, - }, - status_code: 200, - additional_fields: {}, - description: '{user} created workspace {target}', - user: MockUser, - resource_link: '/@admin/bruno-dev', - is_deleted: false, -}; - -export const MockAuditLog2: TypesGen.AuditLog = { - ...MockAuditLog, - id: '53bded77-7b9d-4e82-8771-991a34d759f9', - action: 'write', - time: '2022-05-20T16:45:57.122Z', - description: '{user} updated workspace {target}', - diff: { - workspace_name: { - old: 'old-workspace-name', - new: MockWorkspace.name, - secret: false, - }, - workspace_auto_off: { - old: true, - new: false, - secret: false, - }, - template_version_id: { - old: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', - new: '53bded77-7b9d-4e82-8771-991a34d759f9', - secret: false, - }, - roles: { - old: null, - new: ['admin', 'auditor'], - secret: false, - }, - }, -}; - -export const MockWorkspaceCreateAuditLogForDifferentOwner = { - ...MockAuditLog, - additional_fields: { - workspace_owner: 'Member', - }, -}; - -export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { - ...MockAuditLog, - id: 'f90995bf-4a2b-4089-b597-e66e025e523e', - request_id: '61555889-2875-475c-8494-f7693dd5d75b', - action: 'stop', - resource_type: 'workspace_build', - description: '{user} stopped build for workspace {target}', - additional_fields: { - workspace_name: 'test2', - }, -}; - -export const MockAuditLogWithDeletedResource: TypesGen.AuditLog = { - ...MockAuditLog, - is_deleted: true, -}; - -export const MockAuditLogGitSSH: TypesGen.AuditLog = { - ...MockAuditLog, - diff: { - private_key: { - old: '', - new: '', - secret: true, - }, - public_key: { - old: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINRUPjBSNtOAnL22+r07OSu9t3Lnm8/5OX8bRHECKS9g\n', - new: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwoUPJPMekuSzMZyV0rA82TGGNzw/Uj/dhLbwiczTpV\n', - secret: false, - }, - }, -}; - -export const MockAuditOauthConvert: TypesGen.AuditLog = { - ...MockAuditLog, - resource_type: 'convert_login', - resource_target: 'oidc', - action: 'create', - status_code: 201, - description: '{user} created login type conversion to {target}}', - diff: { - created_at: { - old: '0001-01-01T00:00:00Z', - new: '2023-06-20T20:44:54.243019Z', - secret: false, - }, - expires_at: { - old: '0001-01-01T00:00:00Z', - new: '2023-06-20T20:49:54.243019Z', - secret: false, - }, - state_string: { - old: '', - new: '', - secret: true, - }, - to_type: { - old: '', - new: 'oidc', - secret: false, - }, - user_id: { - old: '', - new: 'dc790496-eaec-4f88-a53f-8ce1f61a1fff', - secret: false, - }, - }, -}; - -export const MockAuditLogSuccessfulLogin: TypesGen.AuditLog = { - ...MockAuditLog, - resource_type: 'api_key', - resource_target: '', - action: 'login', - status_code: 201, - description: '{user} logged in', -}; - -export const MockAuditLogUnsuccessfulLoginKnownUser: TypesGen.AuditLog = { - ...MockAuditLogSuccessfulLogin, - status_code: 401, -}; - -export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { - credits_consumed: 0, - budget: 100, -}; - -export const MockGroup: TypesGen.Group = { - id: 'fbd2116a-8961-4954-87ae-e4575bd29ce0', - name: 'Front-End', - display_name: 'Front-End', - avatar_url: 'https://example.com', - organization_id: MockOrganization.id, - members: [MockUser, MockUser2], - quota_allowance: 5, - source: 'user', -}; - -const everyOneGroup = (organizationId: string): TypesGen.Group => ({ - id: organizationId, - name: 'Everyone', - display_name: '', - organization_id: organizationId, - members: [], - avatar_url: '', - quota_allowance: 0, - source: 'user', -}); - -export const MockTemplateACL: TypesGen.TemplateACL = { - group: [ - { ...everyOneGroup(MockOrganization.id), role: 'use' }, - { ...MockGroup, role: 'admin' }, - ], - users: [{ ...MockUser, role: 'use' }], -}; - -export const MockTemplateACLEmpty: TypesGen.TemplateACL = { - group: [], - users: [], -}; - -export const MockTemplateExample: TypesGen.TemplateExample = { - id: 'aws-windows', - url: 'https://github.com/coder/coder/tree/main/examples/templates/aws-windows', - name: 'Develop in an ECS-hosted container', - description: 'Get started with Linux development on AWS ECS.', - markdown: - '\n# aws-ecs\n\nThis is a sample template for running a Coder workspace on ECS. It assumes there\nis a pre-existing ECS cluster with EC2-based compute to host the workspace.\n\n## Architecture\n\nThis workspace is built using the following AWS resources:\n\n- Task definition - the container definition, includes the image, command, volume(s)\n- ECS service - manages the task definition\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n', - icon: '/icon/aws.svg', - tags: ['aws', 'cloud'], -}; - -export const MockTemplateExample2: TypesGen.TemplateExample = { - id: 'aws-linux', - url: 'https://github.com/coder/coder/tree/main/examples/templates/aws-linux', - name: 'Develop in Linux on AWS EC2', - description: 'Get started with Linux development on AWS EC2.', - markdown: - '\n# aws-linux\n\nTo get started, run `coder templates init`. When prompted, select this template.\nFollow the on-screen instructions to proceed.\n\n## Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith AWS. For example, run `aws configure import` to import credentials on the\nsystem and user running coderd. For other ways to authenticate [consult the\nTerraform docs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Sid": "VisualEditor0",\n "Effect": "Allow",\n "Action": [\n "ec2:GetDefaultCreditSpecification",\n "ec2:DescribeIamInstanceProfileAssociations",\n "ec2:DescribeTags",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:DescribeInstanceCreditSpecifications",\n "ec2:DescribeImages",\n "ec2:ModifyDefaultCreditSpecification",\n "ec2:DescribeVolumes"\n ],\n "Resource": "*"\n },\n {\n "Sid": "CoderResources",\n "Effect": "Allow",\n "Action": [\n "ec2:DescribeInstances",\n "ec2:DescribeInstanceAttribute",\n "ec2:UnmonitorInstances",\n "ec2:TerminateInstances",\n "ec2:StartInstances",\n "ec2:StopInstances",\n "ec2:DeleteTags",\n "ec2:MonitorInstances",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:ModifyInstanceAttribute",\n "ec2:ModifyInstanceCreditSpecification"\n ],\n "Resource": "arn:aws:ec2:*:*:instance/*",\n "Condition": {\n "StringEquals": {\n "aws:ResourceTag/Coder_Provisioned": "true"\n }\n }\n }\n ]\n}\n```\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n', - icon: '/icon/aws.svg', - tags: ['aws', 'cloud'], -}; - -export const MockPermissions: Permissions = { - createGroup: true, - createTemplates: true, - createUser: true, - deleteTemplates: true, - updateTemplates: true, - readAllUsers: true, - updateUsers: true, - viewAuditLog: true, - viewDeploymentValues: true, - viewUpdateCheck: true, - viewDeploymentStats: true, - viewExternalAuthConfig: true, - editWorkspaceProxies: true, -}; - -export const MockDeploymentConfig: DeploymentConfig = { - config: { - enable_terraform_debug_mode: true, - }, - options: [], -}; - -export const MockAppearanceConfig: TypesGen.AppearanceConfig = { - application_name: '', - logo_url: '', - service_banner: { - enabled: false, - }, - notification_banners: [], -}; - -export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = { - name: MockTemplateVersionParameter1.name, - value: 'mock-abc', -}; - -export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = { - name: MockTemplateVersionParameter2.name, - value: '3', -}; - -export const MockWorkspaceBuildParameter3: TypesGen.WorkspaceBuildParameter = { - name: MockTemplateVersionParameter3.name, - value: 'my-database', -}; - -export const MockWorkspaceBuildParameter4: TypesGen.WorkspaceBuildParameter = { - name: MockTemplateVersionParameter4.name, - value: 'immutable-value', -}; - -export const MockWorkspaceBuildParameter5: TypesGen.WorkspaceBuildParameter = { - name: MockTemplateVersionParameter5.name, - value: '5', -}; - -export const MockTemplateVersionExternalAuthGithub: TypesGen.TemplateVersionExternalAuth = - { - id: 'github', - type: 'github', - authenticate_url: 'https://example.com/external-auth/github', - authenticated: false, - display_icon: '/icon/github.svg', - display_name: 'GitHub', - }; - -export const MockTemplateVersionExternalAuthGithubAuthenticated: TypesGen.TemplateVersionExternalAuth = - { - id: 'github', - type: 'github', - authenticate_url: 'https://example.com/external-auth/github', - authenticated: true, - display_icon: '/icon/github.svg', - display_name: 'GitHub', - }; - -export const MockDeploymentStats: TypesGen.DeploymentStats = { - aggregated_from: '2023-03-06T19:08:55.211625Z', - collected_at: '2023-03-06T19:12:55.211625Z', - next_update_at: '2023-03-06T19:20:55.211625Z', - session_count: { - vscode: 128, - jetbrains: 5, - ssh: 32, - reconnecting_pty: 15, - }, - workspaces: { - building: 15, - failed: 12, - pending: 5, - running: 32, - stopped: 16, - connection_latency_ms: { - P50: 32.56, - P95: 15.23, - }, - rx_bytes: 15613513253, - tx_bytes: 36113513253, - }, -}; - -export const MockDeploymentSSH: TypesGen.SSHConfigResponse = { - hostname_prefix: ' coder.', - ssh_config_options: {}, -}; - -export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [ - { - id: 166663, - created_at: '2023-05-04T11:30:41.402072Z', - output: '+ curl -fsSL https://code-server.dev/install.sh', - level: 'info', - source_id: MockWorkspaceAgentLogSource.id, - }, - { - id: 166664, - created_at: '2023-05-04T11:30:41.40228Z', - output: - '+ sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3', - level: 'info', - source_id: MockWorkspaceAgentLogSource.id, - }, - { - id: 166665, - created_at: '2023-05-04T11:30:42.590731Z', - output: 'Ubuntu 22.04.2 LTS', - level: 'info', - source_id: MockWorkspaceAgentLogSource.id, - }, - { - id: 166666, - created_at: '2023-05-04T11:30:42.593686Z', - output: 'Installing v4.8.3 of the amd64 release from GitHub.', - level: 'info', - source_id: MockWorkspaceAgentLogSource.id, - }, -]; - -export const MockLicenseResponse: GetLicensesResponse[] = [ - { - id: 1, - uploaded_at: '1660104000', - expires_at: '3420244800', // expires on 5/20/2078 - uuid: '1', - claims: { - trial: false, - all_features: true, - version: 1, - features: {}, - license_expires: 3420244800, - }, - }, - { - id: 1, - uploaded_at: '1660104000', - expires_at: '1660104000', // expired on 8/10/2022 - uuid: '1', - claims: { - trial: false, - all_features: true, - version: 1, - features: {}, - license_expires: 1660104000, - }, - }, - { - id: 1, - uploaded_at: '1682346425', - expires_at: '1682346425', // expired on 4/24/2023 - uuid: '1', - claims: { - trial: false, - all_features: true, - version: 1, - features: {}, - license_expires: 1682346425, - }, - }, -]; - -export const MockHealth: TypesGen.HealthcheckReport = { - time: '2023-08-01T16:51:03.29792825Z', - healthy: true, - severity: 'ok', - failing_sections: [], - derp: { - healthy: true, - severity: 'ok', - warnings: [], - dismissed: false, - regions: { - '999': { - healthy: true, - severity: 'ok', - warnings: [], - region: { - EmbeddedRelay: true, - RegionID: 999, - RegionCode: 'coder', - RegionName: 'Council Bluffs, Iowa', - Nodes: [ - { - Name: '999stun0', - RegionID: 999, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - { - Name: '999b', - RegionID: 999, - HostName: 'dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - ], - }, - node_reports: [ - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '999stun0', - RegionID: 999, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: false, - round_trip_ping: '0', - round_trip_ping_ms: 0, - uses_websocket: false, - client_logs: [], - client_errs: [], - stun: { - Enabled: true, - CanSTUN: true, - }, - }, - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '999b', - RegionID: 999, - HostName: 'dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: true, - round_trip_ping: '7674330', - round_trip_ping_ms: 7674330, - uses_websocket: false, - client_logs: [ - [ - 'derphttp.Client.Connect: connecting to https://dev.coder.com/derp', - ], - [ - 'derphttp.Client.Connect: connecting to https://dev.coder.com/derp', - ], - ], - client_errs: [ - ['recv derp message: derphttp.Client closed'], - [ - 'connect to derp: derphttp.Client.Connect connect to : context deadline exceeded: read tcp 10.44.1.150:59546->149.248.214.149:443: use of closed network connection', - 'connect to derp: derphttp.Client closed', - 'connect to derp: derphttp.Client closed', - 'connect to derp: derphttp.Client closed', - 'connect to derp: derphttp.Client closed', - "couldn't connect after 5 tries, last error: couldn't connect after 5 tries, last error: derphttp.Client closed", - ], - ], - stun: { - Enabled: false, - CanSTUN: false, - }, - }, - ], - }, - '10007': { - healthy: true, - severity: 'ok', - warnings: [], - region: { - EmbeddedRelay: false, - RegionID: 10007, - RegionCode: 'coder_sydney', - RegionName: 'sydney', - Nodes: [ - { - Name: '10007stun0', - RegionID: 10007, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - { - Name: '10007a', - RegionID: 10007, - HostName: 'sydney.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - ], - }, - node_reports: [ - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10007stun0', - RegionID: 10007, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: false, - round_trip_ping: '0', - round_trip_ping_ms: 0, - uses_websocket: false, - client_logs: [], - client_errs: [], - stun: { - Enabled: true, - CanSTUN: true, - }, - }, - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10007a', - RegionID: 10007, - HostName: 'sydney.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: true, - round_trip_ping: '170527034', - round_trip_ping_ms: 170527034, - uses_websocket: false, - client_logs: [ - [ - 'derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp', - ], - [ - 'derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp', - ], - ], - client_errs: [[], []], - stun: { - Enabled: false, - CanSTUN: false, - }, - }, - ], - }, - '10008': { - healthy: true, - severity: 'ok', - warnings: [], - region: { - EmbeddedRelay: false, - RegionID: 10008, - RegionCode: 'coder_europe-frankfurt', - RegionName: 'europe-frankfurt', - Nodes: [ - { - Name: '10008stun0', - RegionID: 10008, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - { - Name: '10008a', - RegionID: 10008, - HostName: 'europe.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - ], - }, - node_reports: [ - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10008stun0', - RegionID: 10008, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: false, - round_trip_ping: '0', - round_trip_ping_ms: 0, - uses_websocket: false, - client_logs: [], - client_errs: [], - stun: { - Enabled: true, - CanSTUN: true, - }, - }, - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10008a', - RegionID: 10008, - HostName: 'europe.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: true, - round_trip_ping: '111329690', - round_trip_ping_ms: 111329690, - uses_websocket: false, - client_logs: [ - [ - 'derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp', - ], - [ - 'derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp', - ], - ], - client_errs: [[], []], - stun: { - Enabled: false, - CanSTUN: false, - }, - }, - ], - }, - '10009': { - healthy: true, - severity: 'ok', - warnings: [], - region: { - EmbeddedRelay: false, - RegionID: 10009, - RegionCode: 'coder_brazil-saopaulo', - RegionName: 'brazil-saopaulo', - Nodes: [ - { - Name: '10009stun0', - RegionID: 10009, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - { - Name: '10009a', - RegionID: 10009, - HostName: 'brazil.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - ], - }, - node_reports: [ - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10009stun0', - RegionID: 10009, - HostName: 'stun.l.google.com', - STUNPort: 19302, - STUNOnly: true, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: false, - round_trip_ping: '0', - round_trip_ping_ms: 0, - uses_websocket: false, - client_logs: [], - client_errs: [], - stun: { - Enabled: true, - CanSTUN: true, - }, - }, - { - healthy: true, - severity: 'ok', - warnings: [], - node: { - Name: '10009a', - RegionID: 10009, - HostName: 'brazil.dev.coder.com', - STUNPort: -1, - DERPPort: 443, - }, - node_info: { - TokenBucketBytesPerSecond: 0, - TokenBucketBytesBurst: 0, - }, - can_exchange_messages: true, - round_trip_ping: '138185506', - round_trip_ping_ms: 138185506, - uses_websocket: false, - client_logs: [ - [ - 'derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp', - ], - [ - 'derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp', - ], - ], - client_errs: [[], []], - stun: { - Enabled: false, - CanSTUN: false, - }, - }, - ], - }, - }, - netcheck: { - UDP: true, - IPv6: false, - IPv4: true, - IPv6CanSend: false, - IPv4CanSend: true, - OSHasIPv6: true, - ICMPv4: false, - MappingVariesByDestIP: false, - HairPinning: null, - UPnP: false, - PMP: false, - PCP: false, - PreferredDERP: 999, - RegionLatency: { - '999': 1638180, - '10007': 174853022, - '10008': 112142029, - '10009': 138855606, - }, - RegionV4Latency: { - '999': 1638180, - '10007': 174853022, - '10008': 112142029, - '10009': 138855606, - }, - RegionV6Latency: {}, - GlobalV4: '34.71.26.24:55368', - GlobalV6: '', - CaptivePortal: null, - }, - netcheck_logs: [ - 'netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms', - 'netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms', - 'netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms', - 'netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms', - 'netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms', - 'netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms', - 'netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms', - 'netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms', - 'netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted', - 'netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms', - ], - }, - access_url: { - healthy: true, - severity: 'ok', - warnings: [], - dismissed: false, - access_url: 'https://dev.coder.com', - reachable: true, - status_code: 200, - healthz_response: 'OK', - }, - websocket: { - healthy: true, - severity: 'ok', - warnings: [], - dismissed: false, - body: '', - code: 101, - }, - database: { - healthy: true, - severity: 'ok', - warnings: [], - dismissed: false, - reachable: true, - latency: '92570', - latency_ms: 92570, - threshold_ms: 92570, - }, - workspace_proxy: { - healthy: true, - severity: 'warning', - warnings: [ - { - code: 'EWP04', - message: - 'unhealthy: request to proxy failed: Get "http://127.0.0.1:3001/healthz-report": dial tcp 127.0.0.1:3001: connect: connection refused', - }, - ], - dismissed: false, - error: undefined, - workspace_proxies: { - regions: [ - { - id: '1a3e5eb8-d785-4f7d-9188-2eeab140cd06', - name: 'primary', - display_name: 'Council Bluffs, Iowa', - icon_url: '/emojis/1f3e1.png', - healthy: true, - path_app_url: 'https://dev.coder.com', - wildcard_hostname: '*--apps.dev.coder.com', - derp_enabled: false, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.829032482Z', - }, - created_at: '0001-01-01T00:00:00Z', - updated_at: '0001-01-01T00:00:00Z', - deleted: false, - version: '', - }, - { - id: '2876ab4d-bcee-4643-944f-d86323642840', - name: 'sydney', - display_name: 'Sydney GCP', - icon_url: '/emojis/1f1e6-1f1fa.png', - healthy: true, - path_app_url: 'https://sydney.dev.coder.com', - wildcard_hostname: '*--apps.sydney.dev.coder.com', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-05-01T19:15:56.606593Z', - updated_at: '2023-12-05T14:13:36.647535Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - { - id: '9d786ce0-55b1-4ace-8acc-a4672ff8d41f', - name: 'europe-frankfurt', - display_name: 'Europe GCP (Frankfurt)', - icon_url: '/emojis/1f1e9-1f1ea.png', - healthy: true, - path_app_url: 'https://europe.dev.coder.com', - wildcard_hostname: '*--apps.europe.dev.coder.com', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-05-01T20:34:11.114005Z', - updated_at: '2023-12-05T14:13:45.941716Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - { - id: '2e209786-73b1-4838-ba78-e01c9334450a', - name: 'brazil-saopaulo', - display_name: 'Brazil GCP (Sao Paulo)', - icon_url: '/emojis/1f1e7-1f1f7.png', - healthy: true, - path_app_url: 'https://brazil.dev.coder.com', - wildcard_hostname: '*--apps.brazil.dev.coder.com', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-05-01T20:41:02.76448Z', - updated_at: '2023-12-05T14:13:41.968568Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - { - id: 'c272e80c-0cce-49d6-9782-1b5cf90398e8', - name: 'unregistered', - display_name: 'UnregisteredProxy', - icon_url: '/emojis/274c.png', - healthy: false, - path_app_url: '', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'unregistered', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-07-10T14:51:11.539222Z', - updated_at: '2023-07-10T14:51:11.539223Z', - deleted: false, - version: '', - }, - { - id: 'a3efbff1-587b-4677-80a4-dc4f892fed3e', - name: 'unhealthy', - display_name: 'Unhealthy', - icon_url: '/emojis/1f92e.png', - healthy: false, - path_app_url: 'http://127.0.0.1:3001', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'unreachable', - report: { - errors: [ - 'request to proxy failed: Get "http://127.0.0.1:3001/healthz-report": dial tcp 127.0.0.1:3001: connect: connection refused', - ], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-07-10T14:51:48.407017Z', - updated_at: '2023-07-10T14:51:57.993682Z', - deleted: false, - version: '', - }, - { - id: 'b6cefb69-cb6f-46e2-9c9c-39c089fb7e42', - name: 'paris-coder', - display_name: 'Europe (Paris)', - icon_url: '/emojis/1f1eb-1f1f7.png', - healthy: true, - path_app_url: 'https://paris-coder.fly.dev', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-12-01T09:21:15.996267Z', - updated_at: '2023-12-05T14:13:59.663174Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - { - id: '72649dc9-03c7-46a8-bc95-96775e93ddc1', - name: 'sydney-coder', - display_name: 'Australia (Sydney)', - icon_url: '/emojis/1f1e6-1f1fa.png', - healthy: true, - path_app_url: 'https://sydney-coder.fly.dev', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-12-01T09:23:44.505529Z', - updated_at: '2023-12-05T14:13:55.769058Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - { - id: '1f78398f-e5ae-4c38-aa89-30222181d443', - name: 'sao-paulo-coder', - display_name: 'Brazil (Sau Paulo)', - icon_url: '/emojis/1f1e7-1f1f7.png', - healthy: true, - path_app_url: 'https://sao-paulo-coder.fly.dev', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'ok', - report: { - errors: [], - warnings: [], - }, - checked_at: '2023-12-05T14:14:05.250322277Z', - }, - created_at: '2023-12-01T09:36:00.231252Z', - updated_at: '2023-12-05T14:13:47.015031Z', - deleted: false, - version: 'v2.5.0-devel+5fad61102', - }, - ], - }, - }, - provisioner_daemons: { - severity: 'ok', - warnings: [ - { - message: 'Something is wrong!', - code: 'EUNKNOWN', - }, - { - message: 'This is also bad.', - code: 'EPD01', - }, - ], - dismissed: false, - items: [ - { - provisioner_daemon: { - id: 'e455b582-ac04-4323-9ad6-ab71301fa006', - created_at: '2024-01-04T15:53:03.21563Z', - last_seen_at: '2024-01-04T16:05:03.967551Z', - name: 'ok', - version: 'v2.3.4-devel+abcd1234', - api_version: '1.0', - provisioners: ['echo', 'terraform'], - tags: { - owner: '', - scope: 'organization', - tag_value: 'value', - tag_true: 'true', - tag_1: '1', - tag_yes: 'yes', - }, - }, - warnings: [], - }, - { - provisioner_daemon: { - id: '00000000-0000-0000-000000000000', - created_at: '2024-01-04T15:53:03.21563Z', - last_seen_at: '2024-01-04T16:05:03.967551Z', - name: 'user-scoped', - version: 'v2.34-devel+abcd1234', - api_version: '1.0', - provisioners: ['echo', 'terraform'], - tags: { - owner: '12345678-1234-1234-1234-12345678abcd', - scope: 'user', - tag_VALUE: 'VALUE', - tag_TRUE: 'TRUE', - tag_1: '1', - tag_YES: 'YES', - }, - }, - warnings: [], - }, - { - provisioner_daemon: { - id: 'e455b582-ac04-4323-9ad6-ab71301fa006', - created_at: '2024-01-04T15:53:03.21563Z', - last_seen_at: '2024-01-04T16:05:03.967551Z', - name: 'unhappy', - version: 'v0.0.1', - api_version: '0.1', - provisioners: ['echo', 'terraform'], - tags: { - owner: '', - scope: 'organization', - tag_string: 'value', - tag_false: 'false', - tag_0: '0', - tag_no: 'no', - }, - }, - warnings: [ - { - message: 'Something specific is wrong with this daemon.', - code: 'EUNKNOWN', - }, - { - message: 'And now for something completely different.', - code: 'EUNKNOWN', - }, - ], - }, - ], - }, - coder_version: 'v2.5.0-devel+5fad61102', -}; - -export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = - { - ports: [ - { process_name: 'webb', network: '', port: 30000 }, - { process_name: 'gogo', network: '', port: 8080 }, - { process_name: '', network: '', port: 8081 }, - ], - }; - -export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = { - shares: [ - { - workspace_id: MockWorkspace.id, - agent_name: 'a-workspace-agent', - port: 4000, - share_level: 'authenticated', - protocol: 'http', - }, - { - workspace_id: MockWorkspace.id, - agent_name: 'a-workspace-agent', - port: 65535, - share_level: 'authenticated', - protocol: 'https', - }, - { - workspace_id: MockWorkspace.id, - agent_name: 'a-workspace-agent', - port: 8081, - share_level: 'public', - protocol: 'http', - }, - ], -}; - -export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { - healthy: false, - severity: 'ok', - failing_sections: [], // apparently this property is not used at all? - time: '2023-10-12T23:15:00.000000000Z', - coder_version: 'v2.3.0-devel+8cca4915a', - access_url: { - healthy: true, - severity: 'ok', - warnings: [], - dismissed: false, - access_url: '', - healthz_response: '', - reachable: true, - status_code: 0, - }, - database: { - healthy: false, - severity: 'ok', - warnings: [], - dismissed: false, - latency: '', - latency_ms: 0, - reachable: true, - threshold_ms: 92570, - }, - derp: { - healthy: false, - severity: 'ok', - warnings: [], - dismissed: false, - regions: [], - netcheck_logs: [], - }, - websocket: { - healthy: false, - severity: 'ok', - warnings: [], - dismissed: false, - body: '', - code: 0, - }, - workspace_proxy: { - healthy: false, - error: 'some error', - severity: 'error', - warnings: [], - dismissed: false, - workspace_proxies: { - regions: [ - { - id: 'df7e4b2b-2d40-47e5-a021-e5d08b219c77', - name: 'unhealthy', - display_name: 'unhealthy', - icon_url: '/emojis/1f5fa.png', - healthy: false, - path_app_url: 'http://127.0.0.1:3001', - wildcard_hostname: '', - derp_enabled: true, - derp_only: false, - status: { - status: 'unreachable', - report: { - errors: ['some error'], - warnings: [], - }, - checked_at: '2023-11-24T12:14:05.743303497Z', - }, - created_at: '2023-11-23T15:37:25.513213Z', - updated_at: '2023-11-23T18:09:19.734747Z', - deleted: false, - version: 'v2.5.0-devel+89bae7eff', - }, - ], - }, - }, - provisioner_daemons: { - severity: 'error', - error: 'something went wrong', - warnings: [ - { - message: 'this is a message', - code: 'EUNKNOWN', - }, - ], - dismissed: false, - items: [ - { - provisioner_daemon: { - id: 'e455b582-ac04-4323-9ad6-ab71301fa006', - created_at: '2024-01-04T15:53:03.21563Z', - last_seen_at: '2024-01-04T16:05:03.967551Z', - name: 'vvuurrkk-2', - version: 'v2.6.0-devel+965ad5e96', - api_version: '1.0', - provisioners: ['echo', 'terraform'], - tags: { - owner: '', - scope: 'organization', - }, - }, - warnings: [ - { - message: 'this is a specific message for this thing', - code: 'EUNKNOWN', - }, - ], - }, - ], - }, -}; - -export const MockHealthSettings: TypesGen.HealthSettings = { - dismissed_healthchecks: [], -}; - -export const MockGithubExternalProvider: TypesGen.ExternalAuthLinkProvider = { - id: 'github', - type: 'github', - device: false, - display_icon: '/icon/github.svg', - display_name: 'GitHub', - allow_refresh: true, - allow_validate: true, -}; - -export const MockGithubAuthLink: TypesGen.ExternalAuthLink = { - provider_id: 'github', - created_at: '', - updated_at: '', - has_refresh_token: true, - expires: '', - authenticated: true, - validate_error: '', -}; - -export const MockOAuth2ProviderApps: TypesGen.OAuth2ProviderApp[] = [ - { - id: '1', - name: 'foo', - callback_url: 'http://localhost:3001', - icon: '/icon/github.svg', - endpoints: { - authorization: 'http://localhost:3001/oauth2/authorize', - token: 'http://localhost:3001/oauth2/token', - device_authorization: '', - }, - }, -]; - -export const MockOAuth2ProviderAppSecrets: TypesGen.OAuth2ProviderAppSecret[] = - [ - { - id: '1', - client_secret_truncated: 'foo', - }, - { - id: '1', - last_used_at: '2022-12-16T20:10:45.637452Z', - client_secret_truncated: 'foo', - }, - ]; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts index ea5f46ad..455e0629 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts @@ -1,6 +1,5 @@ -import { User } from '../api/vendoredSdk'; -import type { Workspace } from '../api/vendoredSdk'; -import { MockUser } from './coderEntities'; +import type { User, Workspace } from '../api/vendoredSdk'; +import { MockUser, MockWorkspace } from './coderEntities'; import { mockBackstageApiEndpoint, mockBackstageAssetsEndpoint, @@ -15,11 +14,14 @@ export const mockUserWithProxyUrls: User = { * The main mock for a workspace whose repo URL matches cleanedRepoUrl */ export const mockWorkspaceWithMatch: Workspace = { + ...MockWorkspace, id: 'workspace-with-match', name: 'Test-Workspace', template_icon: `${mockBackstageApiEndpoint}/emojis/dog.svg`, owner_name: 'lil brudder', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-build', status: 'running', resources: [ @@ -38,11 +40,14 @@ export const mockWorkspaceWithMatch: Workspace = { * return multiple values back */ export const mockWorkspaceWithMatch2: Workspace = { + ...MockWorkspace, id: 'workspace-with-match-2', name: 'Another-Test', template_icon: `${mockBackstageApiEndpoint}/emojis/z.svg`, owner_name: 'Coach Z', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-with-match-2-build', status: 'running', resources: [ @@ -59,6 +64,7 @@ export const mockWorkspaceWithMatch2: Workspace = { * cleanedRepoUrl */ export const mockWorkspaceNoMatch: Workspace = { + ...MockWorkspace, id: 'workspace-no-match', name: 'No-match', template_icon: `${mockBackstageApiEndpoint}/emojis/star.svg`, @@ -82,6 +88,7 @@ export const mockWorkspaceNoMatch: Workspace = { * A workspace with no build parameters whatsoever */ export const mockWorkspaceNoParameters: Workspace = { + ...MockWorkspace, id: 'workspace-no-parameters', name: 'No-parameters', template_icon: `${mockBackstageApiEndpoint}/emojis/cheese.png`, diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 1031bffd..441cbc5a 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -82,7 +82,6 @@ export function wrappedGet( export const mockServerEndpoints = { workspaces: `${root}/workspaces`, authenticatedUser: `${root}/users/me`, - workspaceBuildParameters: `${root}/workspacebuilds/:workspaceBuildId/parameters`, } as const satisfies Record; const mainTestHandlers: readonly RestHandler[] = [ From a76db16c57df983c56e09027fc4b1070e869364e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 20:58:55 +0000 Subject: [PATCH 12/22] wip: commit progress on updating client/SDK integration --- plugins/backstage-plugin-coder/src/api/UrlSync.ts | 2 +- .../backstage-plugin-coder/src/testHelpers/mockBackstageData.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.ts index ae05294b..686963ce 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.ts @@ -48,7 +48,7 @@ type UrlPrefixes = Readonly<{ export const defaultUrlPrefixes = { proxyPrefix: `/api/proxy`, - apiRoutePrefix: '/api/v2', + apiRoutePrefix: '', // Left as empty string because code assumes that CoderSdk will add /api/v2 assetsRoutePrefix: '', // Deliberately left as empty string } as const satisfies UrlPrefixes; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 34f11218..bed1f457 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -73,7 +73,7 @@ export const mockBackstageUrlRoot = 'http://localhost:7007'; * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}` as const; + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}/api/v2` as const; /** * The assets endpoint to use during testing. From d22bc2056409436dae05eb358252df241e6e507d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 21:03:26 +0000 Subject: [PATCH 13/22] fix: get all tests passing for CoderClient --- plugins/backstage-plugin-coder/src/api/CoderClient.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts index f807adb7..2bfa6b24 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.test.ts @@ -132,10 +132,10 @@ describe(`${CoderClient.name}`, () => { }); const { urlSync } = apis; - const apiEndpoint = await urlSync.getApiEndpoint(); + const assetsEndpoint = await urlSync.getAssetsEndpoint(); - const allWorkspacesAreRemapped = !workspaces.some(ws => - ws.template_icon.startsWith(apiEndpoint), + const allWorkspacesAreRemapped = workspaces.every(ws => + ws.template_icon.startsWith(assetsEndpoint), ); expect(allWorkspacesAreRemapped).toBe(true); From 08cd04950a9ffc92101c959bb3d019314788341d Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 21:19:56 +0000 Subject: [PATCH 14/22] fix: update UrlSync updates --- .../src/api/UrlSync.test.ts | 8 ++++---- .../src/hooks/useUrlSync.test.tsx | 6 +++--- .../src/testHelpers/mockBackstageData.ts | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts index 4932edea..62001e4e 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.test.ts @@ -4,8 +4,8 @@ import { getMockConfigApi, getMockDiscoveryApi, mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, + mockBackstageApiEndpointWithoutSdkPath, } from '../testHelpers/mockBackstageData'; // Tests have to assume that DiscoveryApi and ConfigApi will always be in sync, @@ -23,7 +23,7 @@ describe(`${UrlSync.name}`, () => { const cachedUrls = urlSync.getCachedUrls(); expect(cachedUrls).toEqual({ baseUrl: mockBackstageUrlRoot, - apiRoute: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, assetsRoute: mockBackstageAssetsEndpoint, }); }); @@ -50,7 +50,7 @@ describe(`${UrlSync.name}`, () => { expect(newSnapshot).toEqual({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', }); }); @@ -76,7 +76,7 @@ describe(`${UrlSync.name}`, () => { expect(onChange).toHaveBeenCalledWith({ baseUrl: 'blah', - apiRoute: 'blah/coder/api/v2', + apiRoute: 'blah/coder', assetsRoute: 'blah/coder', } satisfies UrlSyncSnapshot); diff --git a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx index 164242f7..90cac33d 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx +++ b/plugins/backstage-plugin-coder/src/hooks/useUrlSync.test.tsx @@ -6,13 +6,13 @@ import { type UseUrlSyncResult, useUrlSync } from './useUrlSync'; import type { DiscoveryApi } from '@backstage/core-plugin-api'; import { mockBackstageAssetsEndpoint, - mockBackstageApiEndpoint, mockBackstageUrlRoot, getMockConfigApi, + mockBackstageApiEndpointWithoutSdkPath, } from '../testHelpers/mockBackstageData'; function renderUseUrlSync() { - let proxyEndpoint: string = mockBackstageApiEndpoint; + let proxyEndpoint: string = mockBackstageApiEndpointWithoutSdkPath; const mockDiscoveryApi: DiscoveryApi = { getBaseUrl: async () => proxyEndpoint, }; @@ -53,7 +53,7 @@ describe(`${useUrlSync.name}`, () => { state: { baseUrl: mockBackstageUrlRoot, assetsRoute: mockBackstageAssetsEndpoint, - apiRoute: mockBackstageApiEndpoint, + apiRoute: mockBackstageApiEndpointWithoutSdkPath, }, }), ); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index bed1f457..88f45498 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -67,13 +67,25 @@ export const rawRepoUrl = `${cleanedRepoUrl}/tree/main/`; export const mockBackstageUrlRoot = 'http://localhost:7007'; /** - * The API endpoint to use with the mock server during testing. + * A version of the mock API endpoint that doesn't have the Coder API versioning + * prefix. Mainly used for tests that need to assert that the core API URL is + * formatted correctly, before the CoderSdk adds anything else to the end + * + * The string literal expression is complicated, but hover over it to see what + * the final result is. + */ +export const mockBackstageApiEndpointWithoutSdkPath = + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}` as const; + +/** + * The API endpoint to use with the mock server during testing. Adds additional + * path information that will normally be added via the Coder SDK. * * The string literal expression is complicated, but hover over it to see what * the final result is. */ export const mockBackstageApiEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}/api/v2` as const; + `${mockBackstageApiEndpointWithoutSdkPath}/api/v2` as const; /** * The assets endpoint to use during testing. From 2eb4987e0966af9fd1ec73f0da1e7b2a3b1097d6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 22:01:31 +0000 Subject: [PATCH 15/22] fix: get all tests passing --- plugins/backstage-plugin-coder/src/testHelpers/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backstage-plugin-coder/src/testHelpers/server.ts b/plugins/backstage-plugin-coder/src/testHelpers/server.ts index 441cbc5a..bacd3f43 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/server.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/server.ts @@ -91,7 +91,7 @@ const mainTestHandlers: readonly RestHandler[] = [ `param:"\\w+?=${repoUrl.replace('/', '\\/')}"`, ); - const queryText = String(req.url.searchParams.get('q')); + const queryText = String(req.url.searchParams.get('q') ?? ''); const requestContainsRepoInfo = paramMatcherRe.test(queryText); const baseWorkspaces = requestContainsRepoInfo From 37645f4fda68e9998de6cd50874b418cdb1e92f6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 24 May 2024 22:08:56 +0000 Subject: [PATCH 16/22] chore: update all mock data to use Coder core entity mocks --- .../src/testHelpers/mockCoderPluginData.ts | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts index 455e0629..a3bfb10d 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockCoderPluginData.ts @@ -1,5 +1,10 @@ import type { User, Workspace } from '../api/vendoredSdk'; -import { MockUser, MockWorkspace } from './coderEntities'; +import { + MockUser, + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceResource, +} from './coderEntities'; import { mockBackstageApiEndpoint, mockBackstageAssetsEndpoint, @@ -26,8 +31,15 @@ export const mockWorkspaceWithMatch: Workspace = { status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -52,8 +64,15 @@ export const mockWorkspaceWithMatch2: Workspace = { status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-with-match-2-resource', - agents: [{ id: 'test-workspace-agent', status: 'connected' }], + agents: [ + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent', + status: 'connected', + }, + ], }, ], }, @@ -69,15 +88,26 @@ export const mockWorkspaceNoMatch: Workspace = { name: 'No-match', template_icon: `${mockBackstageApiEndpoint}/emojis/star.svg`, owner_name: 'homestar runner', + latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-match-build', status: 'stopped', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-match-resource', agents: [ - { id: 'test-workspace-agent-a', status: 'disconnected' }, - { id: 'test-workspace-agent-b', status: 'timeout' }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-a', + status: 'disconnected', + }, + { + ...MockWorkspaceAgent, + id: 'test-workspace-agent-b', + status: 'timeout', + }, ], }, ], @@ -94,12 +124,16 @@ export const mockWorkspaceNoParameters: Workspace = { template_icon: `${mockBackstageApiEndpoint}/emojis/cheese.png`, owner_name: 'The Cheat', latest_build: { + ...MockWorkspace.latest_build, id: 'workspace-no-parameters-build', status: 'running', resources: [ { + ...MockWorkspaceResource, id: 'workspace-no-parameters-resource', - agents: [{ id: 'test-workspace-c', status: 'timeout' }], + agents: [ + { ...MockWorkspaceAgent, id: 'test-workspace-c', status: 'timeout' }, + ], }, ], }, From 864357dbb9ff646fad0f50365f434ea7a92da185 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 28 May 2024 14:56:22 +0000 Subject: [PATCH 17/22] fix: add extra helpers to useCoderSdk --- .../src/api/queryOptions.ts | 10 +++--- .../CoderProvider/CoderAuthProvider.tsx | 24 +++++++------ .../src/hooks/useCoderSdk.ts | 36 +++++++++++++++++-- .../src/hooks/useCoderWorkspacesQuery.ts | 6 ++-- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index d15d6ce3..4e55861d 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -44,13 +44,13 @@ function getSharedWorkspacesQueryKey(coderQuery: string) { type WorkspacesFetchInputs = Readonly<{ auth: CoderAuth; - coderSdk: BackstageCoderSdk; + sdk: BackstageCoderSdk; coderQuery: string; }>; export function workspaces({ auth, - coderSdk, + sdk, coderQuery, }: WorkspacesFetchInputs): UseQueryOptions { const enabled = auth.isAuthenticated; @@ -61,7 +61,7 @@ export function workspaces({ keepPreviousData: enabled && coderQuery !== '', refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { - const res = await coderSdk.getWorkspaces({ + const res = await sdk.getWorkspaces({ q: coderQuery, limit: 0, }); @@ -79,7 +79,7 @@ type WorkspacesByRepoFetchInputs = Readonly< export function workspacesByRepo({ coderQuery, - coderSdk, + sdk, auth, workspacesConfig, }: WorkspacesByRepoFetchInputs): UseQueryOptions { @@ -95,7 +95,7 @@ export function workspacesByRepo({ refetchInterval: getCoderWorkspacesRefetchInterval, queryFn: async () => { const request: WorkspacesRequest = { q: coderQuery, limit: 0 }; - const res = await coderSdk.getWorkspacesByRepo(request, workspacesConfig); + const res = await sdk.getWorkspacesByRepo(request, workspacesConfig); return res.workspaces; }, }; diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index c9b6fbb1..664bb311 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -165,19 +165,23 @@ function useAuthState(): CoderAuth { return unsubscribe; }, [queryClient]); + const registerNewToken = useCallback((newToken: string) => { + if (newToken !== '') { + setAuthToken(newToken); + } + }, []); + + const ejectToken = useCallback(() => { + setAuthToken(''); + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); + }, [queryClient]); + return { ...authState, isAuthenticated: validAuthStatuses.includes(authState.status), - registerNewToken: newToken => { - if (newToken !== '') { - setAuthToken(newToken); - } - }, - ejectToken: () => { - setAuthToken(''); - window.localStorage.removeItem(TOKEN_STORAGE_KEY); - queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] }); - }, + registerNewToken, + ejectToken, }; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts index 8fbec12c..f394660c 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts @@ -1,7 +1,37 @@ +/** + * @file This defines the general helper for accessing the Coder SDK from + * Backstage in a type-safe way. + * + * This hook is meant to be used both internally AND externally. It exposes some + * auth helpers to make end users' lives easier, but all of them go through + * useEndUserCoderAuth. If building any internal components, be sure to have a + * call to useInternalCoderAuth somewhere, to make sure that the component + * interfaces with the fallback auth UI inputs properly. + * + * See CoderAuthProvider.tsx for more info. + */ import { useApi } from '@backstage/core-plugin-api'; import { coderClientApiRef, type BackstageCoderSdk } from '../api/CoderClient'; +import { useEndUserCoderAuth } from '../components/CoderProvider'; -export function useCoderSdk(): BackstageCoderSdk { - const coderClient = useApi(coderClientApiRef); - return coderClient.sdk; +type UseCoderSdkResult = Readonly<{ + sdk: BackstageCoderSdk; + backstageUtils: Readonly<{ + unlinkCoderAccount: () => void; + }>; +}>; + +export function useCoderSdk(): UseCoderSdkResult { + const { ejectToken } = useEndUserCoderAuth(); + const { sdk } = useApi(coderClientApiRef); + + return { + sdk, + backstageUtils: { + // Hoping that as we support more auth methods, this function gets beefed + // up to be an all-in-one function for removing any and all auth info. + // Simply doing a pass-through for now + unlinkCoderAccount: ejectToken, + }, + }; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts index 4e41ef86..bea87361 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts @@ -14,12 +14,12 @@ export function useCoderWorkspacesQuery({ workspacesConfig, }: QueryInput) { const auth = useInternalCoderAuth(); - const coderSdk = useCoderSdk(); + const { sdk } = useCoderSdk(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData - ? workspacesByRepo({ auth, coderSdk, coderQuery, workspacesConfig }) - : workspaces({ auth, coderSdk, coderQuery }); + ? workspacesByRepo({ auth, sdk, coderQuery, workspacesConfig }) + : workspaces({ auth, sdk, coderQuery }); return useQuery(queryOptions); } From 977b2ebb829d2e3e99acca16efc9274e42bbd472 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 31 May 2024 15:30:22 +0000 Subject: [PATCH 18/22] fix: add additional properties to hide from SDK --- .../backstage-plugin-coder/src/api/vendoredSdk/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts index b64f8419..f8451116 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/index.ts @@ -18,7 +18,12 @@ type PropertyToHide = | 'setHost' | 'getAvailableExperiments' | 'login' - | 'logout'; + | 'logout' + | 'convertToOAUTH' + | 'waitForBuild' + | 'addMember' + | 'removeMember' + | 'getWorkspaceParameters'; // Wanted to have a CoderSdk class (mainly re-exporting the Api class as itself // with the extra properties omitted). But because classes are wonky and exist From 09240cc78513fa49508b9db8bd9063ee68e75664 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 31 May 2024 17:18:54 +0000 Subject: [PATCH 19/22] fix: shrink down the API of useCoderSdk --- .../src/hooks/useCoderSdk.ts | 30 ++----------------- .../src/hooks/useCoderWorkspacesQuery.ts | 2 +- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts index f394660c..7b7017a1 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderSdk.ts @@ -2,36 +2,12 @@ * @file This defines the general helper for accessing the Coder SDK from * Backstage in a type-safe way. * - * This hook is meant to be used both internally AND externally. It exposes some - * auth helpers to make end users' lives easier, but all of them go through - * useEndUserCoderAuth. If building any internal components, be sure to have a - * call to useInternalCoderAuth somewhere, to make sure that the component - * interfaces with the fallback auth UI inputs properly. - * - * See CoderAuthProvider.tsx for more info. + * This hook is meant to be used both internally AND externally. */ import { useApi } from '@backstage/core-plugin-api'; import { coderClientApiRef, type BackstageCoderSdk } from '../api/CoderClient'; -import { useEndUserCoderAuth } from '../components/CoderProvider'; - -type UseCoderSdkResult = Readonly<{ - sdk: BackstageCoderSdk; - backstageUtils: Readonly<{ - unlinkCoderAccount: () => void; - }>; -}>; -export function useCoderSdk(): UseCoderSdkResult { - const { ejectToken } = useEndUserCoderAuth(); +export function useCoderSdk(): BackstageCoderSdk { const { sdk } = useApi(coderClientApiRef); - - return { - sdk, - backstageUtils: { - // Hoping that as we support more auth methods, this function gets beefed - // up to be an all-in-one function for removing any and all auth info. - // Simply doing a pass-through for now - unlinkCoderAccount: ejectToken, - }, - }; + return sdk; } diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts index bea87361..63b4f2f7 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts @@ -13,8 +13,8 @@ export function useCoderWorkspacesQuery({ coderQuery, workspacesConfig, }: QueryInput) { + const sdk = useCoderSdk(); const auth = useInternalCoderAuth(); - const { sdk } = useCoderSdk(); const hasRepoData = workspacesConfig && workspacesConfig.repoUrl; const queryOptions = hasRepoData From 3a8accb15bb31610cd2e3894ba2e1fe98895c37b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 3 Jun 2024 13:39:54 +0000 Subject: [PATCH 20/22] update method name for clarity --- plugins/backstage-plugin-coder/src/api/CoderClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/CoderClient.ts b/plugins/backstage-plugin-coder/src/api/CoderClient.ts index 30f9b767..4c5333dd 100644 --- a/plugins/backstage-plugin-coder/src/api/CoderClient.ts +++ b/plugins/backstage-plugin-coder/src/api/CoderClient.ts @@ -100,7 +100,7 @@ export class CoderClient implements CoderClientApi { this.cleanupController = new AbortController(); this.trackedEjectionIds = new Set(); - this.sdk = this.getBackstageCoderSdk(); + this.sdk = this.createBackstageCoderSdk(); this.addBaseRequestInterceptors(); } @@ -181,7 +181,7 @@ export class CoderClient implements CoderClientApi { this.addRequestInterceptor(baseRequestInterceptor, baseErrorInterceptor); } - private getBackstageCoderSdk(): BackstageCoderSdk { + private createBackstageCoderSdk(): BackstageCoderSdk { const baseSdk = makeCoderSdk(); const getWorkspaces: (typeof baseSdk)['getWorkspaces'] = async request => { From a67fbcfd198ad8e091311de947fdc64ec8549985 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 3 Jun 2024 13:49:44 +0000 Subject: [PATCH 21/22] chore: removal vestigal endpoint properties --- plugins/backstage-plugin-coder/src/api/UrlSync.ts | 10 ++-------- .../src/testHelpers/mockBackstageData.ts | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/api/UrlSync.ts b/plugins/backstage-plugin-coder/src/api/UrlSync.ts index 686963ce..8b3548d6 100644 --- a/plugins/backstage-plugin-coder/src/api/UrlSync.ts +++ b/plugins/backstage-plugin-coder/src/api/UrlSync.ts @@ -42,14 +42,10 @@ const PROXY_URL_KEY_FOR_DISCOVERY_API = 'proxy'; type UrlPrefixes = Readonly<{ proxyPrefix: string; - apiRoutePrefix: string; - assetsRoutePrefix: string; }>; export const defaultUrlPrefixes = { proxyPrefix: `/api/proxy`, - apiRoutePrefix: '', // Left as empty string because code assumes that CoderSdk will add /api/v2 - assetsRoutePrefix: '', // Deliberately left as empty string } as const satisfies UrlPrefixes; export type UrlSyncSnapshot = Readonly<{ @@ -104,12 +100,10 @@ export class UrlSync implements UrlSyncApi { } private prepareNewSnapshot(newProxyUrl: string): UrlSyncSnapshot { - const { assetsRoutePrefix, apiRoutePrefix } = this.urlPrefixes; - return { baseUrl: newProxyUrl.replace(proxyRouteReplacer, ''), - assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${assetsRoutePrefix}`, - apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}${apiRoutePrefix}`, + assetsRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, + apiRoute: `${newProxyUrl}${CODER_PROXY_PREFIX}`, }; } diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 88f45498..8c96f8d2 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -75,7 +75,7 @@ export const mockBackstageUrlRoot = 'http://localhost:7007'; * the final result is. */ export const mockBackstageApiEndpointWithoutSdkPath = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.apiRoutePrefix}` as const; + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; /** * The API endpoint to use with the mock server during testing. Adds additional @@ -94,7 +94,7 @@ export const mockBackstageApiEndpoint = * the final result is. */ export const mockBackstageAssetsEndpoint = - `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}${defaultUrlPrefixes.assetsRoutePrefix}` as const; + `${mockBackstageUrlRoot}${defaultUrlPrefixes.proxyPrefix}${CODER_PROXY_PREFIX}` as const; export const mockBearerToken = 'This-is-an-opaque-value-by-design'; export const mockCoderAuthToken = 'ZG0HRy2gGN-mXljc1s5FqtE8WUJ4sUc5X'; From 6ef87b7459e0cd1ecd026f382fc08163738a52b4 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 3 Jun 2024 17:35:46 +0000 Subject: [PATCH 22/22] fix: update reversion --- .../src/api/queryOptions.ts | 5 +- .../src/api/vendoredSdk/api/api.ts | 2 +- .../CoderProvider/CoderAuthProvider.tsx | 84 +++++- .../CoderProvider/CoderProvider.test.tsx | 1 + .../CoderProvider/CoderProvider.tsx | 5 +- .../components/CoderWorkspacesCard/Root.tsx | 2 +- .../useCoderWorkspacesQuery.test.ts | 7 +- .../useCoderWorkspacesQuery.ts | 8 +- .../src/hooks/reactQueryWrappers.test.tsx | 248 ++++++++++++++++++ .../src/hooks/reactQueryWrappers.ts | 157 +++++++++++ plugins/backstage-plugin-coder/src/plugin.ts | 6 + .../src/testHelpers/setup.tsx | 23 +- 12 files changed, 522 insertions(+), 26 deletions(-) rename plugins/backstage-plugin-coder/src/{hooks => components/CoderWorkspacesCard}/useCoderWorkspacesQuery.test.ts (91%) rename plugins/backstage-plugin-coder/src/{hooks => components/CoderWorkspacesCard}/useCoderWorkspacesQuery.ts (66%) create mode 100644 plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx create mode 100644 plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts diff --git a/plugins/backstage-plugin-coder/src/api/queryOptions.ts b/plugins/backstage-plugin-coder/src/api/queryOptions.ts index 4e55861d..6bfbd800 100644 --- a/plugins/backstage-plugin-coder/src/api/queryOptions.ts +++ b/plugins/backstage-plugin-coder/src/api/queryOptions.ts @@ -4,7 +4,10 @@ import type { CoderWorkspacesConfig } from '../hooks/useCoderWorkspacesConfig'; import type { BackstageCoderSdk } from './CoderClient'; import type { CoderAuth } from '../components/CoderProvider'; -export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin'; +// Making the type more broad to hide some implementation details from the end +// user; the prefix should be treated as an opaque string we can change whenever +// we want +export const CODER_QUERY_KEY_PREFIX = 'coder-backstage-plugin' as string; // Defined here and not in CoderAuthProvider.ts to avoid circular dependency // issues diff --git a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts index e0eafd1d..bf293267 100644 --- a/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts +++ b/plugins/backstage-plugin-coder/src/api/vendoredSdk/api/api.ts @@ -312,7 +312,7 @@ type RestartWorkspaceParameters = Readonly<{ export type DeleteWorkspaceOptions = Pick< TypesGen.CreateWorkspaceBuildRequest, - 'log_level' & 'orphan' + 'log_level' | 'orphan' >; type Claims = { diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 664bb311..33b5bc0a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -1,4 +1,5 @@ import React, { + type FC, type PropsWithChildren, createContext, useCallback, @@ -136,10 +137,16 @@ function useAuthState(): CoderAuth { return () => window.clearTimeout(distrustTimeoutId); }, [authState.status]); + const isAuthenticated = validAuthStatuses.includes(authState.status); + // Sets up subscription to spy on potentially-expired tokens. Can't do this // outside React because we let the user connect their own queryClient const queryClient = useQueryClient(); useEffect(() => { + if (!isAuthenticated) { + return undefined; + } + // Pseudo-mutex; makes sure that if we get a bunch of errors, only one // revalidation will be processed at a time let isRevalidatingToken = false; @@ -163,7 +170,7 @@ function useAuthState(): CoderAuth { const queryCache = queryClient.getQueryCache(); const unsubscribe = queryCache.subscribe(revalidateTokenOnError); return unsubscribe; - }, [queryClient]); + }, [queryClient, isAuthenticated]); const registerNewToken = useCallback((newToken: string) => { if (newToken !== '') { @@ -179,7 +186,7 @@ function useAuthState(): CoderAuth { return { ...authState, - isAuthenticated: validAuthStatuses.includes(authState.status), + isAuthenticated, registerNewToken, ejectToken, }; @@ -607,24 +614,75 @@ export const dummyTrackComponent: TrackComponent = () => { }; }; +export type FallbackAuthInputBehavior = 'restrained' | 'assertive' | 'hidden'; +type AuthFallbackProvider = FC< + Readonly< + PropsWithChildren<{ + isAuthenticated: boolean; + }> + > +>; + +// Matches each behavior for the fallback auth UI to a specific provider. This +// is screwy code, but by doing this, we ensure that if the user chooses not to +// have a dynamic auth fallback UI, their app will have far less tracking logic, +// meaning less performance overhead and fewer re-renders from something the +// user isn't even using +const fallbackProviders = { + hidden: ({ children }) => ( + + {children} + + ), + + assertive: ({ children, isAuthenticated }) => ( + // Don't need the live version of the tracker function if we're always + // going to be showing the fallback auth input no matter what + + {children} + {!isAuthenticated && } + + ), + + // Have to give function a name to satisfy ES Lint (rules of hooks) + restrained: function Restrained({ children, isAuthenticated }) { + const { hasNoAuthInputs, trackComponent } = useAuthFallbackState(); + const needFallbackUi = !isAuthenticated && hasNoAuthInputs; + + return ( + <> + + {children} + + + {needFallbackUi && ( + + + + )} + + ); + }, +} as const satisfies Record; + +export type CoderAuthProviderProps = Readonly< + PropsWithChildren<{ + fallbackAuthUiMode?: FallbackAuthInputBehavior; + }> +>; + export function CoderAuthProvider({ children, -}: Readonly>) { + fallbackAuthUiMode = 'restrained', +}: CoderAuthProviderProps) { const authState = useAuthState(); - const { hasNoAuthInputs, trackComponent } = useAuthFallbackState(); - const needFallbackUi = !authState.isAuthenticated && hasNoAuthInputs; + const AuthFallbackProvider = fallbackProviders[fallbackAuthUiMode]; return ( - + {children} - - - {needFallbackUi && ( - - - - )} + ); } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 73acc13c..382917d8 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -86,6 +86,7 @@ describe(`${CoderProvider.name}`, () => { {children} diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx index 1b825404..fd562851 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx @@ -46,12 +46,15 @@ export const CoderProvider = ({ children, appConfig, queryClient = defaultClient, + fallbackAuthUiMode = 'restrained', }: CoderProviderProps) => { return ( - {children} + + {children} + diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index 452f0a9c..5814d55b 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -16,7 +16,7 @@ import { type CoderWorkspacesConfig, } from '../../hooks/useCoderWorkspacesConfig'; import type { Workspace } from '../../api/vendoredSdk'; -import { useCoderWorkspacesQuery } from '../../hooks/useCoderWorkspacesQuery'; +import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery'; import { CoderAuthFormCardWrapper } from '../CoderAuthFormCardWrapper'; export type WorkspacesQuery = UseQueryResult; diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts similarity index 91% rename from plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts rename to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts index 49535619..9f22cf94 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.test.ts @@ -1,12 +1,11 @@ import { waitFor } from '@testing-library/react'; import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery'; - -import { renderHookAsCoderEntity } from '../testHelpers/setup'; -import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData'; +import { renderHookAsCoderEntity } from '../../testHelpers/setup'; +import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData'; import { mockWorkspaceNoParameters, mockWorkspacesList, -} from '../testHelpers/mockCoderPluginData'; +} from '../../testHelpers/mockCoderPluginData'; beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts similarity index 66% rename from plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts rename to plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts index 63b4f2f7..5f82e6b7 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/useCoderWorkspacesQuery.ts @@ -1,8 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { workspaces, workspacesByRepo } from '../api/queryOptions'; -import type { CoderWorkspacesConfig } from './useCoderWorkspacesConfig'; -import { useCoderSdk } from './useCoderSdk'; -import { useInternalCoderAuth } from '../components/CoderProvider'; +import { workspaces, workspacesByRepo } from '../../api/queryOptions'; +import type { CoderWorkspacesConfig } from '../../hooks/useCoderWorkspacesConfig'; +import { useCoderSdk } from '../../hooks/useCoderSdk'; +import { useInternalCoderAuth } from '../../components/CoderProvider'; type QueryInput = Readonly<{ coderQuery: string; diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx new file mode 100644 index 00000000..83309a08 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.test.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import type { + QueryClient, + QueryKey, + UseQueryResult, +} from '@tanstack/react-query'; +import { + type UseCoderQueryOptions, + useCoderQuery, + CoderQueryFunction, +} from './reactQueryWrappers'; +import { + type CoderAuth, + CoderProvider, + useEndUserCoderAuth, +} from '../components/CoderProvider'; +import { + getMockApiList, + mockAppConfig, + mockCoderAuthToken, +} from '../testHelpers/mockBackstageData'; +import { + createInvertedPromise, + getMockQueryClient, +} from '../testHelpers/setup'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; +import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { mockWorkspacesList } from '../testHelpers/mockCoderPluginData'; + +type RenderUseQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Readonly<{ + authenticateOnMount?: boolean; + queryClient?: QueryClient; + queryOptions: UseCoderQueryOptions; +}>; + +async function renderCoderQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: RenderUseQueryOptions) { + const { + queryOptions, + authenticateOnMount = true, + queryClient = getMockQueryClient(), + } = options; + + let latestRegisterNewToken!: CoderAuth['registerNewToken']; + let latestEjectToken!: CoderAuth['ejectToken']; + const AuthEscapeHatch = () => { + const auth = useEndUserCoderAuth(); + latestRegisterNewToken = auth.registerNewToken; + latestEjectToken = auth.ejectToken; + + return null; + }; + + type Result = UseQueryResult; + const renderOutput = renderHook( + newOptions => useCoderQuery(newOptions), + { + initialProps: queryOptions, + wrapper: ({ children }) => { + const mainMarkup = ( + + + {children} + + + + ); + + return wrapInTestApp(mainMarkup) as unknown as typeof mainMarkup; + }, + }, + ); + + await waitFor(() => expect(renderOutput.result.current).not.toBeNull()); + + const registerMockToken = () => { + return act(() => latestRegisterNewToken(mockCoderAuthToken)); + }; + + const ejectToken = () => { + return act(() => latestEjectToken()); + }; + + if (authenticateOnMount) { + registerMockToken(); + } + + return { ...renderOutput, registerMockToken, ejectToken }; +} + +describe(`${useCoderQuery.name}`, () => { + /** + * Really wanted to make mock components for each test case, to simulate some + * of the steps of using the hook as an actual end-user, but the setup steps + * got to be a bit much, just because of all the dependencies to juggle. + * + * @todo Add a new describe block with custom components to mirror some + * example user flows + */ + describe('Hook functionality', () => { + it('Disables requests while user is not authenticated', async () => { + const { result, registerMockToken, ejectToken } = await renderCoderQuery({ + authenticateOnMount: false, + queryOptions: { + queryKey: ['workspaces'], + queryFn: ({ sdk }) => sdk.getWorkspaces({ q: 'owner:me' }), + select: response => response.workspaces, + }, + }); + + expect(result.current.isLoading).toBe(true); + registerMockToken(); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.length).toBeGreaterThan(0); + }); + + ejectToken(); + await waitFor(() => expect(result.current.isLoading).toBe(true)); + }); + + it("Automatically prefixes queryKey with the global Coder query key prefix if it isn't already there", async () => { + // Have to escape out the key because useQuery doesn't expose any way to + // access the key after it's been processed into a query result object + let processedQueryKey: QueryKey | undefined = undefined; + + const queryFnWithEscape: CoderQueryFunction = ({ queryKey }) => { + processedQueryKey = queryKey; + return Promise.resolve(mockWorkspacesList); + }; + + // Verify that key is updated if the prefix isn't already there + const { unmount } = await renderCoderQuery({ + queryOptions: { + queryKey: ['blah'], + queryFn: queryFnWithEscape, + }, + }); + + await waitFor(() => { + expect(processedQueryKey).toEqual([ + CODER_QUERY_KEY_PREFIX, + 'blah', + ]); + }); + + // Unmounting shouldn't really be necessary, but it helps guarantee that + // there's never any risks of states messing with each other + unmount(); + + // Verify that the key is unchanged if the prefix is already present + await renderCoderQuery({ + queryOptions: { + queryKey: [CODER_QUERY_KEY_PREFIX, 'nah'], + queryFn: queryFnWithEscape, + }, + }); + + await waitFor(() => { + expect(processedQueryKey).toEqual([ + CODER_QUERY_KEY_PREFIX, + 'nah', + ]); + }); + }); + + it('Disables everything when the user unlinks their access token', async () => { + const { result, ejectToken } = await renderCoderQuery({ + queryOptions: { + queryKey: ['workspaces'], + queryFn: () => Promise.resolve(mockWorkspacesList), + }, + }); + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining>({ + isSuccess: true, + isPaused: false, + data: mockWorkspacesList, + }), + ); + }); + + ejectToken(); + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining>({ + isLoading: true, + isPaused: false, + data: undefined, + }), + ); + }); + }); + + /** + * In case the title isn't clear (had to rewrite it a bunch), the flow is: + * + * 1. User gets authenticated + * 2. User makes a request that will fail + * 3. Before the request comes back, the user revokes their authentication + * 4. The failed request comes back, which would normally add error state, + * and kick off a bunch of retry logic for React Query + * 5. But the hook should tell the Query Client NOT retry the request + * because the user is no longer authenticated + */ + it('Will not retry a request if it gets sent out while the user is authenticated, but then fails after the user revokes authentication', async () => { + const { promise, reject } = createInvertedPromise(); + const queryFn = jest.fn(() => promise); + + const { ejectToken } = await renderCoderQuery({ + queryOptions: { + queryFn, + queryKey: ['blah'], + + // From the end user's perspective, the query should always retry, but + // the hook should override that when the user isn't authenticated + retry: true, + }, + }); + + await waitFor(() => expect(queryFn).toHaveBeenCalled()); + ejectToken(); + + queryFn.mockRestore(); + act(() => reject(new Error("Don't feel like giving you data today"))); + expect(queryFn).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts new file mode 100644 index 00000000..6dff0240 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/hooks/reactQueryWrappers.ts @@ -0,0 +1,157 @@ +/** + * @file Defines a couple of wrappers over React Query/Tanstack Query that make + * it easier to use the Coder SDK within UI logic. + * + * These hooks are designed 100% for end-users, and should not be used + * internally. Use useEndUserCoderAuth when working with auth logic within these + * hooks. + * + * --- + * @todo 2024-05-28 - This isn't fully complete until we have an equivalent + * wrapper for useMutation, and have an idea of how useCoderQuery and + * useCoderMutation can be used together. + * + * Making the useMutation wrapper shouldn't be hard, but you want some good + * integration tests to verify that the two hooks can satisfy common user flows. + */ +import { + type QueryFunctionContext, + type QueryKey, + type UseQueryOptions, + type UseQueryResult, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { DEFAULT_TANSTACK_QUERY_RETRY_COUNT } from '../typesConstants'; +import { useEndUserCoderAuth } from '../components/CoderProvider'; +import { CODER_QUERY_KEY_PREFIX } from '../api/queryOptions'; +import { useCoderSdk } from './useCoderSdk'; +import type { BackstageCoderSdk } from '../api/CoderClient'; + +export type CoderQueryFunctionContext = + QueryFunctionContext & { + sdk: BackstageCoderSdk; + }; + +export type CoderQueryFunction< + T = unknown, + TQueryKey extends QueryKey = QueryKey, +> = (context: CoderQueryFunctionContext) => Promise; + +export type UseCoderQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + UseQueryOptions, + // queryFn omitted so that a custom version can be patched in; all other + // properties omitted because they are officially deprecated in React Query v4 + // and outright removed in v5. Want better future-proofing + 'queryFn' | 'isDataEqual' | 'onError' | 'onSuccess' | 'onSettled' +> & { + queryFn: CoderQueryFunction; +}; + +export function useCoderQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryOptions: UseCoderQueryOptions, +): UseQueryResult { + const queryClient = useQueryClient(); + const { isAuthenticated } = useEndUserCoderAuth(); + const sdk = useCoderSdk(); + + let patchedQueryKey = queryOptions.queryKey; + if ( + patchedQueryKey === undefined || + patchedQueryKey[0] !== CODER_QUERY_KEY_PREFIX + ) { + const baseKey = + queryOptions.queryKey ?? queryClient.defaultQueryOptions().queryKey; + + if (baseKey === undefined) { + throw new Error('No queryKey value provided to useCoderQuery'); + } + + patchedQueryKey = [ + CODER_QUERY_KEY_PREFIX, + ...baseKey, + ] as QueryKey as TQueryKey; + } + + type Options = UseQueryOptions; + const patchedOptions: Options = { + ...queryOptions, + queryKey: patchedQueryKey, + enabled: isAuthenticated && (queryOptions.enabled ?? true), + keepPreviousData: + isAuthenticated && (queryOptions.keepPreviousData ?? false), + refetchIntervalInBackground: + isAuthenticated && (queryOptions.refetchIntervalInBackground ?? false), + + queryFn: async context => { + if (!isAuthenticated) { + throw new Error('Cannot complete request - user is not authenticated'); + } + + return queryOptions.queryFn({ ...context, sdk }); + }, + + refetchInterval: (data, query) => { + if (!isAuthenticated) { + return false; + } + + const externalRefetchInterval = queryOptions.refetchInterval; + if (typeof externalRefetchInterval !== 'function') { + return externalRefetchInterval ?? false; + } + + return externalRefetchInterval(data, query); + }, + + refetchOnMount: query => { + if (!isAuthenticated) { + return false; + } + + const externalRefetchOnMount = queryOptions.refetchOnMount; + if (typeof externalRefetchOnMount !== 'function') { + return externalRefetchOnMount ?? true; + } + + return externalRefetchOnMount(query); + }, + + retry: (failureCount, error) => { + if (!isAuthenticated) { + return false; + } + + const externalRetry = queryOptions.retry; + if (typeof externalRetry === 'number') { + const normalized = Number.isInteger(externalRetry) + ? Math.max(1, externalRetry) + : DEFAULT_TANSTACK_QUERY_RETRY_COUNT; + + return failureCount < normalized; + } + + if (typeof externalRetry !== 'function') { + // Could use the nullish coalescing operator here, but Prettier made the + // output hard to read + return externalRetry + ? externalRetry + : failureCount < DEFAULT_TANSTACK_QUERY_RETRY_COUNT; + } + + return externalRetry(failureCount, error); + }, + }; + + return useQuery(patchedOptions); +} diff --git a/plugins/backstage-plugin-coder/src/plugin.ts b/plugins/backstage-plugin-coder/src/plugin.ts index 2aaaab89..904b7705 100644 --- a/plugins/backstage-plugin-coder/src/plugin.ts +++ b/plugins/backstage-plugin-coder/src/plugin.ts @@ -192,6 +192,12 @@ export { useWorkspacesCardContext } from './components/CoderWorkspacesCard/Root' export { useCoderWorkspacesConfig } from './hooks/useCoderWorkspacesConfig'; export { useCoderSdk } from './hooks/useCoderSdk'; export { useEndUserCoderAuth as useCoderAuth } from './components/CoderProvider/CoderAuthProvider'; +export { useCoderQuery } from './hooks/reactQueryWrappers'; + +/** + * General constants + */ +export { CODER_QUERY_KEY_PREFIX } from './api/queryOptions'; /** * All custom types diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index 86ceedcb..cc8c67ad 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -106,7 +106,7 @@ export function getMockQueryClient(): QueryClient { } type MockAuthProps = Readonly< - CoderProviderProps & { + Omit & { auth?: CoderAuth; /** @@ -221,3 +221,24 @@ export async function renderInCoderEnvironment({ await waitFor(() => expect(loadingIndicator).not.toBeInTheDocument()); return renderOutput; } + +type InvertedPromiseResult = Readonly<{ + promise: Promise; + resolve: (value: TData) => void; + reject: (errorReason: TError) => void; +}>; + +export function createInvertedPromise< + TData = unknown, + TError = Error, +>(): InvertedPromiseResult { + let resolve!: (value: TData) => void; + let reject!: (error: TError) => void; + + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + return { promise, resolve, reject }; +}