From 3c994741e45be7aa642243bdb86a990da694ed97 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 18:20:38 +0000 Subject: [PATCH 01/30] Remove template deprecation warning --- .../TemplateVersionWarnings.stories.tsx | 16 ------------ .../TemplateVersionWarnings.tsx | 26 ------------------- .../TemplateSummaryPageView.tsx | 2 -- site/src/pages/WorkspacePage/Workspace.tsx | 5 ---- .../WorkspacePage/WorkspaceReadyPage.tsx | 2 -- .../xServices/workspace/workspaceXService.ts | 12 +++------ 6 files changed, 3 insertions(+), 60 deletions(-) delete mode 100644 site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx delete mode 100644 site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx diff --git a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx deleted file mode 100644 index fe7032304c6e8..0000000000000 --- 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 473488bda92bb..0000000000000 --- 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/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx index a54f96f91d57f..f73e7b1592023 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/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 761592d32f25b..976a3bee9e223 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; @@ -106,7 +104,6 @@ export const Workspace: FC> = ({ template, quotaBudget, handleBuildRetry, - templateWarnings, buildLogs, onLoadMoreBuilds, isLoadingMoreBuilds, @@ -271,8 +268,6 @@ export const Workspace: FC> = ({ onDismiss={() => saveLocal("dismissedWorkspace", workspace.id)} /> - - {showAlertPendingInQueue && ( Workspace build is pending diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 34cbb665c85a8..3cce2985be6ed 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -63,7 +63,6 @@ export const WorkspaceReadyPage = ({ const { workspace, template, - templateVersion: currentVersion, deploymentValues, buildError, cancellationError, @@ -207,7 +206,6 @@ export const WorkspaceReadyPage = ({ sshPrefix={sshPrefix} template={template} quotaBudget={quota?.budget} - templateWarnings={currentVersion?.warnings} buildLogs={ shouldDisplayBuildLogs && ( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 4c41695d9a59d..5a647c218c811 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -18,7 +18,6 @@ export interface WorkspaceContext { workspace?: TypesGen.Workspace; template?: TypesGen.Template; permissions?: Permissions; - templateVersion?: TypesGen.TemplateVersion; deploymentValues?: TypesGen.DeploymentValues; build?: TypesGen.WorkspaceBuild; // Builds @@ -436,7 +435,6 @@ export const workspaceMachine = createMachine( 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, }), @@ -682,12 +680,9 @@ async function loadInitialWorkspaceData({ }, ); 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 permissions = await API.checkAuthorization({ + checks: permissionsToCheck(workspace, template), + }); const canViewDeploymentValues = Boolean( (permissions as Permissions)?.viewDeploymentValues, @@ -698,7 +693,6 @@ async function loadInitialWorkspaceData({ return { workspace, template, - templateVersion, permissions, deploymentValues, }; From 878f99594c9190adb751a991e01d868c643654ef Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 18:30:35 +0000 Subject: [PATCH 02/30] Move deployment values fetching --- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 12 ++++++++---- site/src/xServices/workspace/workspaceXService.ts | 10 ---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 3cce2985be6ed..2d64ec4f1c1c9 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -36,6 +36,7 @@ import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { decreaseDeadline, increaseDeadline } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; +import { deploymentConfig } from "api/queries/deployment"; interface WorkspaceReadyPageProps { workspaceState: StateFrom; @@ -63,7 +64,6 @@ export const WorkspaceReadyPage = ({ const { workspace, template, - deploymentValues, buildError, cancellationError, sshPrefix, @@ -76,9 +76,13 @@ export const WorkspaceReadyPage = ({ 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 { data: deploymentValues } = useQuery({ + ...deploymentConfig(), + enabled: permissions?.viewDeploymentValues, + }); + const canRetryDebugMode = Boolean( + deploymentValues?.config.enable_terraform_debug_mode, + ); const favicon = getFaviconByStatus(workspace.latest_build); const navigate = useNavigate(); const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 5a647c218c811..15afff1a21add 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -11,14 +11,12 @@ export interface WorkspaceContext { orgId: string; username: string; workspaceName: string; - error?: unknown; // our server side events instance eventSource?: EventSource; workspace?: TypesGen.Workspace; template?: TypesGen.Template; permissions?: Permissions; - deploymentValues?: TypesGen.DeploymentValues; build?: TypesGen.WorkspaceBuild; // Builds builds?: TypesGen.WorkspaceBuild[]; @@ -436,7 +434,6 @@ export const workspaceMachine = createMachine( workspace: (_, event) => event.data.workspace, template: (_, event) => event.data.template, permissions: (_, event) => event.data.permissions as Permissions, - deploymentValues: (_, event) => event.data.deploymentValues, }), assignError: assign({ error: (_, event) => event.data, @@ -684,16 +681,9 @@ async function loadInitialWorkspaceData({ checks: permissionsToCheck(workspace, template), }); - const canViewDeploymentValues = Boolean( - (permissions as Permissions)?.viewDeploymentValues, - ); - const deploymentValues = canViewDeploymentValues - ? (await API.getDeploymentConfig())?.config - : undefined; return { workspace, template, permissions, - deploymentValues, }; } From cdf885938f59039ffa0324362837198de366038b Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 18:32:29 +0000 Subject: [PATCH 03/30] Remove unused code from template warning --- .../TemplateSummaryPageView.stories.tsx | 9 --------- .../pages/WorkspacePage/WorkspacePage.test.tsx | 15 --------------- site/src/testHelpers/entities.ts | 14 -------------- 3 files changed, 38 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index edb53c9ee184c..0af2aa64884e4 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/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 8a79bccea4513..78e137353ce53 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, } from "testHelpers/entities"; @@ -267,20 +266,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/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0a8cf0e1b3ff2..24dbc050ecc1b 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -401,20 +401,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", From 964c3d224dc78592d571d0cb824de256b6daf503 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 18:57:47 +0000 Subject: [PATCH 04/30] Extract initial data --- site/src/api/queries/workspaces.ts | 3 +- .../src/pages/WorkspacePage/WorkspacePage.tsx | 41 ++++++- .../WorkspacePage/WorkspaceReadyPage.tsx | 23 ++-- site/src/pages/WorkspacePage/permissions.ts | 39 +++++++ .../xServices/workspace/workspaceXService.ts | 106 +----------------- 5 files changed, 96 insertions(+), 116 deletions(-) create mode 100644 site/src/pages/WorkspacePage/permissions.ts diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index f37c65f9ed589..c1429d53518f0 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -24,7 +24,8 @@ export const workspaceByOwnerAndName = ( ): QueryOptions => { return { queryKey: workspaceByOwnerAndNameKey(owner, name), - queryFn: () => API.getWorkspaceByOwnerAndName(owner, name), + queryFn: () => + API.getWorkspaceByOwnerAndName(owner, name, { include_deleted: true }), }; }; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 2c8dc3d8b9d7d..a459d2d02be0a 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -12,6 +12,10 @@ import { Margins } from "components/Margins/Margins"; import { workspaceQuota } from "api/queries/workspaceQuota"; import { useInfiniteQuery, useQuery } 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"; export const WorkspacePage: FC = () => { const params = useParams() as { @@ -33,14 +37,40 @@ export const WorkspacePage: FC = () => { }, }, }); - const { workspace, error } = workspaceState.context; + + const workspaceQuery = useQuery( + workspaceByOwnerAndName(username, workspaceName), + ); + const workspace = workspaceQuery.data; + + const templateQuery = useQuery({ + ...templateByName(orgId, workspace?.template_name ?? ""), + enabled: workspace !== undefined, + }); + const template = templateQuery.data; + + const checks = + workspace && template ? workspaceChecks(workspace, template) : {}; + const permissionsQuery = useQuery({ + ...checkAuthorization({ checks }), + enabled: workspace !== undefined && template !== undefined, + }); + const permissions = permissionsQuery.data as WorkspacePermissions | undefined; + const quotaQuery = useQuery(workspaceQuota(username)); - const pageError = error ?? quotaQuery.error; + const buildsQuery = useInfiniteQuery({ ...infiniteWorkspaceBuilds(workspace?.id ?? ""), - enabled: Boolean(workspace), + enabled: workspace !== undefined, }); + const pageError = + workspaceQuery.error ?? + templateQuery.error ?? + quotaQuery.error ?? + permissionsQuery.error; + const isLoading = !workspace || !template || !permissions || !quotaQuery.data; + if (pageError) { return ( @@ -49,7 +79,7 @@ export const WorkspacePage: FC = () => { ); } - if (!workspace || !workspaceState.matches("ready") || !quotaQuery.isSuccess) { + if (isLoading) { return ; } @@ -60,6 +90,9 @@ export const WorkspacePage: FC = () => { } > ; + template: TypesGen.Template; + workspace: TypesGen.Workspace; + permissions: WorkspacePermissions; + workspaceState: Omit< + StateFrom, + "template" | "workspace" | "permissions" + >; workspaceSend: (event: WorkspaceEvent) => void; quota?: TypesGen.WorkspaceQuota; builds: TypesGen.WorkspaceBuild[] | undefined; @@ -50,6 +57,9 @@ interface WorkspaceReadyPageProps { } export const WorkspaceReadyPage = ({ + workspace, + template, + permissions, workspaceState, workspaceSend, quota, @@ -61,15 +71,8 @@ export const WorkspaceReadyPage = ({ }: WorkspaceReadyPageProps): JSX.Element => { const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); - const { - workspace, - template, - buildError, - cancellationError, - sshPrefix, - permissions, - missedParameters, - } = workspaceState.context; + const { buildError, cancellationError, sshPrefix, missedParameters } = + workspaceState.context; if (workspace === undefined) { throw Error("Workspace is undefined"); } diff --git a/site/src/pages/WorkspacePage/permissions.ts b/site/src/pages/WorkspacePage/permissions.ts new file mode 100644 index 0000000000000..043a5de7086ce --- /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/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 15afff1a21add..7c0a21985d8c8 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -4,8 +4,6 @@ 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; @@ -35,6 +33,7 @@ export interface WorkspaceContext { } export type WorkspaceEvent = + | { type: "LOAD" } | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "STOP" } @@ -57,49 +56,6 @@ export type WorkspaceEvent = | { type: "RETRY_BUILD" } | { 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", @@ -109,9 +65,6 @@ export const workspaceMachine = createMachine( context: {} as WorkspaceContext, events: {} as WorkspaceEvent, services: {} as { - loadInitialWorkspaceData: { - data: Awaited>; - }; updateWorkspace: { data: TypesGen.WorkspaceBuild; }; @@ -147,17 +100,10 @@ export const workspaceMachine = createMachine( initial: "loadInitialData", states: { loadInitialData: { - entry: ["clearContext"], - invoke: { - src: "loadInitialWorkspaceData", - id: "loadInitialWorkspaceData", - onDone: [{ target: "ready", actions: ["assignInitialData"] }], - onError: [ - { - actions: "assignError", - target: "error", - }, - ], + on: { + LOAD: { + target: "ready", + }, }, }, ready: { @@ -421,23 +367,6 @@ export const workspaceMachine = createMachine( }, { 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, - permissions: (_, event) => event.data.permissions as Permissions, - }), - assignError: assign({ - error: (_, event) => event.data, - }), assignBuild: assign({ build: (_, event) => event.data, }), @@ -530,7 +459,6 @@ export const workspaceMachine = createMachine( Boolean(templateVersionIdToChange), }, services: { - loadInitialWorkspaceData, updateWorkspace: ({ workspace }, { buildParameters }) => async (send) => { @@ -663,27 +591,3 @@ export const workspaceMachine = createMachine( }, }, ); - -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 permissions = await API.checkAuthorization({ - checks: permissionsToCheck(workspace, template), - }); - - return { - workspace, - template, - permissions, - }; -} From 2cd9de1062e03b2fdf3585848caa724d5fde4834 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 19:00:26 +0000 Subject: [PATCH 05/30] Move quota fetching to be used close to where it is used --- site/src/pages/WorkspacePage/Workspace.tsx | 2 -- site/src/pages/WorkspacePage/WorkspacePage.tsx | 11 ++--------- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 3 --- site/src/pages/WorkspacePage/WorkspaceStats.tsx | 6 ++++-- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 976a3bee9e223..9705c63a16295 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -102,7 +102,6 @@ export const Workspace: FC> = ({ buildInfo, sshPrefix, template, - quotaBudget, handleBuildRetry, buildLogs, onLoadMoreBuilds, @@ -185,7 +184,6 @@ export const Workspace: FC> = ({ { }); const permissions = permissionsQuery.data as WorkspacePermissions | undefined; - const quotaQuery = useQuery(workspaceQuota(username)); - const buildsQuery = useInfiniteQuery({ ...infiniteWorkspaceBuilds(workspace?.id ?? ""), enabled: workspace !== undefined, }); const pageError = - workspaceQuery.error ?? - templateQuery.error ?? - quotaQuery.error ?? - permissionsQuery.error; - const isLoading = !workspace || !template || !permissions || !quotaQuery.data; + workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; + const isLoading = !workspace || !template || !permissions; if (pageError) { return ( @@ -94,7 +88,6 @@ export const WorkspacePage: FC = () => { template={template} permissions={permissions} workspaceState={workspaceState} - quota={quotaQuery.data} workspaceSend={workspaceSend} builds={buildsQuery.data?.pages.flat()} buildsError={buildsQuery.error} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 8c9b3ff701570..329c548ad6673 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -48,7 +48,6 @@ interface WorkspaceReadyPageProps { "template" | "workspace" | "permissions" >; workspaceSend: (event: WorkspaceEvent) => void; - quota?: TypesGen.WorkspaceQuota; builds: TypesGen.WorkspaceBuild[] | undefined; buildsError: unknown; onLoadMoreBuilds: () => void; @@ -62,7 +61,6 @@ export const WorkspaceReadyPage = ({ permissions, workspaceState, workspaceSend, - quota, builds, buildsError, onLoadMoreBuilds, @@ -212,7 +210,6 @@ export const WorkspaceReadyPage = ({ buildInfo={buildInfo} sshPrefix={sshPrefix} template={template} - quotaBudget={quota?.budget} buildLogs={ shouldDisplayBuildLogs && ( diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index 72c703ea63f54..a173d04d1b339 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; From 4fd62bb920bdc4272843949bab3fd85786910a73 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 9 Nov 2023 19:04:49 +0000 Subject: [PATCH 06/30] Fix test on Terminal because of workspace query changes --- site/src/pages/TerminalPage/TerminalPage.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx index 064a892d3564e..c6f9aee13629a 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(); From b6ee59a2da06a46be44bbdb9d8faec10691a14a1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 13:07:26 +0000 Subject: [PATCH 07/30] Move can suto start to be on WorkspaceReadyPage --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 7 ------- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 8 ++++++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e249c5d8ff64a..032c3341426d5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -9,7 +9,6 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { useOrganizationId } from "hooks"; import { isAxiosError } from "axios"; import { Margins } from "components/Margins/Margins"; -import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { useInfiniteQuery, useQuery } from "react-query"; import { infiniteWorkspaceBuilds } from "api/queries/workspaceBuilds"; import { templateByName } from "api/queries/templates"; @@ -65,11 +64,6 @@ export const WorkspacePage: FC = () => { const pageError = workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; const isLoading = !workspace || !template || !permissions; - const canAutostartResponse = useQuery( - workspaceResolveAutostart(workspace?.id ?? ""), - ); - - const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; if (pageError) { return ( @@ -102,7 +96,6 @@ export const WorkspacePage: FC = () => { await buildsQuery.fetchNextPage(); }} hasMoreBuilds={Boolean(buildsQuery.hasNextPage)} - canAutostart={canAutostart} /> ); diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index b266bdb728514..8b12e6c720aa6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -38,6 +38,7 @@ import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; import { deploymentConfig } from "api/queries/deployment"; import { WorkspacePermissions } from "./permissions"; +import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; interface WorkspaceReadyPageProps { template: TypesGen.Template; @@ -53,7 +54,6 @@ interface WorkspaceReadyPageProps { onLoadMoreBuilds: () => void; isLoadingMoreBuilds: boolean; hasMoreBuilds: boolean; - canAutostart: boolean; } export const WorkspaceReadyPage = ({ @@ -67,7 +67,6 @@ export const WorkspaceReadyPage = ({ onLoadMoreBuilds, isLoadingMoreBuilds, hasMoreBuilds, - canAutostart, }: WorkspaceReadyPageProps): JSX.Element => { const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); @@ -146,6 +145,11 @@ export const WorkspaceReadyPage = ({ onError: onDeadlineChangeFails, }); + const canAutostartResponse = useQuery( + workspaceResolveAutostart(workspace.id), + ); + const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; + return ( <> From 2388cae5c3fc968c4f4ffa1c7ce38a6a47e0d167 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 13:19:50 +0000 Subject: [PATCH 08/30] Extract ssh config --- site/src/api/queries/deployment.ts | 7 +++ .../WorkspacePage/WorkspaceReadyPage.tsx | 8 +-- .../xServices/workspace/workspaceXService.ts | 50 +------------------ 3 files changed, 13 insertions(+), 52 deletions(-) diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 2ad91c7724ed6..a8d066b41f9b2 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/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 8b12e6c720aa6..b0565244b567c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -36,7 +36,7 @@ import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { decreaseDeadline, increaseDeadline } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; -import { deploymentConfig } from "api/queries/deployment"; +import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; import { WorkspacePermissions } from "./permissions"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; @@ -70,7 +70,7 @@ export const WorkspaceReadyPage = ({ }: WorkspaceReadyPageProps): JSX.Element => { const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); - const { buildError, cancellationError, sshPrefix, missedParameters } = + const { buildError, cancellationError, missedParameters } = workspaceState.context; if (workspace === undefined) { throw Error("Workspace is undefined"); @@ -150,6 +150,8 @@ export const WorkspaceReadyPage = ({ ); const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; + const sshPrefixQuery = useQuery(deploymentSSHConfig()); + return ( <> @@ -214,7 +216,7 @@ export const WorkspaceReadyPage = ({ [WorkspaceErrors.CANCELLATION_ERROR]: cancellationError, }} buildInfo={buildInfo} - sshPrefix={sshPrefix} + sshPrefix={sshPrefixQuery.data?.hostname_prefix} template={template} buildLogs={ shouldDisplayBuildLogs && ( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 7c0a21985d8c8..fa0dfbd3db69e 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -26,14 +26,11 @@ export interface WorkspaceContext { cancellationError?: unknown; // debug createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"]; - // SSH Config - sshPrefix?: string; // Change version templateVersionIdToChange?: TypesGen.TemplateVersion["id"]; } export type WorkspaceEvent = - | { type: "LOAD" } | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "STOP" } @@ -97,15 +94,8 @@ export const workspaceMachine = createMachine( }; }, }, - initial: "loadInitialData", + initial: "ready", states: { - loadInitialData: { - on: { - LOAD: { - target: "ready", - }, - }, - }, ready: { type: "parallel", on: { @@ -334,30 +324,6 @@ export const workspaceMachine = createMachine( }, }, }, - 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: { @@ -407,17 +373,6 @@ export const workspaceMachine = createMachine( 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); @@ -585,9 +540,6 @@ export const workspaceMachine = createMachine( context.eventSource?.close(); }; }, - getSSHPrefix: async () => { - return API.getDeploymentSSHConfig(); - }, }, }, ); From 3d96b94496a4e1ec7e9727ca2384c7461ca7efe4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 16:36:32 +0000 Subject: [PATCH 09/30] Extract watching workspace out of xstate --- site/src/api/queries/workspaces.ts | 5 +- .../pages/WorkspacePage/Workspace.stories.tsx | 7 -- .../src/pages/WorkspacePage/WorkspacePage.tsx | 50 +++++++++++- .../xServices/workspace/workspaceXService.ts | 80 ------------------- 4 files changed, 47 insertions(+), 95 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index c1429d53518f0..c0746648c6490 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -18,10 +18,7 @@ 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: () => diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index a455812f125eb..10887f5b5ebdc 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/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 032c3341426d5..575e8b8963f55 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,6 +1,6 @@ import { useMachine } from "@xstate/react"; import { Loader } from "components/Loader/Loader"; -import { FC } from "react"; +import { FC, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { workspaceMachine } from "xServices/workspace/workspaceXService"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; @@ -9,14 +9,17 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { useOrganizationId } from "hooks"; import { isAxiosError } from "axios"; import { Margins } from "components/Margins/Margins"; -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"; export const WorkspacePage: FC = () => { + const queryClient = useQueryClient(); const params = useParams() as { username: string; workspace: string; @@ -37,9 +40,11 @@ export const WorkspacePage: FC = () => { }, }); - const workspaceQuery = useQuery( - workspaceByOwnerAndName(username, workspaceName), + const workspaceQueryOptions = workspaceByOwnerAndName( + username, + workspaceName, ); + const workspaceQuery = useQuery(workspaceQueryOptions); const workspace = workspaceQuery.data; const templateQuery = useQuery({ @@ -65,6 +70,43 @@ export const WorkspacePage: FC = () => { workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; const isLoading = !workspace || !template || !permissions; + // Watch workspace changes + const workspaceEventSource = useRef(null); + useEffect(() => { + // If there is an event source, we are already watching the workspace + if (!workspace || workspaceEventSource.current) { + return; + } + + const eventSource = watchWorkspace(workspace.id); + workspaceEventSource.current = eventSource; + + eventSource.addEventListener("data", async (event) => { + const newWorkspaceData = JSON.parse(event.data) as Workspace; + queryClient.setQueryData( + workspaceQueryOptions.queryKey, + newWorkspaceData, + ); + + 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(); + } + }); + + eventSource.addEventListener("error", (event) => { + console.error("Error on getting workspace changes.", event); + }); + + return () => { + eventSource.close(); + }; + }, [buildsQuery, queryClient, workspace, workspaceQueryOptions.queryKey]); + if (pageError) { return ( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index fa0dfbd3db69e..4a7bcec71df37 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -104,35 +104,6 @@ export const workspaceMachine = createMachine( }, }, 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: { @@ -359,20 +330,6 @@ export const workspaceMachine = createMachine( 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); - }, displayActivateError: (_, { data }) => { const message = getErrorMessage(data, "Error activate workspace."); displayError(message); @@ -502,44 +459,7 @@ export const workspaceMachine = createMachine( 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(); - }; - }, }, }, ); From b0fba32badf6d2d753f34e81fd239ca81ea53a42 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 16:41:10 +0000 Subject: [PATCH 10/30] Remove unused code --- site/src/xServices/workspace/workspaceXService.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 4a7bcec71df37..ea379878411bf 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -47,9 +47,6 @@ export type WorkspaceEvent = | { type: "REFRESH_TIMELINE"; } - | { type: "EVENT_SOURCE_ERROR"; error: unknown } - | { type: "INCREASE_DEADLINE"; hours: number } - | { type: "DECREASE_DEADLINE"; hours: number } | { type: "RETRY_BUILD" } | { type: "ACTIVATE" }; @@ -83,15 +80,6 @@ export const workspaceMachine = createMachine( activateWorkspace: { data: TypesGen.Response; }; - listening: { - data: TypesGen.ServerSentEvent; - }; - getBuilds: { - data: TypesGen.WorkspaceBuild[]; - }; - getSSHPrefix: { - data: TypesGen.SSHConfigResponse; - }; }, }, initial: "ready", @@ -459,7 +447,6 @@ export const workspaceMachine = createMachine( throw Error("Cannot activate workspace without workspace id"); } }, - }, }, ); From 9edfd717f1673ccb7fc18e352f93bd8cf11ab73d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 16:51:13 +0000 Subject: [PATCH 11/30] Organize code on ready page --- .../WorkspacePage/WorkspaceReadyPage.tsx | 79 +++++++++++++++---- site/src/utils/workspace.tsx | 38 --------- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index b0565244b567c..b9929c9cebf89 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -14,7 +14,7 @@ import { StateFrom } from "xstate"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { Workspace, WorkspaceErrors } from "./Workspace"; import { pageTitle } from "utils/page"; -import { getFaviconByStatus, hasJobError } from "utils/workspace"; +import { hasJobError } from "utils/workspace"; import { WorkspaceEvent, workspaceMachine, @@ -68,6 +68,7 @@ export const WorkspaceReadyPage = ({ isLoadingMoreBuilds, hasMoreBuilds, }: WorkspaceReadyPageProps): JSX.Element => { + const navigate = useNavigate(); const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); const { buildError, cancellationError, missedParameters } = @@ -75,7 +76,6 @@ export const WorkspaceReadyPage = ({ if (workspace === undefined) { throw Error("Workspace is undefined"); } - const deadline = getDeadline(workspace); const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); const canUpdateTemplate = Boolean(permissions?.updateTemplate); const { data: deploymentValues } = useQuery({ @@ -85,15 +85,10 @@ export const WorkspaceReadyPage = ({ const canRetryDebugMode = Boolean( deploymentValues?.config.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 }); + // Versions const { data: allVersions } = useQuery({ ...templateVersions(workspace.template_id), enabled: changeVersionDialogOpen, @@ -102,22 +97,20 @@ export const WorkspaceReadyPage = ({ ...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 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, @@ -126,6 +119,8 @@ export const WorkspaceReadyPage = ({ mutationFn: restartWorkspace, }); + // Schedule controls + const deadline = getDeadline(workspace); const onDeadlineChangeSuccess = () => { displaySuccess("Updated workspace shutdown time."); }; @@ -145,13 +140,28 @@ 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"); + }, []); + return ( <> @@ -320,3 +330,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/utils/workspace.tsx b/site/src/utils/workspace.tsx index bfa06dde290ef..c95d82570c5ef 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 => { From 3e1de931abc2a0d17cf6459a3df647b03c93a788 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:17:02 +0000 Subject: [PATCH 12/30] Extract change version from xstate --- site/src/api/api.ts | 11 ++- site/src/api/queries/workspaces.ts | 29 ++++++++ .../WorkspacePage/WorkspacePage.test.tsx | 8 +-- .../WorkspacePage/WorkspaceReadyPage.tsx | 62 ++++++++++++----- .../xServices/workspace/workspaceXService.ts | 68 +------------------ 5 files changed, 86 insertions(+), 92 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ae26b8b0cbb80..ec1abec0612f2 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1207,10 +1207,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 +1244,7 @@ export const changeWorkspaceVersion = async ( ); if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters); + throw new MissingBuildParameters(missingParameters, templateVersionId); } return postWorkspaceBuild(workspace.id, { @@ -1277,7 +1282,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/workspaces.ts b/site/src/api/queries/workspaces.ts index c0746648c6490..e03ef03ffc1c1 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -9,6 +9,7 @@ import { type CreateWorkspaceRequest, type WorkspacesResponse, type WorkspacesRequest, + WorkspaceBuild, } from "api/typesGenerated"; export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ @@ -122,3 +123,31 @@ 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: (build: WorkspaceBuild) => { + const workspaceKey = workspaceByOwnerAndNameKey( + build.workspace_owner_name, + build.workspace_name, + ); + const previousData = queryClient.getQueryData(workspaceKey) as Workspace; + queryClient.setQueryData(workspaceKey, { + ...previousData, + latest_build: build, + }); + }, + }; +}; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 78e137353ce53..7445c026209a2 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -201,10 +201,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 diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index b9929c9cebf89..b2b06a65d40b1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -21,8 +21,8 @@ import { } from "xServices/workspace/workspaceXService"; 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, @@ -33,7 +33,11 @@ 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 { + changeVersion, + decreaseDeadline, + increaseDeadline, +} from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; @@ -69,6 +73,7 @@ export const WorkspaceReadyPage = ({ hasMoreBuilds, }: WorkspaceReadyPageProps): JSX.Element => { const navigate = useNavigate(); + const queryClient = useQueryClient(); const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); const { buildError, cancellationError, missedParameters } = @@ -85,19 +90,8 @@ export const WorkspaceReadyPage = ({ const canRetryDebugMode = Boolean( deploymentValues?.config.enable_terraform_debug_mode, ); - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); - // Versions - const { data: allVersions } = useQuery({ - ...templateVersions(workspace.template_id), - enabled: changeVersionDialogOpen, - }); - const { data: latestVersion } = useQuery({ - ...templateVersion(workspace.template_active_version_id), - enabled: workspace.outdated, - }); - // Build logs const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id); const shouldDisplayBuildLogs = @@ -162,6 +156,22 @@ export const WorkspaceReadyPage = ({ setFaviconTheme(isDark.matches ? "light" : "dark"); }, []); + // Change version + 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, + }); + return ( <> @@ -247,6 +257,25 @@ export const WorkspaceReadyPage = ({ workspaceSend({ type: "DELETE" }); }} /> + { + changeVersionMutation.reset(); + }} + onUpdate={(buildParameters) => { + if (changeVersionMutation.error instanceof MissingBuildParameters) { + changeVersionMutation.mutate({ + versionId: changeVersionMutation.error.versionId, + buildParameters, + }); + } + }} + /> { setChangeVersionDialogOpen(false); - workspaceSend({ - type: "CHANGE_VERSION", - templateVersionId: templateVersion.id, - }); + changeVersionMutation.mutate({ versionId: templateVersion.id }); }} /> "debug" as const }), disableDebugMode: assign({ createBuildLogLevel: (_) => undefined }), - // Change version - assignTemplateVersionIdToChange: assign({ - templateVersionIdToChange: (_, { templateVersionId }) => - templateVersionId, - }), - clearTemplateVersionIdToChange: assign({ - templateVersionIdToChange: (_) => undefined, - }), }, guards: { isMissingBuildParameterError: (_, { data }) => { @@ -355,8 +308,6 @@ export const workspaceMachine = createMachine( lastBuildWasDeleting: ({ workspace }) => { return workspace?.latest_build.transition === "delete"; }, - isChangingVersion: ({ templateVersionIdToChange }) => - Boolean(templateVersionIdToChange), }, services: { updateWorkspace: @@ -369,23 +320,6 @@ export const workspaceMachine = createMachine( 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( From ab8977cdfcc647881fd17b4d8fcfb97ea05f1bd8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:31:09 +0000 Subject: [PATCH 13/30] Extract update workspace --- site/src/api/queries/workspaces.ts | 36 +++++++++--- .../WorkspacePage/WorkspaceReadyPage.tsx | 36 +++++++----- .../xServices/workspace/workspaceXService.ts | 55 +------------------ 3 files changed, 50 insertions(+), 77 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index e03ef03ffc1c1..247336d4acfee 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -139,15 +139,33 @@ export const changeVersion = ( return API.changeWorkspaceVersion(workspace, versionId, buildParameters); }, onSuccess: (build: WorkspaceBuild) => { - const workspaceKey = workspaceByOwnerAndNameKey( - build.workspace_owner_name, - build.workspace_name, - ); - const previousData = queryClient.getQueryData(workspaceKey) as Workspace; - queryClient.setQueryData(workspaceKey, { - ...previousData, - latest_build: build, - }); + updateWorkspaceBuild(build, queryClient); }, }; }; + +export const update = (workspace: Workspace, queryClient: QueryClient) => { + return { + mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { + return API.updateWorkspace(workspace, buildParameters); + }, + onSuccess: (build: WorkspaceBuild) => { + updateWorkspaceBuild(build, queryClient); + }, + }; +}; + +const updateWorkspaceBuild = ( + 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, + }); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index b2b06a65d40b1..2080c0e269486 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -37,6 +37,7 @@ import { changeVersion, decreaseDeadline, increaseDeadline, + update, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; @@ -76,13 +77,11 @@ export const WorkspaceReadyPage = ({ const queryClient = useQueryClient(); const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); - const { buildError, cancellationError, missedParameters } = - workspaceState.context; + const { buildError, cancellationError } = workspaceState.context; if (workspace === undefined) { throw Error("Workspace is undefined"); } - const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); - const canUpdateTemplate = Boolean(permissions?.updateTemplate); + const { data: deploymentValues } = useQuery({ ...deploymentConfig(), enabled: permissions?.viewDeploymentValues, @@ -90,7 +89,6 @@ export const WorkspaceReadyPage = ({ const canRetryDebugMode = Boolean( deploymentValues?.config.enable_terraform_debug_mode, ); - const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); // Build logs const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id); @@ -157,6 +155,7 @@ export const WorkspaceReadyPage = ({ }, []); // Change version + const canChangeVersions = Boolean(permissions?.updateTemplate); const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const changeVersionMutation = useMutation( changeVersion(workspace, queryClient), @@ -172,6 +171,11 @@ export const WorkspaceReadyPage = ({ enabled: workspace.outdated, }); + // Update workspace + const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); + const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); + const updateWorkspaceMutation = useMutation(update(workspace, queryClient)); + return ( <> @@ -198,7 +202,7 @@ export const WorkspaceReadyPage = ({ deadline, ), }} - isUpdating={workspaceState.matches("ready.build.requestingUpdate")} + isUpdating={updateWorkspaceMutation.isLoading} isRestarting={isRestarting} workspace={workspace} handleStart={(buildParameters) => @@ -227,7 +231,7 @@ export const WorkspaceReadyPage = ({ canUpdateWorkspace={canUpdateWorkspace} updateMessage={latestVersion?.message} canRetryDebugMode={canRetryDebugMode} - canChangeVersions={canUpdateTemplate} + canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} workspaceErrors={{ @@ -277,15 +281,19 @@ export const WorkspaceReadyPage = ({ }} /> { - workspaceSend({ type: "CANCEL" }); + updateWorkspaceMutation.reset(); }} onUpdate={(buildParameters) => { - workspaceSend({ type: "UPDATE", buildParameters }); + if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { + updateWorkspaceMutation.mutate(buildParameters); + } }} /> { - workspaceSend({ type: "UPDATE" }); + updateWorkspaceMutation.mutate(undefined); setIsConfirmingUpdate(false); }} onClose={() => setIsConfirmingUpdate(false)} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index d5013af4dfea8..1c5ea56f7643a 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -19,7 +19,6 @@ export interface WorkspaceContext { // Builds builds?: TypesGen.WorkspaceBuild[]; getBuildsError?: unknown; - missedParameters?: TypesGen.TemplateVersionParameter[]; // error creating a new WorkspaceBuild buildError?: unknown; cancellationMessage?: TypesGen.Response; @@ -37,7 +36,6 @@ export type WorkspaceEvent = | { type: "ASK_DELETE" } | { type: "DELETE" } | { type: "CANCEL_DELETE" } - | { type: "UPDATE"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "CANCEL" } | { type: "REFRESH_TIMELINE"; @@ -54,9 +52,6 @@ export const workspaceMachine = createMachine( context: {} as WorkspaceContext, events: {} as WorkspaceEvent, services: {} as { - updateWorkspace: { - data: TypesGen.WorkspaceBuild; - }; startWorkspace: { data: TypesGen.WorkspaceBuild; }; @@ -92,7 +87,6 @@ export const workspaceMachine = createMachine( START: "requestingStart", STOP: "requestingStop", ASK_DELETE: "askingDelete", - UPDATE: "requestingUpdate", CANCEL: "requestingCancel", RETRY_BUILD: [ { @@ -124,33 +118,6 @@ export const workspaceMachine = createMachine( }, }, }, - requestingUpdate: { - entry: ["clearBuildError"], - invoke: { - src: "updateWorkspace", - onDone: { - target: "idle", - actions: ["assignBuild"], - }, - onError: [ - { - target: "askingForMissedBuildParameters", - cond: "isMissingBuildParameterError", - actions: ["assignMissedParameters"], - }, - { - target: "idle", - actions: ["assignBuildError"], - }, - ], - }, - }, - askingForMissedBuildParameters: { - on: { - CANCEL: "idle", - UPDATE: [{ target: "requestingUpdate" }], - }, - }, requestingStart: { entry: ["clearBuildError"], invoke: { @@ -283,22 +250,12 @@ export const workspaceMachine = createMachine( 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 }), }, guards: { - isMissingBuildParameterError: (_, { data }) => { - return data instanceof API.MissingBuildParameters; - }, lastBuildWasStarting: ({ workspace }) => { return workspace?.latest_build.transition === "start"; }, @@ -310,16 +267,6 @@ export const workspaceMachine = createMachine( }, }, services: { - 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; - }, startWorkspace: (context, data) => async (send) => { if (context.workspace) { const startWorkspacePromise = await API.startWorkspace( From cb2c9662427b0574246f9763409295a3ca6fcff4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:40:46 +0000 Subject: [PATCH 14/30] Extract delete --- site/src/api/queries/workspaces.ts | 14 +++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 21 +++++-- .../xServices/workspace/workspaceXService.ts | 56 ------------------- 3 files changed, 31 insertions(+), 60 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 247336d4acfee..1eb3f0b5d4a08 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -155,6 +155,20 @@ export const update = (workspace: Workspace, queryClient: QueryClient) => { }; }; +export const deleteWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { + return { + mutationFn: () => { + return API.deleteWorkspace(workspace.id); + }, + onSuccess: (build: WorkspaceBuild) => { + updateWorkspaceBuild(build, queryClient); + }, + }; +}; + const updateWorkspaceBuild = ( build: WorkspaceBuild, queryClient: QueryClient, diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 2080c0e269486..3082b429b4b68 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -36,6 +36,7 @@ import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { changeVersion, decreaseDeadline, + deleteWorkspace, increaseDeadline, update, } from "api/queries/workspaces"; @@ -82,6 +83,7 @@ export const WorkspaceReadyPage = ({ throw Error("Workspace is undefined"); } + // Debug mode const { data: deploymentValues } = useQuery({ ...deploymentConfig(), enabled: permissions?.viewDeploymentValues, @@ -176,6 +178,12 @@ export const WorkspaceReadyPage = ({ const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); const updateWorkspaceMutation = useMutation(update(workspace, queryClient)); + // Delete workspace + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const deleteWorkspaceMutation = useMutation( + deleteWorkspace(workspace, queryClient), + ); + return ( <> @@ -209,7 +217,9 @@ export const WorkspaceReadyPage = ({ workspaceSend({ type: "START", buildParameters }) } handleStop={() => workspaceSend({ type: "STOP" })} - handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} + handleDelete={() => { + setIsConfirmingDelete(true); + }} handleRestart={(buildParameters) => { setConfirmingRestart({ open: true, buildParameters }); }} @@ -255,10 +265,13 @@ export const WorkspaceReadyPage = ({ info={`This workspace was created ${dayjs( workspace.created_at, ).fromNow()}.`} - isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })} - onCancel={() => workspaceSend({ type: "CANCEL_DELETE" })} + isOpen={isConfirmingDelete} + onCancel={() => { + setIsConfirmingDelete(false); + }} onConfirm={() => { - workspaceSend({ type: "DELETE" }); + deleteWorkspaceMutation.mutate(); + setIsConfirmingDelete(false); }} /> { return workspace?.latest_build.transition === "stop"; }, - lastBuildWasDeleting: ({ workspace }) => { - return workspace?.latest_build.transition === "delete"; - }, }, services: { startWorkspace: (context, data) => async (send) => { @@ -293,18 +249,6 @@ export const workspaceMachine = createMachine( throw Error("Cannot stop workspace without workspace id"); } }, - deleteWorkspace: (context) => async (send) => { - if (context.workspace) { - const deleteWorkspacePromise = await API.deleteWorkspace( - context.workspace.id, - context.createBuildLogLevel, - ); - 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( From 6c8ec9f6c3546ecbc3c5a2b5aa6ab992086adc94 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:49:42 +0000 Subject: [PATCH 15/30] Extract activate --- site/src/api/queries/workspaces.ts | 14 ++++++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 15 +++++++- .../xServices/workspace/workspaceXService.ts | 35 ++----------------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 1eb3f0b5d4a08..60f4bf4014bc4 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -169,6 +169,20 @@ export const deleteWorkspace = ( }; }; +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 = ( build: WorkspaceBuild, queryClient: QueryClient, diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 3082b429b4b68..ce8181b3c09c2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -34,6 +34,7 @@ import { Alert } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { + activate, changeVersion, decreaseDeadline, deleteWorkspace, @@ -184,6 +185,11 @@ export const WorkspaceReadyPage = ({ deleteWorkspace(workspace, queryClient), ); + // Activate workspace + const activateWorkspaceMutation = useMutation( + activate(workspace, queryClient), + ); + return ( <> @@ -232,7 +238,14 @@ export const WorkspaceReadyPage = ({ 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} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 227b95e639372..5da48c71f99a1 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,8 +1,7 @@ -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"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; export interface WorkspaceContext { // Initial data @@ -37,8 +36,7 @@ export type WorkspaceEvent = | { type: "REFRESH_TIMELINE"; } - | { type: "RETRY_BUILD" } - | { type: "ACTIVATE" }; + | { type: "RETRY_BUILD" }; export const workspaceMachine = createMachine( { @@ -93,7 +91,6 @@ export const workspaceMachine = createMachine( actions: ["enableDebugMode"], }, ], - ACTIVATE: "requestingActivate", }, }, requestingStart: { @@ -156,18 +153,6 @@ export const workspaceMachine = createMachine( ], }, }, - requestingActivate: { - entry: ["clearBuildError"], - invoke: { - src: "activateWorkspace", - id: "activateWorkspace", - onDone: "idle", - onError: { - target: "idle", - actions: ["displayActivateError"], - }, - }, - }, }, }, }, @@ -205,10 +190,6 @@ export const workspaceMachine = createMachine( clearCancellationError: assign({ cancellationError: (_) => undefined, }), - displayActivateError: (_, { data }) => { - const message = getErrorMessage(data, "Error activate workspace."); - displayError(message); - }, // Debug mode when build fails enableDebugMode: assign({ createBuildLogLevel: (_) => "debug" as const }), @@ -260,18 +241,6 @@ export const workspaceMachine = createMachine( 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"); - } - }, }, }, ); From 039f4bbc665ab1c83462cbe10856920d3a06b6d2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:53:20 +0000 Subject: [PATCH 16/30] Remove unecessary require permission --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 575e8b8963f55..d609c9f78775a 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -120,26 +120,20 @@ export const WorkspacePage: FC = () => { } return ( - - { - await buildsQuery.fetchNextPage(); - }} - hasMoreBuilds={Boolean(buildsQuery.hasNextPage)} - /> - + { + await buildsQuery.fetchNextPage(); + }} + hasMoreBuilds={Boolean(buildsQuery.hasNextPage)} + /> ); }; From 987f14ff141b7793b18e31992d5d390ebe1c83b8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 17:54:41 +0000 Subject: [PATCH 17/30] Remove unused RequirePermission usage --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index d609c9f78775a..769de9a123be5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -4,10 +4,8 @@ import { FC, useEffect, useRef } 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 { useInfiniteQuery, useQuery, useQueryClient } from "react-query"; import { infiniteWorkspaceBuilds } from "api/queries/workspaceBuilds"; From 13e954f098ef033e9d39386014983794e7ca199d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 18:00:48 +0000 Subject: [PATCH 18/30] Extract refresh timeline --- site/src/api/queries/workspaceBuilds.ts | 7 ++++++- site/src/api/queries/workspaces.ts | 18 ++++++++++------- .../src/pages/WorkspacePage/WorkspacePage.tsx | 5 ----- .../xServices/workspace/workspaceXService.ts | 20 ++++++------------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 9da020881b2b4..ecace99560ca2 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 60f4bf4014bc4..c2e85221e38de 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -11,6 +11,7 @@ import { type WorkspacesRequest, WorkspaceBuild, } from "api/typesGenerated"; +import { workspaceBuildsKey } from "./workspaceBuilds"; export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ "workspace", @@ -138,8 +139,8 @@ export const changeVersion = ( }) => { return API.changeWorkspaceVersion(workspace, versionId, buildParameters); }, - onSuccess: (build: WorkspaceBuild) => { - updateWorkspaceBuild(build, queryClient); + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); }, }; }; @@ -149,8 +150,8 @@ export const update = (workspace: Workspace, queryClient: QueryClient) => { mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { return API.updateWorkspace(workspace, buildParameters); }, - onSuccess: (build: WorkspaceBuild) => { - updateWorkspaceBuild(build, queryClient); + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); }, }; }; @@ -163,8 +164,8 @@ export const deleteWorkspace = ( mutationFn: () => { return API.deleteWorkspace(workspace.id); }, - onSuccess: (build: WorkspaceBuild) => { - updateWorkspaceBuild(build, queryClient); + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); }, }; }; @@ -183,7 +184,7 @@ export const activate = (workspace: Workspace, queryClient: QueryClient) => { }; }; -const updateWorkspaceBuild = ( +const updateWorkspaceBuild = async ( build: WorkspaceBuild, queryClient: QueryClient, ) => { @@ -196,4 +197,7 @@ const updateWorkspaceBuild = ( ...previousData, latest_build: build, }); + await queryClient.invalidateQueries({ + queryKey: workspaceBuildsKey(build.workspace_id), + }); }; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 769de9a123be5..74cb5dd2850a6 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -31,11 +31,6 @@ export const WorkspacePage: FC = () => { workspaceName, username, }, - actions: { - refreshBuilds: async () => { - await buildsQuery.refetch(); - }, - }, }); const workspaceQueryOptions = workspaceByOwnerAndName( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 5da48c71f99a1..a8930399e33d0 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -33,9 +33,6 @@ export type WorkspaceEvent = | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "STOP" } | { type: "CANCEL" } - | { - type: "REFRESH_TIMELINE"; - } | { type: "RETRY_BUILD" }; export const workspaceMachine = createMachine( @@ -65,11 +62,6 @@ export const workspaceMachine = createMachine( states: { ready: { type: "parallel", - on: { - REFRESH_TIMELINE: { - actions: ["refreshBuilds"], - }, - }, states: { build: { initial: "idle", @@ -204,7 +196,7 @@ export const workspaceMachine = createMachine( }, }, services: { - startWorkspace: (context, data) => async (send) => { + startWorkspace: (context, data) => async () => { if (context.workspace) { const startWorkspacePromise = await API.startWorkspace( context.workspace.id, @@ -212,30 +204,30 @@ export const workspaceMachine = createMachine( 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) => { + stopWorkspace: (context) => async () => { 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"); } }, - cancelWorkspace: (context) => async (send) => { + cancelWorkspace: (context) => async () => { 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"); From 764f047af91891e2cb25bcf28b985081e8b88053 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 18:01:16 +0000 Subject: [PATCH 19/30] Remove REFRESH_WORKSPACE --- site/src/xServices/workspace/workspaceXService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index a8930399e33d0..092a7d2b0953e 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -29,7 +29,6 @@ export interface WorkspaceContext { } export type WorkspaceEvent = - | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { type: "STOP" } | { type: "CANCEL" } From 633c431fc1f7abc8a6b6c880b328528d15d9be5c Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:08:02 +0000 Subject: [PATCH 20/30] Extract stop --- site/src/api/queries/workspaces.ts | 11 +++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 8 +++- .../xServices/workspace/workspaceXService.ts | 41 ------------------- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index c2e85221e38de..1503d950610ae 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -170,6 +170,17 @@ export const deleteWorkspace = ( }; }; +export const stop = (workspace: Workspace, queryClient: QueryClient) => { + return { + mutationFn: () => { + return API.stopWorkspace(workspace.id); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + export const activate = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ce8181b3c09c2..7ccb90097f17d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -40,6 +40,7 @@ import { deleteWorkspace, increaseDeadline, update, + stop, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; @@ -190,6 +191,9 @@ export const WorkspaceReadyPage = ({ activate(workspace, queryClient), ); + // Stop workspace + const stopWorkspaceMutation = useMutation(stop(workspace, queryClient)); + return ( <> @@ -222,7 +226,9 @@ export const WorkspaceReadyPage = ({ handleStart={(buildParameters) => workspaceSend({ type: "START", buildParameters }) } - handleStop={() => workspaceSend({ type: "STOP" })} + handleStop={() => { + stopWorkspaceMutation.mutate(); + }} handleDelete={() => { setIsConfirmingDelete(true); }} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 092a7d2b0953e..fab4ccc0e6162 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -30,7 +30,6 @@ export interface WorkspaceContext { export type WorkspaceEvent = | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } - | { type: "STOP" } | { type: "CANCEL" } | { type: "RETRY_BUILD" }; @@ -68,7 +67,6 @@ export const workspaceMachine = createMachine( idle: { on: { START: "requestingStart", - STOP: "requestingStop", CANCEL: "requestingCancel", RETRY_BUILD: [ { @@ -76,11 +74,6 @@ export const workspaceMachine = createMachine( cond: "lastBuildWasStarting", actions: ["enableDebugMode"], }, - { - target: "requestingStop", - cond: "lastBuildWasStopping", - actions: ["enableDebugMode"], - }, ], }, }, @@ -103,25 +96,6 @@ export const workspaceMachine = createMachine( ], }, }, - requestingStop: { - entry: ["clearBuildError"], - invoke: { - src: "stopWorkspace", - id: "stopWorkspace", - onDone: [ - { - actions: ["assignBuild", "disableDebugMode"], - target: "idle", - }, - ], - onError: [ - { - actions: "assignBuildError", - target: "idle", - }, - ], - }, - }, requestingCancel: { entry: ["clearCancellationMessage", "clearCancellationError"], invoke: { @@ -190,9 +164,6 @@ export const workspaceMachine = createMachine( lastBuildWasStarting: ({ workspace }) => { return workspace?.latest_build.transition === "start"; }, - lastBuildWasStopping: ({ workspace }) => { - return workspace?.latest_build.transition === "stop"; - }, }, services: { startWorkspace: (context, data) => async () => { @@ -209,18 +180,6 @@ export const workspaceMachine = createMachine( throw Error("Cannot start workspace without workspace id"); } }, - stopWorkspace: (context) => async () => { - if (context.workspace) { - const stopWorkspacePromise = await API.stopWorkspace( - context.workspace.id, - context.createBuildLogLevel, - ); - - return stopWorkspacePromise; - } else { - throw Error("Cannot stop workspace without workspace id"); - } - }, cancelWorkspace: (context) => async () => { if (context.workspace) { const cancelWorkspacePromise = await API.cancelWorkspaceBuild( From 20e073f728a35b95937405c65ebde213951efc76 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:13:40 +0000 Subject: [PATCH 21/30] Extract start --- site/src/api/queries/workspaces.ts | 16 ++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 10 ++- .../xServices/workspace/workspaceXService.ts | 75 +------------------ 3 files changed, 26 insertions(+), 75 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 1503d950610ae..e10c2bf72b06b 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -181,6 +181,22 @@ export const stop = (workspace: Workspace, queryClient: QueryClient) => { }; }; +export const start = (workspace: Workspace, queryClient: QueryClient) => { + return { + mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { + return API.startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + undefined, + buildParameters, + ); + }, + onSuccess: async (build: WorkspaceBuild) => { + await updateWorkspaceBuild(build, queryClient); + }, + }; +}; + export const activate = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 7ccb90097f17d..feffb836bfd1c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -41,6 +41,7 @@ import { increaseDeadline, update, stop, + start, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; @@ -194,6 +195,9 @@ export const WorkspaceReadyPage = ({ // Stop workspace const stopWorkspaceMutation = useMutation(stop(workspace, queryClient)); + // Start workspace + const startWorkspaceMutation = useMutation(start(workspace, queryClient)); + return ( <> @@ -223,9 +227,9 @@ export const WorkspaceReadyPage = ({ isUpdating={updateWorkspaceMutation.isLoading} isRestarting={isRestarting} workspace={workspace} - handleStart={(buildParameters) => - workspaceSend({ type: "START", buildParameters }) - } + handleStart={(buildParameters) => { + startWorkspaceMutation.mutate(buildParameters); + }} handleStop={() => { stopWorkspaceMutation.mutate(); }} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index fab4ccc0e6162..0898c96d3ec8c 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -28,10 +28,7 @@ export interface WorkspaceContext { templateVersionIdToChange?: TypesGen.TemplateVersion["id"]; } -export type WorkspaceEvent = - | { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } - | { type: "CANCEL" } - | { type: "RETRY_BUILD" }; +export type WorkspaceEvent = { type: "CANCEL" } | { type: "RETRY_BUILD" }; export const workspaceMachine = createMachine( { @@ -42,18 +39,9 @@ export const workspaceMachine = createMachine( context: {} as WorkspaceContext, events: {} as WorkspaceEvent, services: {} as { - startWorkspace: { - data: TypesGen.WorkspaceBuild; - }; - stopWorkspace: { - data: TypesGen.WorkspaceBuild; - }; cancelWorkspace: { data: TypesGen.Response; }; - activateWorkspace: { - data: TypesGen.Response; - }; }, }, initial: "ready", @@ -66,36 +54,11 @@ export const workspaceMachine = createMachine( states: { idle: { on: { - START: "requestingStart", CANCEL: "requestingCancel", - RETRY_BUILD: [ - { - target: "requestingStart", - cond: "lastBuildWasStarting", - actions: ["enableDebugMode"], - }, - ], - }, - }, - requestingStart: { - entry: ["clearBuildError"], - invoke: { - src: "startWorkspace", - id: "startWorkspace", - onDone: [ - { - actions: ["assignBuild", "disableDebugMode"], - target: "idle", - }, - ], - onError: [ - { - actions: "assignBuildError", - target: "idle", - }, - ], + RETRY_BUILD: [], }, }, + requestingCancel: { entry: ["clearCancellationMessage", "clearCancellationError"], invoke: { @@ -129,15 +92,6 @@ export const workspaceMachine = createMachine( }, { actions: { - assignBuild: assign({ - build: (_, event) => event.data, - }), - assignBuildError: assign({ - buildError: (_, event) => event.data, - }), - clearBuildError: assign({ - buildError: (_) => undefined, - }), assignCancellationMessage: assign({ cancellationMessage: (_, event) => event.data, }), @@ -155,31 +109,8 @@ export const workspaceMachine = createMachine( clearCancellationError: assign({ cancellationError: (_) => undefined, }), - - // Debug mode when build fails - enableDebugMode: assign({ createBuildLogLevel: (_) => "debug" as const }), - disableDebugMode: assign({ createBuildLogLevel: (_) => undefined }), - }, - guards: { - lastBuildWasStarting: ({ workspace }) => { - return workspace?.latest_build.transition === "start"; - }, }, services: { - startWorkspace: (context, data) => async () => { - 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, - ); - - return startWorkspacePromise; - } else { - throw Error("Cannot start workspace without workspace id"); - } - }, cancelWorkspace: (context) => async () => { if (context.workspace) { const cancelWorkspacePromise = await API.cancelWorkspaceBuild( From bfcc99d9989b6c8d6b1dbdbdb44679a94aebbc1a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:21:17 +0000 Subject: [PATCH 22/30] Extract cancellation and remove xservices --- site/src/api/queries/workspaces.ts | 8 ++ .../src/pages/WorkspacePage/WorkspacePage.tsx | 3 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 31 ++--- .../xServices/workspace/workspaceXService.ts | 127 ------------------ 4 files changed, 23 insertions(+), 146 deletions(-) delete mode 100644 site/src/xServices/workspace/workspaceXService.ts diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index e10c2bf72b06b..1c3f5fbe838ef 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -197,6 +197,14 @@ export const start = (workspace: Workspace, queryClient: QueryClient) => { }; }; +export const cancelBuild = (workspace: Workspace) => { + return { + mutationFn: () => { + return API.cancelWorkspaceBuild(workspace.id); + }, + }; +}; + export const activate = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 74cb5dd2850a6..cb8c20480f811 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -25,7 +25,7 @@ export const WorkspacePage: FC = () => { const workspaceName = params.workspace; const username = params.username.replace("@", ""); const orgId = useOrganizationId(); - const [workspaceState, workspaceSend] = useMachine(workspaceMachine, { + const [_, workspaceSend] = useMachine(workspaceMachine, { context: { orgId, workspaceName, @@ -117,7 +117,6 @@ export const WorkspacePage: FC = () => { workspace={workspace} template={template} permissions={permissions} - workspaceState={workspaceState} workspaceSend={workspaceSend} builds={buildsQuery.data?.pages.flat()} buildsError={buildsQuery.error} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index feffb836bfd1c..27924c9a311ea 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -10,15 +10,11 @@ import { getMaxDeadlineChange, getMinDeadline, } from "utils/schedule"; -import { StateFrom } from "xstate"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { Workspace, WorkspaceErrors } from "./Workspace"; import { pageTitle } from "utils/page"; import { hasJobError } from "utils/workspace"; -import { - WorkspaceEvent, - workspaceMachine, -} from "xServices/workspace/workspaceXService"; +import { WorkspaceEvent } from "xServices/workspace/workspaceXService"; import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { ChangeVersionDialog } from "./ChangeVersionDialog"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -42,6 +38,7 @@ import { update, stop, start, + cancelBuild, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; @@ -53,10 +50,6 @@ interface WorkspaceReadyPageProps { template: TypesGen.Template; workspace: TypesGen.Workspace; permissions: WorkspacePermissions; - workspaceState: Omit< - StateFrom, - "template" | "workspace" | "permissions" - >; workspaceSend: (event: WorkspaceEvent) => void; builds: TypesGen.WorkspaceBuild[] | undefined; buildsError: unknown; @@ -69,7 +62,6 @@ export const WorkspaceReadyPage = ({ workspace, template, permissions, - workspaceState, workspaceSend, builds, buildsError, @@ -81,7 +73,6 @@ export const WorkspaceReadyPage = ({ const queryClient = useQueryClient(); const { buildInfo } = useDashboard(); const featureVisibility = useFeatureVisibility(); - const { buildError, cancellationError } = workspaceState.context; if (workspace === undefined) { throw Error("Workspace is undefined"); } @@ -198,6 +189,9 @@ export const WorkspaceReadyPage = ({ // Start workspace const startWorkspaceMutation = useMutation(start(workspace, queryClient)); + // Cancel build + const cancelBuildMutation = useMutation(cancelBuild(workspace)); + return ( <> @@ -230,9 +224,7 @@ export const WorkspaceReadyPage = ({ handleStart={(buildParameters) => { startWorkspaceMutation.mutate(buildParameters); }} - handleStop={() => { - stopWorkspaceMutation.mutate(); - }} + handleStop={stopWorkspaceMutation.mutate} handleDelete={() => { setIsConfirmingDelete(true); }} @@ -242,7 +234,7 @@ export const WorkspaceReadyPage = ({ handleUpdate={() => { setIsConfirmingUpdate(true); }} - handleCancel={() => workspaceSend({ type: "CANCEL" })} + handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })} handleChangeVersion={() => { @@ -269,8 +261,13 @@ export const WorkspaceReadyPage = ({ 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={sshPrefixQuery.data?.hostname_prefix} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts deleted file mode 100644 index 0898c96d3ec8c..0000000000000 --- a/site/src/xServices/workspace/workspaceXService.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { assign, createMachine } from "xstate"; -import * as API from "api/api"; -import * as TypesGen from "api/typesGenerated"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; - -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; - build?: TypesGen.WorkspaceBuild; - // Builds - builds?: TypesGen.WorkspaceBuild[]; - getBuildsError?: unknown; - // error creating a new WorkspaceBuild - buildError?: unknown; - cancellationMessage?: TypesGen.Response; - cancellationError?: unknown; - // debug - createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"]; - // Change version - templateVersionIdToChange?: TypesGen.TemplateVersion["id"]; -} - -export type WorkspaceEvent = { type: "CANCEL" } | { type: "RETRY_BUILD" }; - -export const workspaceMachine = createMachine( - { - id: "workspaceState", - predictableActionArguments: true, - tsTypes: {} as import("./workspaceXService.typegen").Typegen0, - schema: { - context: {} as WorkspaceContext, - events: {} as WorkspaceEvent, - services: {} as { - cancelWorkspace: { - data: TypesGen.Response; - }; - }, - }, - initial: "ready", - states: { - ready: { - type: "parallel", - states: { - build: { - initial: "idle", - states: { - idle: { - on: { - CANCEL: "requestingCancel", - RETRY_BUILD: [], - }, - }, - - requestingCancel: { - entry: ["clearCancellationMessage", "clearCancellationError"], - invoke: { - src: "cancelWorkspace", - id: "cancelWorkspace", - onDone: [ - { - actions: [ - "assignCancellationMessage", - "displayCancellationMessage", - ], - target: "idle", - }, - ], - onError: [ - { - actions: "assignCancellationError", - target: "idle", - }, - ], - }, - }, - }, - }, - }, - }, - error: { - type: "final", - }, - }, - }, - { - actions: { - 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, - }), - }, - services: { - cancelWorkspace: (context) => async () => { - if (context.workspace) { - const cancelWorkspacePromise = await API.cancelWorkspaceBuild( - context.workspace.latest_build.id, - ); - - return cancelWorkspacePromise; - } else { - throw Error("Cannot cancel workspace without build id"); - } - }, - }, - }, -); From cf6896a2121b266bd30f10df2a4c3db825d3ed7a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:23:26 +0000 Subject: [PATCH 23/30] Clean up --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 23 ++++++++----------- .../WorkspacePage/WorkspaceReadyPage.tsx | 5 +--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index cb8c20480f811..54a80afb86996 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,8 +1,6 @@ -import { useMachine } from "@xstate/react"; import { Loader } from "components/Loader/Loader"; import { FC, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; -import { workspaceMachine } from "xServices/workspace/workspaceXService"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { useOrganizationId } from "hooks"; @@ -25,14 +23,8 @@ export const WorkspacePage: FC = () => { const workspaceName = params.workspace; const username = params.username.replace("@", ""); const orgId = useOrganizationId(); - const [_, workspaceSend] = useMachine(workspaceMachine, { - context: { - orgId, - workspaceName, - username, - }, - }); + // Workspace const workspaceQueryOptions = workspaceByOwnerAndName( username, workspaceName, @@ -40,12 +32,14 @@ export const WorkspacePage: FC = () => { const workspaceQuery = useQuery(workspaceQueryOptions); const workspace = workspaceQuery.data; + // Template const templateQuery = useQuery({ ...templateByName(orgId, workspace?.template_name ?? ""), enabled: workspace !== undefined, }); const template = templateQuery.data; + // Permissions const checks = workspace && template ? workspaceChecks(workspace, template) : {}; const permissionsQuery = useQuery({ @@ -54,15 +48,12 @@ export const WorkspacePage: FC = () => { }); const permissions = permissionsQuery.data as WorkspacePermissions | undefined; + // Builds const buildsQuery = useInfiniteQuery({ ...infiniteWorkspaceBuilds(workspace?.id ?? ""), enabled: workspace !== undefined, }); - const pageError = - workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; - const isLoading = !workspace || !template || !permissions; - // Watch workspace changes const workspaceEventSource = useRef(null); useEffect(() => { @@ -100,6 +91,11 @@ export const WorkspacePage: FC = () => { }; }, [buildsQuery, queryClient, workspace, workspaceQueryOptions.queryKey]); + // Page statuses + const pageError = + workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; + const isLoading = !workspace || !template || !permissions; + if (pageError) { return ( @@ -117,7 +113,6 @@ export const WorkspacePage: FC = () => { workspace={workspace} template={template} permissions={permissions} - workspaceSend={workspaceSend} builds={buildsQuery.data?.pages.flat()} buildsError={buildsQuery.error} isLoadingMoreBuilds={buildsQuery.isFetchingNextPage} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 27924c9a311ea..e6c7ec11cb05c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -14,7 +14,6 @@ import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { Workspace, WorkspaceErrors } from "./Workspace"; import { pageTitle } from "utils/page"; import { hasJobError } from "utils/workspace"; -import { WorkspaceEvent } from "xServices/workspace/workspaceXService"; import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { ChangeVersionDialog } from "./ChangeVersionDialog"; import { useMutation, useQuery, useQueryClient } from "react-query"; @@ -50,7 +49,6 @@ interface WorkspaceReadyPageProps { template: TypesGen.Template; workspace: TypesGen.Workspace; permissions: WorkspacePermissions; - workspaceSend: (event: WorkspaceEvent) => void; builds: TypesGen.WorkspaceBuild[] | undefined; buildsError: unknown; onLoadMoreBuilds: () => void; @@ -62,7 +60,6 @@ export const WorkspaceReadyPage = ({ workspace, template, permissions, - workspaceSend, builds, buildsError, onLoadMoreBuilds, @@ -236,7 +233,7 @@ export const WorkspaceReadyPage = ({ }} handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} - handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })} + handleBuildRetry={() => {}} handleChangeVersion={() => { setChangeVersionDialogOpen(true); }} From b6c9469d020c50679435daf289f01ac58d852674 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:36:01 +0000 Subject: [PATCH 24/30] Add retry with debug --- site/src/api/api.ts | 6 ++--- site/src/api/queries/workspaces.ts | 19 +++++++++++----- .../WorkspacePage/WorkspaceReadyPage.tsx | 22 +++++++++++++++---- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ec1abec0612f2..94199b0dbcae5 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,7 +536,7 @@ export const startWorkspace = ( }); export const stopWorkspace = ( workspaceId: string, - logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], + logLevel?: TypesGen.ProvisionerLogLevel, ) => postWorkspaceBuild(workspaceId, { transition: "stop", @@ -545,7 +545,7 @@ export const stopWorkspace = ( export const deleteWorkspace = ( workspaceId: string, - logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], + logLevel?: TypesGen.ProvisionerLogLevel, ) => postWorkspaceBuild(workspaceId, { transition: "delete", diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 1c3f5fbe838ef..7ea0b1290c2ad 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -10,6 +10,7 @@ import { type WorkspacesResponse, type WorkspacesRequest, WorkspaceBuild, + ProvisionerLogLevel, } from "api/typesGenerated"; import { workspaceBuildsKey } from "./workspaceBuilds"; @@ -161,8 +162,8 @@ export const deleteWorkspace = ( queryClient: QueryClient, ) => { return { - mutationFn: () => { - return API.deleteWorkspace(workspace.id); + mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { + return API.deleteWorkspace(workspace.id, logLevel); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -172,8 +173,8 @@ export const deleteWorkspace = ( export const stop = (workspace: Workspace, queryClient: QueryClient) => { return { - mutationFn: () => { - return API.stopWorkspace(workspace.id); + mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { + return API.stopWorkspace(workspace.id, logLevel); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -183,11 +184,17 @@ export const stop = (workspace: Workspace, queryClient: QueryClient) => { export const start = (workspace: Workspace, queryClient: QueryClient) => { return { - mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { + mutationFn: ({ + buildParameters, + logLevel, + }: { + buildParameters?: WorkspaceBuildParameter[]; + logLevel?: ProvisionerLogLevel; + }) => { return API.startWorkspace( workspace.id, workspace.latest_build.template_version_id, - undefined, + logLevel, buildParameters, ); }, diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e6c7ec11cb05c..f66aed1677cb2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -219,9 +219,11 @@ export const WorkspaceReadyPage = ({ isRestarting={isRestarting} workspace={workspace} handleStart={(buildParameters) => { - startWorkspaceMutation.mutate(buildParameters); + startWorkspaceMutation.mutate({ buildParameters }); + }} + handleStop={() => { + stopWorkspaceMutation.mutate({}); }} - handleStop={stopWorkspaceMutation.mutate} handleDelete={() => { setIsConfirmingDelete(true); }} @@ -233,7 +235,19 @@ export const WorkspaceReadyPage = ({ }} handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} - handleBuildRetry={() => {}} + 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); }} @@ -287,7 +301,7 @@ export const WorkspaceReadyPage = ({ setIsConfirmingDelete(false); }} onConfirm={() => { - deleteWorkspaceMutation.mutate(); + deleteWorkspaceMutation.mutate({}); setIsConfirmingDelete(false); }} /> From a4a33f9400060678d7535576a58d1c9e786c5356 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 13 Nov 2023 19:41:51 +0000 Subject: [PATCH 25/30] Remove XState --- .github/dependabot.yaml | 4 - .gitignore | 1 - .prettierignore | 1 - .vscode/settings.json | 1 - docs/contributing/frontend.md | 9 +- site/.eslintignore | 1 - site/.prettierignore | 1 - site/package.json | 10 +-- site/pnpm-lock.yaml | 149 ---------------------------------- site/src/index.tsx | 17 ---- 10 files changed, 3 insertions(+), 191 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index d0d75f78a7b99..8711e64a822a8 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 2ccd459b811b9..5e5631409ce86 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 c7882767e85af..011d66b70991e 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 6f726162d260a..cbaf02266c035 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 3a207dd5cd44d..17432a4243537 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 4909d9bf919d2..20570ccb94cfd 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 4909d9bf919d2..20570ccb94cfd 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 d9bab274dfcd6..019dbf6a40605 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 85cd8bd5957da..f0a9685c8efb3 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/index.tsx b/site/src/index.tsx index 48130401be9b7..a6d366d5d59c5 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 From 75d39a2d5a2de7044b487d394cb185690fce11cd Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 14 Nov 2023 15:52:00 +0000 Subject: [PATCH 26/30] Clean up event source --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 54a80afb86996..2ff7206928911 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -88,6 +88,7 @@ export const WorkspacePage: FC = () => { return () => { eventSource.close(); + workspaceEventSource.current = null; }; }, [buildsQuery, queryClient, workspace, workspaceQueryOptions.queryKey]); From c3de809584c434abaa885df7f83040b3b586676f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 14 Nov 2023 16:00:36 +0000 Subject: [PATCH 27/30] Rename workspace mutations --- site/src/api/queries/workspaces.ts | 15 ++++++++++++--- .../pages/WorkspacePage/WorkspaceReadyPage.tsx | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 7ea0b1290c2ad..aa8068eef1240 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -146,7 +146,10 @@ export const changeVersion = ( }; }; -export const update = (workspace: Workspace, queryClient: QueryClient) => { +export const updateWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { return { mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { return API.updateWorkspace(workspace, buildParameters); @@ -171,7 +174,10 @@ export const deleteWorkspace = ( }; }; -export const stop = (workspace: Workspace, queryClient: QueryClient) => { +export const stopWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { return { mutationFn: ({ logLevel }: { logLevel?: ProvisionerLogLevel }) => { return API.stopWorkspace(workspace.id, logLevel); @@ -182,7 +188,10 @@ export const stop = (workspace: Workspace, queryClient: QueryClient) => { }; }; -export const start = (workspace: Workspace, queryClient: QueryClient) => { +export const startWorkspace = ( + workspace: Workspace, + queryClient: QueryClient, +) => { return { mutationFn: ({ buildParameters, diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index f66aed1677cb2..d477ffeac1c1e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -34,9 +34,9 @@ import { decreaseDeadline, deleteWorkspace, increaseDeadline, - update, - stop, - start, + updateWorkspace, + stopWorkspace, + startWorkspace, cancelBuild, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; @@ -167,7 +167,9 @@ export const WorkspaceReadyPage = ({ // Update workspace const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); - const updateWorkspaceMutation = useMutation(update(workspace, queryClient)); + const updateWorkspaceMutation = useMutation( + updateWorkspace(workspace, queryClient), + ); // Delete workspace const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); @@ -181,10 +183,14 @@ export const WorkspaceReadyPage = ({ ); // Stop workspace - const stopWorkspaceMutation = useMutation(stop(workspace, queryClient)); + const stopWorkspaceMutation = useMutation( + stopWorkspace(workspace, queryClient), + ); // Start workspace - const startWorkspaceMutation = useMutation(start(workspace, queryClient)); + const startWorkspaceMutation = useMutation( + startWorkspace(workspace, queryClient), + ); // Cancel build const cancelBuildMutation = useMutation(cancelBuild(workspace)); From 477b928f878d212400ac0936b3334204d5ca1bf2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 14 Nov 2023 16:07:55 +0000 Subject: [PATCH 28/30] Invalidate workspace builds data on cancel --- site/src/api/queries/workspaces.ts | 7 ++++++- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index aa8068eef1240..ca8abb2d9c1ff 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -213,11 +213,16 @@ export const startWorkspace = ( }; }; -export const cancelBuild = (workspace: Workspace) => { +export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { return API.cancelWorkspaceBuild(workspace.id); }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: workspaceBuildsKey(workspace.id), + }); + }, }; }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index d477ffeac1c1e..fe45e679157a0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -193,7 +193,7 @@ export const WorkspaceReadyPage = ({ ); // Cancel build - const cancelBuildMutation = useMutation(cancelBuild(workspace)); + const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); return ( <> From d189c20a766368bdf94084d961456d43dcbfdde0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 14 Nov 2023 16:42:13 +0000 Subject: [PATCH 29/30] use useEffectEvent to avoid unecessary updates --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 2ff7206928911..b11cfb1a1a59c 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,5 +1,5 @@ import { Loader } from "components/Loader/Loader"; -import { FC, useEffect, useRef } from "react"; +import { FC, useEffect } from "react"; import { useParams } from "react-router-dom"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; import { ErrorAlert } from "components/Alert/ErrorAlert"; @@ -13,6 +13,7 @@ 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(); @@ -55,31 +56,34 @@ export const WorkspacePage: FC = () => { }); // Watch workspace changes - const workspaceEventSource = useRef(null); - useEffect(() => { - // If there is an event source, we are already watching the workspace - if (!workspace || workspaceEventSource.current) { - return; - } - - const eventSource = watchWorkspace(workspace.id); - workspaceEventSource.current = eventSource; - - eventSource.addEventListener("data", async (event) => { - const newWorkspaceData = JSON.parse(event.data) as Workspace; + const updateWorkspaceData = useEffectEvent( + async (newWorkspaceData: Workspace) => { queryClient.setQueryData( workspaceQueryOptions.queryKey, newWorkspaceData, ); const hasNewBuild = - newWorkspaceData.latest_build.id !== workspace.latest_build.id; + newWorkspaceData.latest_build.id !== workspace!.latest_build.id; const lastBuildHasChanged = - newWorkspaceData.latest_build.status !== workspace.latest_build.status; + 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) => { @@ -88,9 +92,8 @@ export const WorkspacePage: FC = () => { return () => { eventSource.close(); - workspaceEventSource.current = null; }; - }, [buildsQuery, queryClient, workspace, workspaceQueryOptions.queryKey]); + }, [updateWorkspaceData, workspaceId]); // Page statuses const pageError = From 6cc257a70fcdd459316976a78dcb53577418ff33 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 14 Nov 2023 17:08:34 +0000 Subject: [PATCH 30/30] Fix cancel action --- site/src/api/queries/workspaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index a45c87aebc313..1dd3c0be1a592 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -216,7 +216,7 @@ export const startWorkspace = ( export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { return { mutationFn: () => { - return API.cancelWorkspaceBuild(workspace.id); + return API.cancelWorkspaceBuild(workspace.latest_build.id); }, onSuccess: async () => { await queryClient.invalidateQueries({