chore(site): remove xstate (#10659)

This commit is contained in:
Bruno Quaresma 2023-11-14 15:34:38 -03:00 committed by GitHub
parent ef70165a8a
commit 90b6e86555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 536 additions and 1194 deletions

View File

@ -101,10 +101,6 @@ updates:
xterm:
patterns:
- "xterm*"
xstate:
patterns:
- "xstate"
- "@xstate*"
mui:
patterns:
- "@mui*"

1
.gitignore vendored
View File

@ -20,7 +20,6 @@ yarn-error.log
# Front-end ignore patterns.
.next/
site/**/*.typegen.ts
site/build-storybook.log
site/coverage/
site/storybook-static/

View File

@ -23,7 +23,6 @@ yarn-error.log
# Front-end ignore patterns.
.next/
site/**/*.typegen.ts
site/build-storybook.log
site/coverage/
site/storybook-static/

View File

@ -170,7 +170,6 @@
"wsconncache",
"wsjson",
"xerrors",
"xstate",
"yamux"
],
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],

View File

@ -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

View File

@ -23,7 +23,6 @@ yarn-error.log
# Front-end ignore patterns.
.next/
**/*.typegen.ts
build-storybook.log
coverage/
storybook-static/

View File

@ -23,7 +23,6 @@ yarn-error.log
# Front-end ignore patterns.
.next/
**/*.typegen.ts
build-storybook.log
coverage/
storybook-static/

View File

@ -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",

View File

@ -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'}

View File

@ -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<TypesGen.CreateWorkspaceBuildRequest, "log_level" & "orphan">,
options?: DeleteWorkspaceOptions,
) =>
postWorkspaceBuild(workspaceId, {
transition: "delete",
@ -1207,10 +1212,15 @@ export const removeLicense = async (licenseId: number): Promise<void> => {
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, {

View File

@ -27,3 +27,10 @@ export const health = () => {
queryFn: API.getHealth,
};
};
export const deploymentSSHConfig = () => {
return {
queryKey: ["deployment", "sshConfig"],
queryFn: API.getDeploymentSSHConfig,
};
};

View File

@ -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;

View File

@ -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<Workspace> => {
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),
});
};

View File

@ -1,16 +0,0 @@
import { TemplateVersionWarnings } from "./TemplateVersionWarnings";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta<typeof TemplateVersionWarnings> = {
title: "components/TemplateVersionWarnings",
component: TemplateVersionWarnings,
};
export default meta;
type Story = StoryObj<typeof TemplateVersionWarnings>;
export const UnsupportedWorkspaces: Story = {
args: {
warnings: ["UNSUPPORTED_WORKSPACES"],
},
};

View File

@ -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<TemplateVersionWarningsProps>
> = (props) => {
const { warnings = [] } = props;
if (!warnings.includes("UNSUPPORTED_WORKSPACES")) {
return null;
}
return (
<div data-testid="error-unsupported-workspaces">
<Alert severity="error">
This template uses legacy parameters which are not supported anymore.
Contact your administrator for assistance.
</Alert>
</div>
);
};

View File

@ -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

View File

@ -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],
},
};

View File

