fix: Optimistically update the UI when a workspace action is triggered (#4898)

This commit is contained in:
Bruno Quaresma
2022-11-04 16:09:45 -03:00
committed by GitHub
parent 55fe26bdfa
commit 8f4ae5b6ac
2 changed files with 81 additions and 18 deletions

View File

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

View File

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