mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
fix: Optimistically update the UI when a workspace action is triggered (#4898)
This commit is contained in:
@ -92,16 +92,6 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
describe("WorkspacePage", () => {
|
||||
it("requests a stop job when the user presses Stop", async () => {
|
||||
const stopWorkspaceMock = jest
|
||||
.spyOn(api, "stopWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
testButton(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
stopWorkspaceMock,
|
||||
)
|
||||
})
|
||||
|
||||
it("requests a delete job when the user presses Delete and confirms", async () => {
|
||||
const user = userEvent.setup()
|
||||
const deleteWorkspaceMock = jest
|
||||
@ -140,11 +130,23 @@ describe("WorkspacePage", () => {
|
||||
const startWorkspaceMock = jest
|
||||
.spyOn(api, "startWorkspace")
|
||||
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild))
|
||||
testButton(
|
||||
await testButton(
|
||||
t("actionButton.start", { ns: "workspacePage" }),
|
||||
startWorkspaceMock,
|
||||
)
|
||||
})
|
||||
|
||||
it("requests a stop job when the user presses Stop", async () => {
|
||||
const stopWorkspaceMock = jest
|
||||
.spyOn(api, "stopWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
|
||||
await testButton(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
stopWorkspaceMock,
|
||||
)
|
||||
})
|
||||
|
||||
it("requests cancellation when the user presses Cancel", async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
|
@ -40,6 +40,27 @@ const moreBuildsAvailable = (
|
||||
return event.data.latest_build.updated_at !== latestBuildInTimeline.updated_at
|
||||
}
|
||||
|
||||
const updateWorkspaceStatus = (
|
||||
status: TypesGen.WorkspaceStatus,
|
||||
workspace?: TypesGen.Workspace,
|
||||
) => {
|
||||
if (!workspace) {
|
||||
throw new Error("Workspace not defined")
|
||||
}
|
||||
|
||||
return {
|
||||
...workspace,
|
||||
latest_build: {
|
||||
...workspace.latest_build,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const isUpdated = (newDateStr: string, oldDateStr: string): boolean => {
|
||||
return new Date(oldDateStr).getTime() - new Date(newDateStr).getTime() > 0
|
||||
}
|
||||
|
||||
const Language = {
|
||||
getTemplateWarning:
|
||||
"Error updating workspace: latest template could not be fetched.",
|
||||
@ -252,6 +273,7 @@ export const workspaceMachine = createMachine(
|
||||
on: {
|
||||
REFRESH_WORKSPACE: {
|
||||
actions: ["refreshWorkspace"],
|
||||
cond: "hasUpdates",
|
||||
},
|
||||
EVENT_SOURCE_ERROR: {
|
||||
target: "error",
|
||||
@ -325,7 +347,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
requestingStart: {
|
||||
entry: "clearBuildError",
|
||||
entry: ["clearBuildError", "updateStatusToStarting"],
|
||||
invoke: {
|
||||
src: "startWorkspace",
|
||||
id: "startWorkspace",
|
||||
@ -344,7 +366,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
requestingStop: {
|
||||
entry: "clearBuildError",
|
||||
entry: ["clearBuildError", "updateStatusToStopping"],
|
||||
invoke: {
|
||||
src: "stopWorkspace",
|
||||
id: "stopWorkspace",
|
||||
@ -363,7 +385,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
requestingDelete: {
|
||||
entry: "clearBuildError",
|
||||
entry: ["clearBuildError", "updateStatusToDeleting"],
|
||||
invoke: {
|
||||
src: "deleteWorkspace",
|
||||
id: "deleteWorkspace",
|
||||
@ -382,7 +404,11 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
requestingCancel: {
|
||||
entry: ["clearCancellationMessage", "clearCancellationError"],
|
||||
entry: [
|
||||
"clearCancellationMessage",
|
||||
"clearCancellationError",
|
||||
"updateStatusToCanceling",
|
||||
],
|
||||
invoke: {
|
||||
src: "cancelWorkspace",
|
||||
id: "cancelWorkspace",
|
||||
@ -430,9 +456,7 @@ export const workspaceMachine = createMachine(
|
||||
on: {
|
||||
REFRESH_TIMELINE: {
|
||||
target: "#workspaceState.ready.timeline.gettingBuilds",
|
||||
cond: {
|
||||
type: "moreBuildsAvailable",
|
||||
},
|
||||
cond: "moreBuildsAvailable",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -599,9 +623,46 @@ export const workspaceMachine = createMachine(
|
||||
}),
|
||||
{ to: "scheduleBannerMachine" },
|
||||
),
|
||||
// Optimistically updates. So when the user clicks on stop, we can show
|
||||
// the "stopping" state right away without having to wait 0.5s ~ 2s to
|
||||
// display the visual feedback to the user.
|
||||
updateStatusToStarting: assign({
|
||||
workspace: ({ workspace }) =>
|
||||
updateWorkspaceStatus("starting", workspace),
|
||||
}),
|
||||
updateStatusToStopping: assign({
|
||||
workspace: ({ workspace }) =>
|
||||
updateWorkspaceStatus("stopping", workspace),
|
||||
}),
|
||||
updateStatusToDeleting: assign({
|
||||
workspace: ({ workspace }) =>
|
||||
updateWorkspaceStatus("deleting", workspace),
|
||||
}),
|
||||
updateStatusToCanceling: assign({
|
||||
workspace: ({ workspace }) =>
|
||||
updateWorkspaceStatus("canceling", workspace),
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
moreBuildsAvailable,
|
||||
// We only want to update the workspace when there are changes to it to
|
||||
// avoid re-renderings and allow optimistically updates to improve the UI.
|
||||
// When updating the workspace every second, the optimistic updates that
|
||||
// were applied before get lost since it will be rewrite.
|
||||
hasUpdates: ({ workspace }, event: { data: TypesGen.Workspace }) => {
|
||||
if (!workspace) {
|
||||
throw new Error("Workspace not defined")
|
||||
}
|
||||
const isWorkspaceUpdated = isUpdated(
|
||||
event.data.updated_at,
|
||||
workspace.updated_at,
|
||||
)
|
||||
const isBuildUpdated = isUpdated(
|
||||
event.data.latest_build.updated_at,
|
||||
workspace.latest_build.updated_at,
|
||||
)
|
||||
return isWorkspaceUpdated || isBuildUpdated
|
||||
},
|
||||
},
|
||||
services: {
|
||||
getWorkspace: async (_, event) => {
|
||||
|
Reference in New Issue
Block a user