@ -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<TemplateSummaryPageViewProps> = ({
return (
<Stack spacing={4}>
<TemplateVersionWarnings warnings={activeVersion.warnings} />
<TemplateStats template={template} activeVersion={activeVersion} />
<TemplateResourcesTable resources={getStartedResources(resources)} />
</Stack>

View File

@ -77,6 +77,7 @@ describe("TerminalPage", () => {
expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith(
MockUser.username,
MockWorkspace.name,
{ include_deleted: true },
);
});
spy.mockRestore();

View File

@ -747,10 +747,3 @@ function makeFailedBuildLogs(): ProvisionerJobLog[] {
},
];
}
export const UnsupportedWorkspace: Story = {
args: {
...Running.args,
templateWarnings: ["UNSUPPORTED_WORKSPACES"],
},
};

View File

@ -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<React.PropsWithChildren<WorkspaceProps>> = ({
buildInfo,
sshPrefix,
template,
quotaBudget,
handleBuildRetry,
templateWarnings,
buildLogs,
onLoadMoreBuilds,
isLoadingMoreBuilds,
@ -198,7 +194,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
<WorkspaceStats
workspace={workspace}
quotaBudget={quotaBudget}
handleUpdate={handleUpdate}
canUpdateWorkspace={canUpdateWorkspace}
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
@ -294,8 +289,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
onDismiss={() => saveLocal("dismissedWorkspace", workspace.id)}
/>
<TemplateVersionWarnings warnings={templateWarnings} />
{showAlertPendingInQueue && (
<Alert severity="info">
<AlertTitle>Workspace build is pending</AlertTitle>

View File

@ -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({

View File

@ -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 <Loader />;
}
return (
<RequirePermission
isFeatureVisible={
!(isAxiosError(pageError) && pageError.response?.status === 404)
}
>
<WorkspaceReadyPage
workspaceState={workspaceState}
quota={quotaQuery.data}
workspaceSend={workspaceSend}
builds={buildsQuery.data?.pages.flat()}
buildsError={buildsQuery.error}
isLoadingMoreBuilds={buildsQuery.isFetchingNextPage}
onLoadMoreBuilds={async () => {
await buildsQuery.fetchNextPage();
}}
hasMoreBuilds={Boolean(buildsQuery.hasNextPage)}
canAutostart={canAutostart}
/>
</RequirePermission>
<WorkspaceReadyPage
workspace={workspace}
template={template}
permissions={permissions}
builds={buildsQuery.data?.pages.flat()}
buildsError={buildsQuery.error}
isLoadingMoreBuilds={buildsQuery.isFetchingNextPage}
onLoadMoreBuilds={async () => {
await buildsQuery.fetchNextPage();
}}
hasMoreBuilds={Boolean(buildsQuery.hasNextPage)}
/>
);
};

View File

@ -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<typeof workspaceMachine>;
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 (
<>
<Helmet>
@ -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 && (
<WorkspaceBuildLogsSection logs={buildLogs} />
@ -219,24 +299,50 @@ export const WorkspaceReadyPage = ({
/>
<WorkspaceDeleteDialog
workspace={workspace}
canUpdateTemplate={canUpdateTemplate}
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
onCancel={() => 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()}
/>
<UpdateBuildParametersDialog
missedParameters={missedParameters ?? []}
open={workspaceState.matches(
"ready.build.askingForMissedBuildParameters",
)}
missedParameters={
changeVersionMutation.error instanceof MissingBuildParameters
? changeVersionMutation.error.parameters
: []
}
open={changeVersionMutation.error instanceof MissingBuildParameters}
onClose={() => {
workspaceSend({ type: "CANCEL" });
changeVersionMutation.reset();
}}
onUpdate={(buildParameters) => {
workspaceSend({ type: "UPDATE", buildParameters });
if (changeVersionMutation.error instanceof MissingBuildParameters) {
changeVersionMutation.mutate({
versionId: changeVersionMutation.error.versionId,
buildParameters,
});
}
}}
/>
<UpdateBuildParametersDialog
missedParameters={
updateWorkspaceMutation.error instanceof MissingBuildParameters
? updateWorkspaceMutation.error.parameters
: []
}
open={updateWorkspaceMutation.error instanceof MissingBuildParameters}
onClose={() => {
updateWorkspaceMutation.reset();
}}
onUpdate={(buildParameters) => {
if (updateWorkspaceMutation.error instanceof MissingBuildParameters) {
updateWorkspaceMutation.mutate(buildParameters);
}
}}
/>
<ChangeVersionDialog
@ -251,16 +357,13 @@ export const WorkspaceReadyPage = ({
}}
onConfirm={(templateVersion) => {
setChangeVersionDialogOpen(false);
workspaceSend({
type: "CHANGE_VERSION",
templateVersionId: templateVersion.id,
});
changeVersionMutation.mutate({ versionId: templateVersion.id });
}}
/>
<WarningDialog
open={isConfirmingUpdate}
onConfirm={() => {
workspaceSend({ type: "UPDATE" });
updateWorkspaceMutation.mutate(undefined);
setIsConfirmingUpdate(false);
}}
onClose={() => setIsConfirmingUpdate(false)}
@ -310,3 +413,38 @@ const WarningDialog: FC<
> = (props) => {
return <ConfirmDialog type="info" hideCancel={false} {...props} />;
};
// 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";
}
};

View File

@ -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<WorkspaceStatsProps> = ({
workspace,
quotaBudget,
maxDeadlineDecrease,
maxDeadlineIncrease,
canUpdateWorkspace,
@ -56,6 +56,8 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
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;

View File

@ -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<typeof workspaceChecks>,
boolean
>;

View File

@ -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",

View File

@ -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 => {

View File

@ -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<keyof ReturnType<typeof permissionsToCheck>, 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<ReturnType<typeof loadInitialWorkspaceData>>;
};
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,
};
}