diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 6b243aaa500a2..3f37593a8b707 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -43,7 +43,7 @@ export const WorkspacePage: FC = () => { workspace, getWorkspaceError, template, - refreshTemplateWarning, + getTemplateWarning, refreshWorkspaceWarning, builds, getBuildsError, @@ -72,7 +72,7 @@ export const WorkspacePage: FC = () => { return (
{Boolean(getWorkspaceError) && } - {Boolean(refreshTemplateWarning) && } + {Boolean(getTemplateWarning) && } {Boolean(checkPermissionsError) && }
) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 0db51aa54fcb5..5a1318e3a5abf 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,5 +1,4 @@ import { assign, createMachine, send } from "xstate" -import { pure } from "xstate/lib/actions" import * as API from "../../api/api" import * as Types from "../../api/types" import * as TypesGen from "../../api/typesGenerated" @@ -12,8 +11,31 @@ const latestBuild = (builds: TypesGen.WorkspaceBuild[]) => { })[0] } +const moreBuildsAvailable = ( + context: WorkspaceContext, + event: { + type: "REFRESH_TIMELINE" + checkRefresh?: boolean + data?: TypesGen.ServerSentEvent["data"] + }, +) => { + // No need to refresh the timeline if it is not loaded + if (!context.builds) { + return false + } + + if (!event.checkRefresh) { + return true + } + + // After we refresh a workspace, we want to check if the latest + // build was updated before refreshing the timeline so as to not over fetch the builds + const latestBuildInTimeline = latestBuild(context.builds) + return event.data.latest_build.updated_at !== latestBuildInTimeline.updated_at +} + const Language = { - refreshTemplateWarning: "Error updating workspace: latest template could not be fetched.", + getTemplateWarning: "Error updating workspace: latest template could not be fetched.", buildError: "Workspace action failed.", } @@ -28,7 +50,7 @@ export interface WorkspaceContext { getWorkspaceError?: Error | unknown // these are labeled as warnings because they don't make the page unusable refreshWorkspaceWarning?: Error | unknown - refreshTemplateWarning: Error | unknown + getTemplateWarning: Error | unknown // Builds builds?: TypesGen.WorkspaceBuild[] getBuildsError?: Error | unknown @@ -52,8 +74,7 @@ export type WorkspaceEvent = | { type: "CANCEL_DELETE" } | { type: "UPDATE" } | { type: "CANCEL" } - | { type: "CHECK_REFRESH_TIMELINE"; data: TypesGen.ServerSentEvent["data"] } - | { type: "REFRESH_TIMELINE" } + | { type: "REFRESH_TIMELINE"; checkRefresh?: boolean; data?: TypesGen.ServerSentEvent["data"] } | { type: "EVENT_SOURCE_ERROR"; error: Error | unknown } export const checks = { @@ -140,7 +161,7 @@ export const workspaceMachine = createMachine( onDone: [ { actions: "assignWorkspace", - target: "refreshingTemplate", + target: "gettingTemplate", }, ], onError: [ @@ -152,11 +173,11 @@ export const workspaceMachine = createMachine( }, tags: "loading", }, - refreshingTemplate: { - entry: "clearRefreshTemplateWarning", + gettingTemplate: { + entry: "clearGettingTemplateWarning", invoke: { src: "getTemplate", - id: "refreshTemplate", + id: "getTemplate", onDone: [ { actions: "assignTemplate", @@ -165,7 +186,7 @@ export const workspaceMachine = createMachine( ], onError: [ { - actions: ["assignRefreshTemplateWarning", "displayRefreshTemplateWarning"], + actions: ["assignGetTemplateWarning", "displayGetTemplateWarning"], target: "error", }, ], @@ -211,9 +232,6 @@ export const workspaceMachine = createMachine( EVENT_SOURCE_ERROR: { target: "error", }, - CHECK_REFRESH_TIMELINE: { - actions: ["refreshTimeline"], - }, }, }, error: { @@ -255,7 +273,7 @@ export const workspaceMachine = createMachine( src: "startWorkspaceWithLatestTemplate", onDone: { target: "idle", - actions: ["assignBuild", "refreshTimeline"], + actions: ["assignBuild"], }, onError: { target: "idle", @@ -270,7 +288,7 @@ export const workspaceMachine = createMachine( id: "startWorkspace", onDone: [ { - actions: ["assignBuild", "refreshTimeline"], + actions: ["assignBuild"], target: "idle", }, ], @@ -289,7 +307,7 @@ export const workspaceMachine = createMachine( id: "stopWorkspace", onDone: [ { - actions: ["assignBuild", "refreshTimeline"], + actions: ["assignBuild"], target: "idle", }, ], @@ -308,7 +326,7 @@ export const workspaceMachine = createMachine( id: "deleteWorkspace", onDone: [ { - actions: ["assignBuild", "refreshTimeline"], + actions: ["assignBuild"], target: "idle", }, ], @@ -327,11 +345,7 @@ export const workspaceMachine = createMachine( id: "cancelWorkspace", onDone: [ { - actions: [ - "assignCancellationMessage", - "displayCancellationMessage", - "refreshTimeline", - ], + actions: ["assignCancellationMessage", "displayCancellationMessage"], target: "idle", }, ], @@ -343,31 +357,11 @@ export const workspaceMachine = createMachine( ], }, }, - refreshingTemplate: { - entry: "clearRefreshTemplateWarning", - invoke: { - src: "getTemplate", - id: "refreshTemplate", - onDone: [ - { - actions: "assignTemplate", - target: "requestingStart", - }, - ], - onError: [ - { - actions: ["assignRefreshTemplateWarning", "displayRefreshTemplateWarning"], - target: "idle", - }, - ], - }, - }, }, }, timeline: { initial: "gettingBuilds", states: { - idle: {}, gettingBuilds: { entry: "clearGetBuildsError", invoke: { @@ -381,19 +375,17 @@ export const workspaceMachine = createMachine( onError: [ { actions: "assignGetBuildsError", - target: "idle", + target: "loadedBuilds", }, ], }, }, loadedBuilds: { - initial: "idle", - states: { - idle: { - on: { - REFRESH_TIMELINE: { - target: "#workspaceState.ready.timeline.gettingBuilds", - }, + on: { + REFRESH_TIMELINE: { + target: "#workspaceState.ready.timeline.gettingBuilds", + cond: { + type: "moreBuildsAvailable", }, }, }, @@ -484,14 +476,14 @@ export const workspaceMachine = createMachine( clearRefreshWorkspaceWarning: assign({ refreshWorkspaceWarning: (_) => undefined, }), - assignRefreshTemplateWarning: assign({ - refreshTemplateWarning: (_, event) => event.data, + assignGetTemplateWarning: assign({ + getTemplateWarning: (_, event) => event.data, }), - displayRefreshTemplateWarning: () => { - displayError(Language.refreshTemplateWarning) + displayGetTemplateWarning: () => { + displayError(Language.getTemplateWarning) }, - clearRefreshTemplateWarning: assign({ - refreshTemplateWarning: (_) => undefined, + clearGettingTemplateWarning: assign({ + getTemplateWarning: (_) => undefined, }), // Timeline assignBuilds: assign({ @@ -503,24 +495,9 @@ export const workspaceMachine = createMachine( clearGetBuildsError: assign({ getBuildsError: (_) => undefined, }), - refreshTimeline: pure((context, event) => { - // No need to refresh the timeline if it is not loaded - if (!context.builds) { - return - } - - // When it is a CHECK_REFRESH_TIMELINE workspace event, we want to check if the latest - // build was updated to not over fetch the builds - if (event.type === "CHECK_REFRESH_TIMELINE") { - const latestBuildInTimeline = latestBuild(context.builds) - const isUpdated = event.data?.latest_build.updated_at !== latestBuildInTimeline.updated_at - if (isUpdated) { - return send({ type: "REFRESH_TIMELINE" }) - } - } else { - return send({ type: "REFRESH_TIMELINE" }) - } - }), + }, + guards: { + moreBuildsAvailable, }, services: { getWorkspace: async (_, event) => { @@ -535,40 +512,55 @@ export const workspaceMachine = createMachine( throw Error("Cannot get template without workspace") } }, - startWorkspaceWithLatestTemplate: async (context) => { + startWorkspaceWithLatestTemplate: (context) => async (send) => { if (context.workspace && context.template) { - return await API.startWorkspace(context.workspace.id, context.template.active_version_id) + const startWorkspacePromise = await API.startWorkspace( + context.workspace.id, + context.template.active_version_id, + ) + send({ type: "REFRESH_TIMELINE" }) + return startWorkspacePromise } else { throw Error("Cannot start workspace without workspace id") } }, - startWorkspace: async (context) => { + startWorkspace: (context) => async (send) => { if (context.workspace) { - return await API.startWorkspace( + const startWorkspacePromise = await API.startWorkspace( context.workspace.id, context.workspace.latest_build.template_version_id, ) + send({ type: "REFRESH_TIMELINE" }) + return startWorkspacePromise } else { throw Error("Cannot start workspace without workspace id") } }, - stopWorkspace: async (context) => { + stopWorkspace: (context) => async (send) => { if (context.workspace) { - return await API.stopWorkspace(context.workspace.id) + const stopWorkspacePromise = await API.stopWorkspace(context.workspace.id) + send({ type: "REFRESH_TIMELINE" }) + return stopWorkspacePromise } else { throw Error("Cannot stop workspace without workspace id") } }, deleteWorkspace: async (context) => { if (context.workspace) { - return await API.deleteWorkspace(context.workspace.id) + const deleteWorkspacePromise = await API.deleteWorkspace(context.workspace.id) + send({ type: "REFRESH_TIMELINE" }) + return deleteWorkspacePromise } else { throw Error("Cannot delete workspace without workspace id") } }, - cancelWorkspace: async (context) => { + cancelWorkspace: (context) => async (send) => { if (context.workspace) { - return await API.cancelWorkspaceBuild(context.workspace.latest_build.id) + 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") } @@ -583,7 +575,7 @@ export const workspaceMachine = createMachine( // refresh our workspace with each SSE send({ type: "REFRESH_WORKSPACE", data: JSON.parse(event.data) }) // refresh our timeline - send({ type: "CHECK_REFRESH_TIMELINE", data: JSON.parse(event.data) }) + send({ type: "REFRESH_TIMELINE", checkRefresh: true, data: JSON.parse(event.data) }) }) // handle any error events returned by our sse