diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index d0d75f78a7..8711e64a82 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -101,10 +101,6 @@ updates: xterm: patterns: - "xterm*" - xstate: - patterns: - - "xstate" - - "@xstate*" mui: patterns: - "@mui*" diff --git a/.gitignore b/.gitignore index 2ccd459b81..5e5631409c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ yarn-error.log # Front-end ignore patterns. .next/ -site/**/*.typegen.ts site/build-storybook.log site/coverage/ site/storybook-static/ diff --git a/.prettierignore b/.prettierignore index c7882767e8..011d66b709 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,7 +23,6 @@ yarn-error.log # Front-end ignore patterns. .next/ -site/**/*.typegen.ts site/build-storybook.log site/coverage/ site/storybook-static/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f726162d2..cbaf02266c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -170,7 +170,6 @@ "wsconncache", "wsjson", "xerrors", - "xstate", "yamux" ], "cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"], diff --git a/docs/contributing/frontend.md b/docs/contributing/frontend.md index 3a207dd5cd..17432a4243 100644 --- a/docs/contributing/frontend.md +++ b/docs/contributing/frontend.md @@ -34,7 +34,6 @@ important ones: - [react-router](https://reactrouter.com/en/main) for routing - [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview) for fetching data -- [XState](https://xstate.js.org/docs/) for handling complex state flows - [axios](https://github.com/axios/axios) as fetching lib - [Playwright](https://playwright.dev/) for end-to-end (E2E) testing - [Jest](https://jestjs.io/) for integration testing @@ -96,13 +95,7 @@ a `*.stories.ts` file. We use [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview)(previously -known as react-query) to fetch data from the API. We also use -[XState](https://xstate.js.org/docs/) to handle complex flows with multiple -states and transitions. - -> ℹ️ We recently changed how we are going to fetch data from the server so you -> will see a lot of fetches being made using XState machines but feel free to -> refactor it if you are already touching those files. +known as react-query) to fetch data from the API. ### Where to fetch data diff --git a/site/.eslintignore b/site/.eslintignore index 4909d9bf91..20570ccb94 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -23,7 +23,6 @@ yarn-error.log # Front-end ignore patterns. .next/ -**/*.typegen.ts build-storybook.log coverage/ storybook-static/ diff --git a/site/.prettierignore b/site/.prettierignore index 4909d9bf91..20570ccb94 100644 --- a/site/.prettierignore +++ b/site/.prettierignore @@ -23,7 +23,6 @@ yarn-error.log # Front-end ignore patterns. .next/ -**/*.typegen.ts build-storybook.log coverage/ storybook-static/ diff --git a/site/package.json b/site/package.json index d9bab274df..019dbf6a40 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,6 @@ "private": true, "license": "AGPL-3.0", "scripts": { - "postinstall": "pnpm typegen", "build": "NODE_ENV=production pnpm vite build", "check:all": "pnpm format:check && pnpm lint && pnpm test", "chromatic": "chromatic", @@ -13,7 +12,7 @@ "format:check": "prettier --cache --check '../**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'", "format:write": "prettier --cache --write '../**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'", "format:write:only": "prettier --cache --write", - "lint": "pnpm typegen && pnpm run lint:types && pnpm exec jest --selectProjects lint", + "lint": "pnpm run lint:types && pnpm exec jest --selectProjects lint", "lint:fix": "FIX=true pnpm lint", "lint:types": "tsc --noEmit", "playwright:install": "playwright install --with-deps chromium", @@ -25,9 +24,8 @@ "test:ci": "jest --selectProjects test --silent", "test:coverage": "jest --selectProjects test --collectCoverage", "test:watch": "jest --selectProjects test --watch", - "typegen": "xstate typegen 'src/**/*.ts'", "stats": "STATS=true pnpm build && npx http-server ./stats -p 8081 -c-1", - "deadcode": "ts-prune | grep -v \".stories\\|.typegen\\|.config\\|e2e\\|__mocks__\\|used in module\\|testHelpers\\|typesGenerated\" || echo \"No deadcode found.\"" + "deadcode": "ts-prune | grep -v \".stories\\|.config\\|e2e\\|__mocks__\\|used in module\\|testHelpers\\|typesGenerated\" || echo \"No deadcode found.\"" }, "dependencies": { "@emoji-mart/data": "1.1.2", @@ -46,8 +44,6 @@ "@mui/system": "5.14.0", "@mui/utils": "5.14.11", "@vitejs/plugin-react": "4.1.0", - "@xstate/inspect": "0.8.0", - "@xstate/react": "3.2.1", "ansi-to-html": "0.7.2", "axios": "1.6.0", "canvas": "2.11.0", @@ -94,7 +90,6 @@ "unique-names-generator": "4.7.1", "uuid": "9.0.0", "vite": "4.5.0", - "xstate": "4.38.1", "xterm": "5.2.0", "xterm-addon-canvas": "0.5.0", "xterm-addon-fit": "0.8.0", @@ -138,7 +133,6 @@ "@types/uuid": "9.0.2", "@typescript-eslint/eslint-plugin": "6.9.1", "@typescript-eslint/parser": "6.9.1", - "@xstate/cli": "0.5.2", "chromatic": "7.6.0", "eslint": "8.52.0", "eslint-config-prettier": "9.0.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 85cd8bd595..f0a9685c8e 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -57,12 +57,6 @@ dependencies: '@vitejs/plugin-react': specifier: 4.1.0 version: 4.1.0(vite@4.5.0) - '@xstate/inspect': - specifier: 0.8.0 - version: 0.8.0(ws@8.14.2)(xstate@4.38.1) - '@xstate/react': - specifier: 3.2.1 - version: 3.2.1(@types/react@18.2.6)(react@18.2.0)(xstate@4.38.1) ansi-to-html: specifier: 0.7.2 version: 0.7.2 @@ -201,9 +195,6 @@ dependencies: vite: specifier: 4.5.0 version: 4.5.0(@types/node@18.18.1) - xstate: - specifier: 4.38.1 - version: 4.38.1 xterm: specifier: 5.2.0 version: 5.2.0 @@ -329,9 +320,6 @@ devDependencies: '@typescript-eslint/parser': specifier: 6.9.1 version: 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@xstate/cli': - specifier: 0.5.2 - version: 0.5.2 chromatic: specifier: 7.6.0 version: 7.6.0 @@ -474,29 +462,6 @@ packages: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} engines: {node: '>=6.9.0'} - /@babel/core@7.22.9: - resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.13 - '@babel/generator': 7.23.0 - '@babel/helper-compilation-targets': 7.22.15 - '@babel/helper-module-transforms': 7.23.0(@babel/core@7.22.9) - '@babel/helpers': 7.23.2 - '@babel/parser': 7.23.0 - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 7.5.3 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/core@7.23.0: resolution: {integrity: sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==} engines: {node: '>=6.9.0'} @@ -668,20 +633,6 @@ packages: dependencies: '@babel/types': 7.23.0 - /@babel/helper-module-transforms@7.23.0(@babel/core@7.22.9): - resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true - /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.0): resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} engines: {node: '>=6.9.0'} @@ -5799,81 +5750,6 @@ packages: resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} dev: false - /@xstate/cli@0.5.2: - resolution: {integrity: sha512-KA0BJMd80Z3lp1MmVqlUpHkjLkKcq4Z09P19It4iJ6IX8Hzwo5lmRTZwX938UCiAjWWSA2jl6nfrJnfBR21riA==} - hasBin: true - dependencies: - '@babel/core': 7.22.9 - '@xstate/machine-extractor': 0.10.0(xstate@4.38.2) - '@xstate/tools-shared': 3.0.1(xstate@4.38.2) - chokidar: 3.5.3 - commander: 8.3.0 - prettier: 2.8.8 - xstate: 4.38.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@xstate/inspect@0.8.0(ws@8.14.2)(xstate@4.38.1): - resolution: {integrity: sha512-wSkFeOnp+7dhn+zTThO0M4D2FEqZN9lGIWowJu5JLa2ojjtlzRwK8SkjcHZ4rLX8VnMev7kGjgQLrGs8kxy+hw==} - peerDependencies: - '@types/ws': ^8.0.0 - ws: ^8.0.0 - xstate: ^4.37.0 - peerDependenciesMeta: - '@types/ws': - optional: true - dependencies: - fast-safe-stringify: 2.1.1 - ws: 8.14.2 - xstate: 4.38.1 - dev: false - - /@xstate/machine-extractor@0.10.0(xstate@4.38.2): - resolution: {integrity: sha512-jsnYU9Y0DfFQCisY0IGxjmFrU6y3aqdBXBNohasxHiKHfuItkk4AUAQ+07MykJ+RiczXIYqYQNu5DvZAfCqKCA==} - peerDependencies: - xstate: ^4 - dependencies: - '@babel/parser': 7.23.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 - recast: 0.23.4 - xstate: 4.38.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@xstate/react@3.2.1(@types/react@18.2.6)(react@18.2.0)(xstate@4.38.1): - resolution: {integrity: sha512-L/mqYRxyBWVdIdSaXBHacfvS8NKn3sTKbPb31aRADbE9spsJ1p+tXil0GVQHPlzrmjGeozquLrxuYGiXsFNU7g==} - peerDependencies: - '@xstate/fsm': ^2.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - xstate: ^4.36.0 - peerDependenciesMeta: - '@xstate/fsm': - optional: true - xstate: - optional: true - dependencies: - react: 18.2.0 - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.6)(react@18.2.0) - use-sync-external-store: 1.2.0(react@18.2.0) - xstate: 4.38.1 - transitivePeerDependencies: - - '@types/react' - dev: false - - /@xstate/tools-shared@3.0.1(xstate@4.38.2): - resolution: {integrity: sha512-XW00KB72i4XiQPiB0e4P7Fsn9TvYBxqVR0HNGGEkmvQ7l8FZM2FpzBDAriVH67XRUgI1crfNyisxXmGlpB5WYg==} - peerDependencies: - xstate: ^4 - dependencies: - '@xstate/machine-extractor': 0.10.0(xstate@4.38.2) - xstate: 4.38.2 - transitivePeerDependencies: - - supports-color - dev: true - /@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.18.20): resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==} engines: {node: '>=14.15.0'} @@ -8169,10 +8045,6 @@ packages: resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} dev: false - /fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: false - /fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} dev: false @@ -13477,19 +13349,6 @@ packages: tslib: 2.6.2 dev: true - /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.6)(react@18.2.0): - resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.6 - react: 18.2.0 - dev: false - /use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: @@ -13924,14 +13783,6 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false - /xstate@4.38.1: - resolution: {integrity: sha512-1gBUcFWBj/rv/pRcP2Bedl5sNRGX2d36CaOx9z7fE9uSiHaOEHIWzLg1B853q2xdUHUA9pEiWKjLZ3can4SJaQ==} - dev: false - - /xstate@4.38.2: - resolution: {integrity: sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==} - dev: true - /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2379d00cf1..6f0e4d3832 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -525,7 +525,7 @@ export const postWorkspaceBuild = async ( export const startWorkspace = ( workspaceId: string, templateVersionId: string, - logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], + logLevel?: TypesGen.ProvisionerLogLevel, buildParameters?: TypesGen.WorkspaceBuildParameter[], ) => postWorkspaceBuild(workspaceId, { @@ -536,16 +536,21 @@ export const startWorkspace = ( }); export const stopWorkspace = ( workspaceId: string, - logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], + logLevel?: TypesGen.ProvisionerLogLevel, ) => postWorkspaceBuild(workspaceId, { transition: "stop", log_level: logLevel, }); +export type DeleteWorkspaceOptions = Pick< + TypesGen.CreateWorkspaceBuildRequest, + "log_level" & "orphan" +>; + export const deleteWorkspace = ( workspaceId: string, - options?: Pick, + options?: DeleteWorkspaceOptions, ) => postWorkspaceBuild(workspaceId, { transition: "delete", @@ -1207,10 +1212,15 @@ export const removeLicense = async (licenseId: number): Promise => { export class MissingBuildParameters extends Error { parameters: TypesGen.TemplateVersionParameter[] = []; + versionId: string; - constructor(parameters: TypesGen.TemplateVersionParameter[]) { + constructor( + parameters: TypesGen.TemplateVersionParameter[], + versionId: string, + ) { super("Missing build parameters."); this.parameters = parameters; + this.versionId = versionId; } } @@ -1239,7 +1249,7 @@ export const changeWorkspaceVersion = async ( ); if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters); + throw new MissingBuildParameters(missingParameters, templateVersionId); } return postWorkspaceBuild(workspace.id, { @@ -1277,7 +1287,7 @@ export const updateWorkspace = async ( ); if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters); + throw new MissingBuildParameters(missingParameters, activeVersionId); } return postWorkspaceBuild(workspace.id, { diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 2ad91c7724..a8d066b41f 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -27,3 +27,10 @@ export const health = () => { queryFn: API.getHealth, }; }; + +export const deploymentSSHConfig = () => { + return { + queryKey: ["deployment", "sshConfig"], + queryFn: API.getDeploymentSSHConfig, + }; +}; diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 9da020881b..ecace99560 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -29,6 +29,11 @@ export const workspaceBuildByNumber = ( }; }; +export const workspaceBuildsKey = (workspaceId: string) => [ + "workspaceBuilds", + workspaceId, +]; + export const infiniteWorkspaceBuilds = ( workspaceId: string, req?: WorkspaceBuildsRequest, @@ -36,7 +41,7 @@ export const infiniteWorkspaceBuilds = ( const limit = req?.limit ?? 25; return { - queryKey: ["workspaceBuilds", workspaceId, req], + queryKey: [...workspaceBuildsKey(workspaceId), req], getNextPageParam: (lastPage, pages) => { if (lastPage.length < limit) { return undefined; diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index f37c65f9ed..1dd3c0be1a 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -9,7 +9,10 @@ import { type CreateWorkspaceRequest, type WorkspacesResponse, type WorkspacesRequest, + WorkspaceBuild, + ProvisionerLogLevel, } from "api/typesGenerated"; +import { workspaceBuildsKey } from "./workspaceBuilds"; export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ "workspace", @@ -18,13 +21,11 @@ export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ "settings", ]; -export const workspaceByOwnerAndName = ( - owner: string, - name: string, -): QueryOptions => { +export const workspaceByOwnerAndName = (owner: string, name: string) => { return { queryKey: workspaceByOwnerAndNameKey(owner, name), - queryFn: () => API.getWorkspaceByOwnerAndName(owner, name), + queryFn: () => + API.getWorkspaceByOwnerAndName(owner, name, { include_deleted: true }), }; }; @@ -124,3 +125,135 @@ export const increaseDeadline = (workspace: Workspace) => { }, }; }; + +export const changeVersion = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: ({ + versionId, + buildParameters, + }: { + versionId: string; + buildParameters?: WorkspaceBuildParameter[]; + }) => { + return API.changeWorkspaceVersion(workspace, versionId, buildParameters); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +export const updateWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { + return API.updateWorkspace(workspace, buildParameters); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +export const deleteWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: (options: API.DeleteWorkspaceOptions) => { + return API.deleteWorkspace(workspace.id, options); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +export const stopWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { + return API.stopWorkspace(workspace.id, logLevel); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +export const startWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: ({ + buildParameters, + logLevel, + }: { + buildParameters?: WorkspaceBuildParameter[]; + logLevel?: ProvisionerLogLevel; + }) => { + return API.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + logLevel, + buildParameters, + ); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { + return { + mutationFn: () => { + return API.cancelWorkspaceBuild(workspace.latest_build.id); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: workspaceBuildsKey(workspace.id), + }); + }, + }; +}; + +export const activate = (workspace: Workspace, queryClient: QueryClient) => { + return { + mutationFn: () => { + return API.updateWorkspaceDormancy(workspace.id, false); + }, + onSuccess: (updatedWorkspace: Workspace) => { + queryClient.setQueryData( + workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), + updatedWorkspace, + ); + }, + }; +}; + +const updateWorkspaceBuild = async ( + build: WorkspaceBuild, + queryClient: QueryClient, +) => { + const workspaceKey = workspaceByOwnerAndNameKey( + build.workspace_owner_name, + build.workspace_name, + ); + const previousData = queryClient.getQueryData(workspaceKey) as Workspace; + queryClient.setQueryData(workspaceKey, { + ...previousData, + latest_build: build, + }); + await queryClient.invalidateQueries({ + queryKey: workspaceBuildsKey(build.workspace_id), + }); +}; diff --git a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx deleted file mode 100644 index fe7032304c..0000000000 --- a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { TemplateVersionWarnings } from "./TemplateVersionWarnings"; -import type { Meta, StoryObj } from "@storybook/react"; - -const meta: Meta = { - title: "components/TemplateVersionWarnings", - component: TemplateVersionWarnings, -}; - -export default meta; -type Story = StoryObj; - -export const UnsupportedWorkspaces: Story = { - args: { - warnings: ["UNSUPPORTED_WORKSPACES"], - }, -}; diff --git a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx deleted file mode 100644 index 473488bda9..0000000000 --- a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { FC } from "react"; -import * as TypesGen from "api/typesGenerated"; -import { Alert } from "components/Alert/Alert"; - -export interface TemplateVersionWarningsProps { - warnings?: TypesGen.TemplateVersionWarning[]; -} - -export const TemplateVersionWarnings: FC< - React.PropsWithChildren -> = (props) => { - const { warnings = [] } = props; - - if (!warnings.includes("UNSUPPORTED_WORKSPACES")) { - return null; - } - - return ( -
- - This template uses legacy parameters which are not supported anymore. - Contact your administrator for assistance. - -
- ); -}; diff --git a/site/src/index.tsx b/site/src/index.tsx index 48130401be..a6d366d5d5 100644 --- a/site/src/index.tsx +++ b/site/src/index.tsx @@ -1,23 +1,6 @@ -import { inspect } from "@xstate/inspect"; import { createRoot } from "react-dom/client"; -import { Interpreter } from "xstate"; import { App } from "./App"; -// if this is a development build and the developer wants to inspect -// helpful to see realtime changes on the services -if ( - process.env.NODE_ENV === "development" && - process.env.INSPECT_XSTATE === "true" -) { - // configure the XState inspector to open in a new tab - inspect({ - url: "https://stately.ai/viz?inspect", - iframe: false, - }); - // configure all XServices to use the inspector - Interpreter.defaultOptions.devTools = true; -} - // This is the entry point for the app - where everything start. // In the future, we'll likely bring in more bootstrapping logic - // like: https://github.com/coder/m/blob/50898bd4803df7639bd181e484c74ac5d84da474/product/coder/site/pages/_app.tsx#L32 diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index edb53c9ee1..0af2aa6488 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx @@ -2,7 +2,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { MockTemplate, MockTemplateVersion, - MockTemplateVersion3, MockWorkspaceResource, MockWorkspaceVolumeResource, } from "testHelpers/entities"; @@ -31,11 +30,3 @@ export const NoIcon: Story = { resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], }, }; - -export const WithDeprecatedParameters: Story = { - args: { - template: MockTemplate, - activeVersion: MockTemplateVersion3, - resources: [MockWorkspaceResource, MockWorkspaceVolumeResource], - }, -}; diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx index a54f96f91d..f73e7b1592 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx @@ -9,7 +9,6 @@ import { Loader } from "components/Loader/Loader"; import { Stack } from "components/Stack/Stack"; import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"; import { TemplateStats } from "./TemplateStats"; -import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings"; export interface TemplateSummaryPageViewProps { resources?: WorkspaceResource[]; @@ -45,7 +44,6 @@ export const TemplateSummaryPageView: FC = ({ return ( - diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx index 064a892d35..c6f9aee136 100644 --- a/site/src/pages/TerminalPage/TerminalPage.test.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx @@ -77,6 +77,7 @@ describe("TerminalPage", () => { expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( MockUser.username, MockWorkspace.name, + { include_deleted: true }, ); }); spy.mockRestore(); diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index a455812f12..10887f5b5e 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -747,10 +747,3 @@ function makeFailedBuildLogs(): ProvisionerJobLog[] { }, ]; } - -export const UnsupportedWorkspace: Story = { - args: { - ...Running.args, - templateWarnings: ["UNSUPPORTED_WORKSPACES"], - }, -}; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index b8d49bf17d..060ee10c5b 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -15,7 +15,6 @@ import { PageHeaderTitle, PageHeaderSubtitle, } from "components/PageHeader/FullWidthPageHeader"; -import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { DormantWorkspaceBanner } from "components/WorkspaceDeletion"; import { Avatar } from "components/Avatar/Avatar"; @@ -55,7 +54,6 @@ export interface WorkspaceProps { isRestarting: boolean; workspace: TypesGen.Workspace; resources?: TypesGen.WorkspaceResource[]; - templateWarnings?: TypesGen.TemplateVersionWarning[]; canUpdateWorkspace: boolean; updateMessage?: string; canRetryDebugMode: boolean; @@ -105,9 +103,7 @@ export const Workspace: FC> = ({ buildInfo, sshPrefix, template, - quotaBudget, handleBuildRetry, - templateWarnings, buildLogs, onLoadMoreBuilds, isLoadingMoreBuilds, @@ -198,7 +194,6 @@ export const Workspace: FC> = ({ > = ({ onDismiss={() => saveLocal("dismissedWorkspace", workspace.id)} /> - - {showAlertPendingInQueue && ( Workspace build is pending diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index fa9096b088..008a3d2c54 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -12,7 +12,6 @@ import { MockTemplateVersionParameter1, MockTemplateVersionParameter2, MockBuilds, - MockTemplateVersion3, MockUser, MockDeploymentConfig, MockWorkspaceBuildDelete, @@ -259,10 +258,10 @@ describe("WorkspacePage", () => { const updateWorkspaceSpy = jest .spyOn(api, "updateWorkspace") .mockRejectedValueOnce( - new api.MissingBuildParameters([ - MockTemplateVersionParameter1, - MockTemplateVersionParameter2, - ]), + new api.MissingBuildParameters( + [MockTemplateVersionParameter1, MockTemplateVersionParameter2], + MockOutdatedWorkspace.template_active_version_id, + ), ); // Render @@ -324,20 +323,6 @@ describe("WorkspacePage", () => { }); }); - it("shows the template warning", async () => { - server.use( - rest.get( - "/api/v2/templateversions/:templateVersionId", - async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockTemplateVersion3)); - }, - ), - ); - - await renderWorkspacePage(); - await screen.findByTestId("error-unsupported-workspaces"); - }); - it("restart the workspace with one time parameters when having the confirmation dialog", async () => { window.localStorage.removeItem(`${MockUser.id}_ignoredWarnings`); jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 01bd83075f..b11cfb1a1a 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,22 +1,22 @@ -import { useMachine } from "@xstate/react"; import { Loader } from "components/Loader/Loader"; -import { FC } from "react"; +import { FC, useEffect } from "react"; import { useParams } from "react-router-dom"; -import { workspaceMachine } from "xServices/workspace/workspaceXService"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; -import { RequirePermission } from "components/RequirePermission/RequirePermission"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { useOrganizationId } from "hooks"; -import { isAxiosError } from "axios"; import { Margins } from "components/Margins/Margins"; -import { - workspaceQuota, - workspaceResolveAutostart, -} from "api/queries/workspaceQuota"; -import { useInfiniteQuery, useQuery } from "react-query"; +import { useInfiniteQuery, useQuery, useQueryClient } from "react-query"; import { infiniteWorkspaceBuilds } from "api/queries/workspaceBuilds"; +import { templateByName } from "api/queries/templates"; +import { workspaceByOwnerAndName } from "api/queries/workspaces"; +import { checkAuthorization } from "api/queries/authCheck"; +import { WorkspacePermissions, workspaceChecks } from "./permissions"; +import { watchWorkspace } from "api/api"; +import { Workspace } from "api/typesGenerated"; +import { useEffectEvent } from "hooks/hookPolyfills"; export const WorkspacePage: FC = () => { + const queryClient = useQueryClient(); const params = useParams() as { username: string; workspace: string; @@ -24,31 +24,81 @@ export const WorkspacePage: FC = () => { const workspaceName = params.workspace; const username = params.username.replace("@", ""); const orgId = useOrganizationId(); - const [workspaceState, workspaceSend] = useMachine(workspaceMachine, { - context: { - orgId, - workspaceName, - username, - }, - actions: { - refreshBuilds: async () => { - await buildsQuery.refetch(); - }, - }, + + // Workspace + const workspaceQueryOptions = workspaceByOwnerAndName( + username, + workspaceName, + ); + const workspaceQuery = useQuery(workspaceQueryOptions); + const workspace = workspaceQuery.data; + + // Template + const templateQuery = useQuery({ + ...templateByName(orgId, workspace?.template_name ?? ""), + enabled: workspace !== undefined, }); - const { workspace, error } = workspaceState.context; - const quotaQuery = useQuery(workspaceQuota(username)); - const pageError = error ?? quotaQuery.error; + const template = templateQuery.data; + + // Permissions + const checks = + workspace && template ? workspaceChecks(workspace, template) : {}; + const permissionsQuery = useQuery({ + ...checkAuthorization({ checks }), + enabled: workspace !== undefined && template !== undefined, + }); + const permissions = permissionsQuery.data as WorkspacePermissions | undefined; + + // Builds const buildsQuery = useInfiniteQuery({ ...infiniteWorkspaceBuilds(workspace?.id ?? ""), - enabled: Boolean(workspace), + enabled: workspace !== undefined, }); - const canAutostartResponse = useQuery( - workspaceResolveAutostart(workspace?.id ?? ""), - ); + // Watch workspace changes + const updateWorkspaceData = useEffectEvent( + async (newWorkspaceData: Workspace) => { + queryClient.setQueryData( + workspaceQueryOptions.queryKey, + newWorkspaceData, + ); - const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; + const hasNewBuild = + newWorkspaceData.latest_build.id !== workspace!.latest_build.id; + const lastBuildHasChanged = + newWorkspaceData.latest_build.status !== workspace!.latest_build.status; + + if (hasNewBuild || lastBuildHasChanged) { + await buildsQuery.refetch(); + } + }, + ); + const workspaceId = workspace?.id; + useEffect(() => { + if (!workspaceId) { + return; + } + + const eventSource = watchWorkspace(workspaceId); + + eventSource.addEventListener("data", async (event) => { + const newWorkspaceData = JSON.parse(event.data) as Workspace; + await updateWorkspaceData(newWorkspaceData); + }); + + eventSource.addEventListener("error", (event) => { + console.error("Error on getting workspace changes.", event); + }); + + return () => { + eventSource.close(); + }; + }, [updateWorkspaceData, workspaceId]); + + // Page statuses + const pageError = + workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; + const isLoading = !workspace || !template || !permissions; if (pageError) { return ( @@ -58,30 +108,23 @@ export const WorkspacePage: FC = () => { ); } - if (!workspace || !workspaceState.matches("ready") || !quotaQuery.isSuccess) { + if (isLoading) { return ; } return ( - - { - await buildsQuery.fetchNextPage(); - }} - hasMoreBuilds={Boolean(buildsQuery.hasNextPage)} - canAutostart={canAutostart} - /> - + { + await buildsQuery.fetchNextPage(); + }} + hasMoreBuilds={Boolean(buildsQuery.hasNextPage)} + /> ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 11185f5a90..fd6a9343fa 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -9,18 +9,13 @@ import { getMaxDeadlineChange, getMinDeadline, } from "utils/schedule"; -import { StateFrom } from "xstate"; import { Workspace, WorkspaceErrors } from "./Workspace"; import { pageTitle } from "utils/page"; -import { getFaviconByStatus, hasJobError } from "utils/workspace"; -import { - WorkspaceEvent, - workspaceMachine, -} from "xServices/workspace/workspaceXService"; +import { hasJobError } from "utils/workspace"; import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { ChangeVersionDialog } from "./ChangeVersionDialog"; -import { useMutation, useQuery } from "react-query"; -import { restartWorkspace } from "api/api"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { MissingBuildParameters, restartWorkspace } from "api/api"; import { ConfirmDialog, ConfirmDialogProps, @@ -31,90 +26,76 @@ import { templateVersion, templateVersions } from "api/queries/templates"; import { Alert } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; -import { decreaseDeadline, increaseDeadline } from "api/queries/workspaces"; +import { + activate, + changeVersion, + decreaseDeadline, + deleteWorkspace, + increaseDeadline, + updateWorkspace, + stopWorkspace, + startWorkspace, + cancelBuild, +} from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; +import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; +import { WorkspacePermissions } from "./permissions"; +import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import dayjs from "dayjs"; interface WorkspaceReadyPageProps { - workspaceState: StateFrom; - workspaceSend: (event: WorkspaceEvent) => void; - quota?: TypesGen.WorkspaceQuota; + template: TypesGen.Template; + workspace: TypesGen.Workspace; + permissions: WorkspacePermissions; builds: TypesGen.WorkspaceBuild[] | undefined; buildsError: unknown; onLoadMoreBuilds: () => void; isLoadingMoreBuilds: boolean; hasMoreBuilds: boolean; - canAutostart: boolean; } export const WorkspaceReadyPage = ({ - workspaceState, - workspaceSend, - quota, + workspace, + template, + permissions, builds, buildsError, onLoadMoreBuilds, isLoadingMoreBuilds, hasMoreBuilds, - canAutostart, }: WorkspaceReadyPageProps): JSX.Element => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); - const { - workspace, - template, - templateVersion: currentVersion, - deploymentValues, - buildError, - cancellationError, - sshPrefix, - permissions, - missedParameters, - } = workspaceState.context; if (workspace === undefined) { throw Error("Workspace is undefined"); } - const deadline = getDeadline(workspace); - const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); - const canUpdateTemplate = Boolean(permissions?.updateTemplate); - const canRetryDebugMode = - Boolean(permissions?.viewDeploymentValues) && - Boolean(deploymentValues?.enable_terraform_debug_mode); - const favicon = getFaviconByStatus(workspace.latest_build); - const navigate = useNavigate(); - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); - const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); - const [confirmingRestart, setConfirmingRestart] = useState<{ - open: boolean; - buildParameters?: TypesGen.WorkspaceBuildParameter[]; - }>({ open: false }); - const { data: allVersions } = useQuery({ - ...templateVersions(workspace.template_id), - enabled: changeVersionDialogOpen, + // Debug mode + const { data: deploymentValues } = useQuery({ + ...deploymentConfig(), + enabled: permissions?.viewDeploymentValues, }); - const { data: latestVersion } = useQuery({ - ...templateVersion(workspace.template_active_version_id), - enabled: workspace.outdated, - }); - const [faviconTheme, setFaviconTheme] = useState<"light" | "dark">("dark"); - useEffect(() => { - if (typeof window === "undefined" || !window.matchMedia) { - return; - } + const canRetryDebugMode = Boolean( + deploymentValues?.config.enable_terraform_debug_mode, + ); - const isDark = window.matchMedia("(prefers-color-scheme: dark)"); - // We want the favicon the opposite of the theme. - setFaviconTheme(isDark.matches ? "light" : "dark"); - }, []); + // Build logs const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id); const shouldDisplayBuildLogs = hasJobError(workspace) || ["canceling", "deleting", "pending", "starting", "stopping"].includes( workspace.latest_build.status, ); + + // Restart + const [confirmingRestart, setConfirmingRestart] = useState<{ + open: boolean; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; + }>({ open: false }); const { mutate: mutateRestartWorkspace, error: restartBuildError, @@ -123,6 +104,8 @@ export const WorkspaceReadyPage = ({ mutationFn: restartWorkspace, }); + // Schedule controls + const deadline = getDeadline(workspace); const onDeadlineChangeSuccess = () => { displaySuccess("Updated workspace shutdown time."); }; @@ -142,6 +125,77 @@ export const WorkspaceReadyPage = ({ onError: onDeadlineChangeFails, }); + // Auto start + const canAutostartResponse = useQuery( + workspaceResolveAutostart(workspace.id), + ); + const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; + + // SSH Prefix + const sshPrefixQuery = useQuery(deploymentSSHConfig()); + + // Favicon + const favicon = getFaviconByStatus(workspace.latest_build); + const [faviconTheme, setFaviconTheme] = useState<"light" | "dark">("dark"); + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) { + return; + } + + const isDark = window.matchMedia("(prefers-color-scheme: dark)"); + // We want the favicon the opposite of the theme. + setFaviconTheme(isDark.matches ? "light" : "dark"); + }, []); + + // Change version + const canChangeVersions = Boolean(permissions?.updateTemplate); + const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); + const changeVersionMutation = useMutation( + changeVersion(workspace, queryClient), + ); + + // Versions + const { data: allVersions } = useQuery({ + ...templateVersions(workspace.template_id), + enabled: changeVersionDialogOpen, + }); + const { data: latestVersion } = useQuery({ + ...templateVersion(workspace.template_active_version_id), + enabled: workspace.outdated, + }); + + // Update workspace + const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); + const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); + const updateWorkspaceMutation = useMutation( + updateWorkspace(workspace, queryClient), + ); + + // Delete workspace + const canDeleteWorkspace = Boolean(permissions?.updateWorkspace); + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const deleteWorkspaceMutation = useMutation( + deleteWorkspace(workspace, queryClient), + ); + + // Activate workspace + const activateWorkspaceMutation = useMutation( + activate(workspace, queryClient), + ); + + // Stop workspace + const stopWorkspaceMutation = useMutation( + stopWorkspace(workspace, queryClient), + ); + + // Start workspace + const startWorkspaceMutation = useMutation( + startWorkspace(workspace, queryClient), + ); + + // Cancel build + const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); + return ( <> @@ -168,27 +222,50 @@ export const WorkspaceReadyPage = ({ deadline, ), }} - isUpdating={workspaceState.matches("ready.build.requestingUpdate")} + isUpdating={updateWorkspaceMutation.isLoading} isRestarting={isRestarting} workspace={workspace} - handleStart={(buildParameters) => - workspaceSend({ type: "START", buildParameters }) - } - handleStop={() => workspaceSend({ type: "STOP" })} - handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} + handleStart={(buildParameters) => { + startWorkspaceMutation.mutate({ buildParameters }); + }} + handleStop={() => { + stopWorkspaceMutation.mutate({}); + }} + handleDelete={() => { + setIsConfirmingDelete(true); + }} handleRestart={(buildParameters) => { setConfirmingRestart({ open: true, buildParameters }); }} handleUpdate={() => { setIsConfirmingUpdate(true); }} - handleCancel={() => workspaceSend({ type: "CANCEL" })} + handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} - handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })} + handleBuildRetry={() => { + switch (workspace.latest_build.transition) { + case "start": + startWorkspaceMutation.mutate({ logLevel: "debug" }); + break; + case "stop": + stopWorkspaceMutation.mutate({ logLevel: "debug" }); + break; + case "delete": + deleteWorkspaceMutation.mutate({ logLevel: "debug" }); + break; + } + }} handleChangeVersion={() => { setChangeVersionDialogOpen(true); }} - handleDormantActivate={() => workspaceSend({ type: "ACTIVATE" })} + handleDormantActivate={async () => { + try { + await activateWorkspaceMutation.mutateAsync(); + } catch (e) { + const message = getErrorMessage(e, "Error activate workspace."); + displayError(message); + } + }} resources={workspace.latest_build.resources} builds={builds} onLoadMoreBuilds={onLoadMoreBuilds} @@ -197,19 +274,22 @@ export const WorkspaceReadyPage = ({ canUpdateWorkspace={canUpdateWorkspace} updateMessage={latestVersion?.message} canRetryDebugMode={canRetryDebugMode} - canChangeVersions={canUpdateTemplate} + canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} workspaceErrors={{ [WorkspaceErrors.GET_BUILDS_ERROR]: buildsError, - [WorkspaceErrors.BUILD_ERROR]: buildError || restartBuildError, - [WorkspaceErrors.CANCELLATION_ERROR]: cancellationError, + [WorkspaceErrors.BUILD_ERROR]: + restartBuildError ?? + startWorkspaceMutation.error ?? + stopWorkspaceMutation.error ?? + deleteWorkspaceMutation.error ?? + updateWorkspaceMutation.error, + [WorkspaceErrors.CANCELLATION_ERROR]: cancelBuildMutation.error, }} buildInfo={buildInfo} - sshPrefix={sshPrefix} + sshPrefix={sshPrefixQuery.data?.hostname_prefix} template={template} - quotaBudget={quota?.budget} - templateWarnings={currentVersion?.warnings} buildLogs={ shouldDisplayBuildLogs && ( @@ -219,24 +299,50 @@ export const WorkspaceReadyPage = ({ /> workspaceSend({ type: "CANCEL_DELETE" })} + canUpdateTemplate={canDeleteWorkspace} + isOpen={isConfirmingDelete} + onCancel={() => { + setIsConfirmingDelete(false); + }} onConfirm={(orphan) => { - workspaceSend({ type: "DELETE", orphan }); + deleteWorkspaceMutation.mutate({ orphan }); + setIsConfirmingDelete(false); }} workspaceBuildDateStr={dayjs(workspace.created_at).fromNow()} /> { - workspaceSend({ type: "CANCEL" }); + changeVersionMutation.reset(); }} onUpdate={(buildParameters) => { - workspaceSend({ type: "UPDATE", buildParameters }); + if (changeVersionMutation.error instanceof MissingBuildParameters) { + changeVersionMutation.mutate({ + versionId: changeVersionMutation.error.versionId, + buildParameters, + }); + } + }} + /> + { + updateWorkspaceMutation.reset(); + }} + onUpdate={(buildParameters) => { + if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { + updateWorkspaceMutation.mutate(buildParameters); + } }} /> { setChangeVersionDialogOpen(false); - workspaceSend({ - type: "CHANGE_VERSION", - templateVersionId: templateVersion.id, - }); + changeVersionMutation.mutate({ versionId: templateVersion.id }); }} /> { - workspaceSend({ type: "UPDATE" }); + updateWorkspaceMutation.mutate(undefined); setIsConfirmingUpdate(false); }} onClose={() => setIsConfirmingUpdate(false)} @@ -310,3 +413,38 @@ const WarningDialog: FC< > = (props) => { return ; }; + +// You can see the favicon designs here: https://www.figma.com/file/YIGBkXUcnRGz2ZKNmLaJQf/Coder-v2-Design?node-id=560%3A620 +type FaviconType = + | "favicon" + | "favicon-success" + | "favicon-error" + | "favicon-warning" + | "favicon-running"; + +const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType => { + switch (build.status) { + case undefined: + return "favicon"; + case "running": + return "favicon-success"; + case "starting": + return "favicon-running"; + case "stopping": + return "favicon-running"; + case "stopped": + return "favicon"; + case "deleting": + return "favicon"; + case "deleted": + return "favicon"; + case "canceling": + return "favicon-warning"; + case "canceled": + return "favicon"; + case "failed": + return "favicon-error"; + case "pending": + return "favicon"; + } +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index 72c703ea63..a173d04d1b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -24,6 +24,8 @@ import { PopoverTrigger, usePopover, } from "components/Popover/Popover"; +import { workspaceQuota } from "api/queries/workspaceQuota"; +import { useQuery } from "react-query"; const Language = { workspaceDetails: "Workspace Details", @@ -37,7 +39,6 @@ export interface WorkspaceStatsProps { maxDeadlineIncrease: number; maxDeadlineDecrease: number; canUpdateWorkspace: boolean; - quotaBudget?: number; onDeadlinePlus: (hours: number) => void; onDeadlineMinus: (hours: number) => void; handleUpdate: () => void; @@ -45,7 +46,6 @@ export interface WorkspaceStatsProps { export const WorkspaceStats: FC = ({ workspace, - quotaBudget, maxDeadlineDecrease, maxDeadlineIncrease, canUpdateWorkspace, @@ -56,6 +56,8 @@ export const WorkspaceStats: FC = ({ const displayTemplateName = getDisplayWorkspaceTemplateName(workspace); const deadlinePlusEnabled = maxDeadlineIncrease >= 1; const deadlineMinusEnabled = maxDeadlineDecrease >= 1; + const quotaQuery = useQuery(workspaceQuota(workspace.owner_name)); + const quotaBudget = quotaQuery.data?.budget; const paperStyles = css` padding: 24px; diff --git a/site/src/pages/WorkspacePage/permissions.ts b/site/src/pages/WorkspacePage/permissions.ts new file mode 100644 index 0000000000..043a5de708 --- /dev/null +++ b/site/src/pages/WorkspacePage/permissions.ts @@ -0,0 +1,39 @@ +import { Workspace, Template } from "api/typesGenerated"; + +export const workspaceChecks = (workspace: Workspace, template: Template) => + ({ + readWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "read", + }, + updateWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "update", + }, + updateTemplate: { + object: { + resource_type: "template", + resource_id: template.id, + }, + action: "update", + }, + viewDeploymentValues: { + object: { + resource_type: "deployment_config", + }, + action: "read", + }, + }) as const; + +export type WorkspacePermissions = Record< + keyof ReturnType, + boolean +>; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ab3de63140..ad822e76e2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -400,20 +400,6 @@ You can add instructions here archived: false, }; -export const MockTemplateVersion3: TypesGen.TemplateVersion = { - id: "test-template-version-3", - 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-3", - message: "first version", - readme: "README", - created_by: MockUser, - warnings: ["UNSUPPORTED_WORKSPACES"], - archived: false, -}; - export const MockTemplate: TypesGen.Template = { id: "test-template", created_at: "2022-05-17T17:39:01.382927298Z", diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index bfa06dde29..c95d82570c 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -147,44 +147,6 @@ export const defaultWorkspaceExtension = ( }; }; -// You can see the favicon designs here: https://www.figma.com/file/YIGBkXUcnRGz2ZKNmLaJQf/Coder-v2-Design?node-id=560%3A620 - -type FaviconType = - | "favicon" - | "favicon-success" - | "favicon-error" - | "favicon-warning" - | "favicon-running"; - -export const getFaviconByStatus = ( - build: TypesGen.WorkspaceBuild, -): FaviconType => { - switch (build.status) { - case undefined: - return "favicon"; - case "running": - return "favicon-success"; - case "starting": - return "favicon-running"; - case "stopping": - return "favicon-running"; - case "stopped": - return "favicon"; - case "deleting": - return "favicon"; - case "deleted": - return "favicon"; - case "canceling": - return "favicon-warning"; - case "canceled": - return "favicon"; - case "failed": - return "favicon-error"; - case "pending": - return "favicon"; - } -}; - export const getDisplayWorkspaceTemplateName = ( workspace: TypesGen.Workspace, ): string => { diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts deleted file mode 100644 index 00fb5f83e8..0000000000 --- a/site/src/xServices/workspace/workspaceXService.ts +++ /dev/null @@ -1,714 +0,0 @@ -import { getErrorMessage } from "api/errors"; -import { assign, createMachine } from "xstate"; -import * as API from "api/api"; -import * as TypesGen from "api/typesGenerated"; -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; - -type Permissions = Record, boolean>; - -export interface WorkspaceContext { - // Initial data - orgId: string; - username: string; - workspaceName: string; - - error?: unknown; - // our server side events instance - eventSource?: EventSource; - workspace?: TypesGen.Workspace; - template?: TypesGen.Template; - permissions?: Permissions; - templateVersion?: TypesGen.TemplateVersion; - deploymentValues?: TypesGen.DeploymentValues; - build?: TypesGen.WorkspaceBuild; - // Builds - builds?: TypesGen.WorkspaceBuild[]; - getBuildsError?: unknown; - missedParameters?: TypesGen.TemplateVersionParameter[]; - // error creating a new WorkspaceBuild - buildError?: unknown; - cancellationMessage?: TypesGen.Response; - cancellationError?: unknown; - // debug - createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"]; - // SSH Config - sshPrefix?: string; - // Change version - templateVersionIdToChange?: TypesGen.TemplateVersion["id"]; -} - -export type WorkspaceEvent = - | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } - | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } - | { type: "STOP" } - | { type: "ASK_DELETE" } - | { - type: "DELETE"; - orphan: TypesGen.CreateWorkspaceBuildRequest["orphan"]; - } - | { type: "CANCEL_DELETE" } - | { type: "UPDATE"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } - | { - type: "CHANGE_VERSION"; - templateVersionId: TypesGen.TemplateVersion["id"]; - buildParameters?: TypesGen.WorkspaceBuildParameter[]; - } - | { type: "CANCEL" } - | { - type: "REFRESH_TIMELINE"; - } - | { type: "EVENT_SOURCE_ERROR"; error: unknown } - | { type: "INCREASE_DEADLINE"; hours: number } - | { type: "DECREASE_DEADLINE"; hours: number } - | { - type: "RETRY_BUILD"; - orphan?: TypesGen.CreateWorkspaceBuildRequest["orphan"]; - } - | { type: "ACTIVATE" }; - -export const checks = { - readWorkspace: "readWorkspace", - updateWorkspace: "updateWorkspace", - updateTemplate: "updateTemplate", - viewDeploymentValues: "viewDeploymentValues", -} as const; - -const permissionsToCheck = ( - workspace: TypesGen.Workspace, - template: TypesGen.Template, -) => - ({ - [checks.readWorkspace]: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "read", - }, - [checks.updateWorkspace]: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "update", - }, - [checks.updateTemplate]: { - object: { - resource_type: "template", - resource_id: template.id, - }, - action: "update", - }, - [checks.viewDeploymentValues]: { - object: { - resource_type: "deployment_config", - }, - action: "read", - }, - }) as const; - -export const workspaceMachine = createMachine( - { - id: "workspaceState", - predictableActionArguments: true, - tsTypes: {} as import("./workspaceXService.typegen").Typegen0, - schema: { - context: {} as WorkspaceContext, - events: {} as WorkspaceEvent, - services: {} as { - loadInitialWorkspaceData: { - data: Awaited>; - }; - updateWorkspace: { - data: TypesGen.WorkspaceBuild; - }; - changeWorkspaceVersion: { - data: TypesGen.WorkspaceBuild; - }; - startWorkspace: { - data: TypesGen.WorkspaceBuild; - }; - stopWorkspace: { - data: TypesGen.WorkspaceBuild; - }; - deleteWorkspace: { - data: TypesGen.WorkspaceBuild; - }; - cancelWorkspace: { - data: TypesGen.Response; - }; - activateWorkspace: { - data: TypesGen.Response; - }; - listening: { - data: TypesGen.ServerSentEvent; - }; - getBuilds: { - data: TypesGen.WorkspaceBuild[]; - }; - getSSHPrefix: { - data: TypesGen.SSHConfigResponse; - }; - }, - }, - initial: "loadInitialData", - states: { - loadInitialData: { - entry: ["clearContext"], - invoke: { - src: "loadInitialWorkspaceData", - id: "loadInitialWorkspaceData", - onDone: [{ target: "ready", actions: ["assignInitialData"] }], - onError: [ - { - actions: "assignError", - target: "error", - }, - ], - }, - }, - ready: { - type: "parallel", - on: { - REFRESH_TIMELINE: { - actions: ["refreshBuilds"], - }, - }, - states: { - listening: { - initial: "gettingEvents", - states: { - gettingEvents: { - entry: ["initializeEventSource"], - exit: "closeEventSource", - invoke: { - src: "listening", - id: "listening", - }, - on: { - REFRESH_WORKSPACE: { - actions: ["refreshWorkspace"], - }, - EVENT_SOURCE_ERROR: { - target: "error", - }, - }, - }, - error: { - entry: "logWatchWorkspaceWarning", - after: { - "2000": { - target: "gettingEvents", - }, - }, - }, - }, - }, - build: { - initial: "idle", - states: { - idle: { - on: { - START: "requestingStart", - STOP: "requestingStop", - ASK_DELETE: "askingDelete", - UPDATE: "requestingUpdate", - CHANGE_VERSION: { - target: "requestingChangeVersion", - actions: ["assignTemplateVersionIdToChange"], - }, - CANCEL: "requestingCancel", - RETRY_BUILD: [ - { - target: "requestingStart", - cond: "lastBuildWasStarting", - actions: ["enableDebugMode"], - }, - { - target: "requestingStop", - cond: "lastBuildWasStopping", - actions: ["enableDebugMode"], - }, - { - target: "requestingDelete", - cond: "lastBuildWasDeleting", - actions: ["enableDebugMode"], - }, - ], - ACTIVATE: "requestingActivate", - }, - }, - askingDelete: { - on: { - DELETE: { - target: "requestingDelete", - }, - CANCEL_DELETE: { - target: "idle", - }, - }, - }, - requestingUpdate: { - entry: ["clearBuildError"], - invoke: { - src: "updateWorkspace", - onDone: { - target: "idle", - actions: ["assignBuild"], - }, - onError: [ - { - target: "askingForMissedBuildParameters", - cond: "isMissingBuildParameterError", - actions: ["assignMissedParameters"], - }, - { - target: "idle", - actions: ["assignBuildError"], - }, - ], - }, - }, - requestingChangeVersion: { - entry: ["clearBuildError"], - invoke: { - src: "changeWorkspaceVersion", - onDone: { - target: "idle", - actions: ["assignBuild", "clearTemplateVersionIdToChange"], - }, - onError: [ - { - target: "askingForMissedBuildParameters", - cond: "isMissingBuildParameterError", - actions: ["assignMissedParameters"], - }, - { - target: "idle", - actions: ["assignBuildError"], - }, - ], - }, - }, - askingForMissedBuildParameters: { - on: { - CANCEL: "idle", - UPDATE: [ - { - target: "requestingChangeVersion", - cond: "isChangingVersion", - }, - { target: "requestingUpdate" }, - ], - }, - }, - requestingStart: { - entry: ["clearBuildError"], - invoke: { - src: "startWorkspace", - id: "startWorkspace", - onDone: [ - { - actions: ["assignBuild", "disableDebugMode"], - target: "idle", - }, - ], - onError: [ - { - actions: "assignBuildError", - target: "idle", - }, - ], - }, - }, - requestingStop: { - entry: ["clearBuildError"], - invoke: { - src: "stopWorkspace", - id: "stopWorkspace", - onDone: [ - { - actions: ["assignBuild", "disableDebugMode"], - target: "idle", - }, - ], - onError: [ - { - actions: "assignBuildError", - target: "idle", - }, - ], - }, - }, - requestingDelete: { - entry: ["clearBuildError"], - invoke: { - src: "deleteWorkspace", - id: "deleteWorkspace", - onDone: [ - { - actions: ["assignBuild", "disableDebugMode"], - target: "idle", - }, - ], - onError: [ - { - actions: "assignBuildError", - target: "idle", - }, - ], - }, - }, - requestingCancel: { - entry: ["clearCancellationMessage", "clearCancellationError"], - invoke: { - src: "cancelWorkspace", - id: "cancelWorkspace", - onDone: [ - { - actions: [ - "assignCancellationMessage", - "displayCancellationMessage", - ], - target: "idle", - }, - ], - onError: [ - { - actions: "assignCancellationError", - target: "idle", - }, - ], - }, - }, - requestingActivate: { - entry: ["clearBuildError"], - invoke: { - src: "activateWorkspace", - id: "activateWorkspace", - onDone: "idle", - onError: { - target: "idle", - actions: ["displayActivateError"], - }, - }, - }, - }, - }, - sshConfig: { - initial: "gettingSshConfig", - states: { - gettingSshConfig: { - invoke: { - src: "getSSHPrefix", - onDone: { - target: "success", - actions: ["assignSSHPrefix"], - }, - onError: { - target: "error", - actions: ["displaySSHPrefixError"], - }, - }, - }, - error: { - type: "final", - }, - success: { - type: "final", - }, - }, - }, - }, - }, - error: { - type: "final", - }, - }, - }, - { - actions: { - // Clear data about an old workspace when looking at a new one - clearContext: () => - assign({ - workspace: undefined, - template: undefined, - build: undefined, - permissions: undefined, - eventSource: undefined, - }), - assignInitialData: assign({ - workspace: (_, event) => event.data.workspace, - template: (_, event) => event.data.template, - templateVersion: (_, event) => event.data.templateVersion, - permissions: (_, event) => event.data.permissions as Permissions, - deploymentValues: (_, event) => event.data.deploymentValues, - }), - assignError: assign({ - error: (_, event) => event.data, - }), - assignBuild: assign({ - build: (_, event) => event.data, - }), - assignBuildError: assign({ - buildError: (_, event) => event.data, - }), - clearBuildError: assign({ - buildError: (_) => undefined, - }), - assignCancellationMessage: assign({ - cancellationMessage: (_, event) => event.data, - }), - clearCancellationMessage: assign({ - cancellationMessage: (_) => undefined, - }), - displayCancellationMessage: (context) => { - if (context.cancellationMessage) { - displaySuccess(context.cancellationMessage.message); - } - }, - assignCancellationError: assign({ - cancellationError: (_, event) => event.data, - }), - clearCancellationError: assign({ - cancellationError: (_) => undefined, - }), - // SSE related actions - // open a new EventSource so we can stream SSE - initializeEventSource: assign({ - eventSource: (context) => - context.workspace && API.watchWorkspace(context.workspace.id), - }), - closeEventSource: (context) => - context.eventSource && context.eventSource.close(), - refreshWorkspace: assign({ - workspace: (_, event) => event.data, - }), - logWatchWorkspaceWarning: (_, event) => { - console.error("Watch workspace error:", event); - }, - // SSH - assignSSHPrefix: assign({ - sshPrefix: (_, { data }) => data.hostname_prefix, - }), - displaySSHPrefixError: (_, { data }) => { - const message = getErrorMessage( - data, - "Error getting the deployment ssh configuration.", - ); - displayError(message); - }, - displayActivateError: (_, { data }) => { - const message = getErrorMessage(data, "Error activate workspace."); - displayError(message); - }, - assignMissedParameters: assign({ - missedParameters: (_, { data }) => { - if (!(data instanceof API.MissingBuildParameters)) { - throw new Error("data is not a MissingBuildParameters error"); - } - return data.parameters; - }, - }), - // Debug mode when build fails - enableDebugMode: assign({ createBuildLogLevel: (_) => "debug" as const }), - disableDebugMode: assign({ createBuildLogLevel: (_) => undefined }), - // Change version - assignTemplateVersionIdToChange: assign({ - templateVersionIdToChange: (_, { templateVersionId }) => - templateVersionId, - }), - clearTemplateVersionIdToChange: assign({ - templateVersionIdToChange: (_) => undefined, - }), - }, - guards: { - isMissingBuildParameterError: (_, { data }) => { - return data instanceof API.MissingBuildParameters; - }, - lastBuildWasStarting: ({ workspace }) => { - return workspace?.latest_build.transition === "start"; - }, - lastBuildWasStopping: ({ workspace }) => { - return workspace?.latest_build.transition === "stop"; - }, - lastBuildWasDeleting: ({ workspace }) => { - return workspace?.latest_build.transition === "delete"; - }, - isChangingVersion: ({ templateVersionIdToChange }) => - Boolean(templateVersionIdToChange), - }, - services: { - loadInitialWorkspaceData, - updateWorkspace: - ({ workspace }, { buildParameters }) => - async (send) => { - if (!workspace) { - throw new Error("Workspace is not set"); - } - const build = await API.updateWorkspace(workspace, buildParameters); - send({ type: "REFRESH_TIMELINE" }); - return build; - }, - changeWorkspaceVersion: - ({ workspace, templateVersionIdToChange }, { buildParameters }) => - async (send) => { - if (!workspace) { - throw new Error("Workspace is not set"); - } - if (!templateVersionIdToChange) { - throw new Error("Template version id to change is not set"); - } - const build = await API.changeWorkspaceVersion( - workspace, - templateVersionIdToChange, - buildParameters, - ); - send({ type: "REFRESH_TIMELINE" }); - return build; - }, - startWorkspace: (context, data) => async (send) => { - if (context.workspace) { - const startWorkspacePromise = await API.startWorkspace( - context.workspace.id, - context.workspace.latest_build.template_version_id, - context.createBuildLogLevel, - "buildParameters" in data ? data.buildParameters : undefined, - ); - send({ type: "REFRESH_TIMELINE" }); - return startWorkspacePromise; - } else { - throw Error("Cannot start workspace without workspace id"); - } - }, - stopWorkspace: (context) => async (send) => { - if (context.workspace) { - const stopWorkspacePromise = await API.stopWorkspace( - context.workspace.id, - context.createBuildLogLevel, - ); - send({ type: "REFRESH_TIMELINE" }); - return stopWorkspacePromise; - } else { - throw Error("Cannot stop workspace without workspace id"); - } - }, - deleteWorkspace: (context, data) => async (send) => { - if (context.workspace) { - const deleteWorkspacePromise = await API.deleteWorkspace( - context.workspace.id, - { - log_level: context.createBuildLogLevel, - orphan: data.orphan, - }, - ); - send({ type: "REFRESH_TIMELINE" }); - return deleteWorkspacePromise; - } else { - throw Error("Cannot delete workspace without workspace id"); - } - }, - cancelWorkspace: (context) => async (send) => { - if (context.workspace) { - const cancelWorkspacePromise = await API.cancelWorkspaceBuild( - context.workspace.latest_build.id, - ); - send({ type: "REFRESH_TIMELINE" }); - return cancelWorkspacePromise; - } else { - throw Error("Cannot cancel workspace without build id"); - } - }, - activateWorkspace: (context) => async (send) => { - if (context.workspace) { - const activateWorkspacePromise = await API.updateWorkspaceDormancy( - context.workspace.id, - false, - ); - send({ type: "REFRESH_WORKSPACE", data: activateWorkspacePromise }); - return activateWorkspacePromise; - } else { - throw Error("Cannot activate workspace without workspace id"); - } - }, - listening: (context) => (send) => { - if (!context.eventSource) { - send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" }); - return; - } - - context.eventSource.addEventListener("data", (event) => { - const newWorkspaceData = JSON.parse(event.data) as TypesGen.Workspace; - // refresh our workspace with each SSE - send({ type: "REFRESH_WORKSPACE", data: newWorkspaceData }); - - const currentWorkspace = context.workspace!; - const hasNewBuild = - newWorkspaceData.latest_build.id !== - currentWorkspace.latest_build.id; - const lastBuildHasChanged = - newWorkspaceData.latest_build.status !== - currentWorkspace.latest_build.status; - - if (hasNewBuild || lastBuildHasChanged) { - send({ type: "REFRESH_TIMELINE" }); - } - }); - - // handle any error events returned by our sse - context.eventSource.addEventListener("error", (event) => { - send({ type: "EVENT_SOURCE_ERROR", error: event }); - }); - - // handle any sse implementation exceptions - context.eventSource.onerror = () => { - send({ type: "EVENT_SOURCE_ERROR", error: "sse error" }); - }; - - return () => { - context.eventSource?.close(); - }; - }, - getSSHPrefix: async () => { - return API.getDeploymentSSHConfig(); - }, - }, - }, -); - -async function loadInitialWorkspaceData({ - orgId, - username, - workspaceName, -}: WorkspaceContext) { - const workspace = await API.getWorkspaceByOwnerAndName( - username, - workspaceName, - { - include_deleted: true, - }, - ); - const template = await API.getTemplateByName(orgId, workspace.template_name); - const [templateVersion, permissions] = await Promise.all([ - API.getTemplateVersion(template.active_version_id), - API.checkAuthorization({ - checks: permissionsToCheck(workspace, template), - }), - ]); - - const canViewDeploymentValues = Boolean( - (permissions as Permissions)?.viewDeploymentValues, - ); - const deploymentValues = canViewDeploymentValues - ? (await API.getDeploymentConfig())?.config - : undefined; - return { - workspace, - template, - templateVersion, - permissions, - deploymentValues, - }; -}