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

This commit is contained in:
Bruno Quaresma
2022-11-08 09:35:19 -05:00
committed by GitHub
parent bf4a6fb5b5
commit e7bd04999f
6 changed files with 45 additions and 24 deletions
@@ -160,7 +160,6 @@ const useStyles = makeStyles((theme) => ({
// this is all custom to work with our button wrapper
loadingButton: {
border: "none",
borderLeft: "1px solid #333740", // MUI disabled button
borderRadius: "3px 0px 0px 3px",
borderRadius: `${theme.shape.borderRadius} 0px 0px ${theme.shape.borderRadius}`,
},
}))
@@ -95,7 +95,7 @@ const useStyles = makeStyles((theme) => ({
borderLeft: `1px solid ${theme.palette.divider}`,
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
minWidth: "unset",
width: "63px", // matching cancel button so button grouping doesn't grow in size
width: "64px", // matching cancel button so button grouping doesn't grow in size
"& .MuiButton-label": {
marginRight: "8px",
},
@@ -31,7 +31,7 @@ export const LoadingButton: FC<React.PropsWithChildren<LoadingButtonProps>> = ({
<span style={hidden}>{children}</span>
{loading && (
<div className={styles.loader}>
<CircularProgress size={18} className={styles.spinner} />
<CircularProgress size={16} className={styles.spinner} />
</div>
)}
{Boolean(loadingLabel) && loadingLabel}
@@ -63,7 +63,7 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
top: "50%",
left: "50%",
height: 22, // centering loading icon
width: 18,
width: 16,
},
spinner: {
color: theme.palette.text.disabled,
@@ -64,7 +64,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
<DisabledButton label={t("disabledButton.deleted")} />
),
[ButtonTypesEnum.pending]: (
<DisabledButton label={t("disabledButton.pending")} />
<ActionLoadingButton label={t("disabledButton.pending")} />
),
}
@@ -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(
@@ -325,7 +325,7 @@ export const workspaceMachine = createMachine(
},
},
requestingStart: {
entry: "clearBuildError",
entry: ["clearBuildError", "updateStatusToPending"],
invoke: {
src: "startWorkspace",
id: "startWorkspace",
@@ -344,7 +344,7 @@ export const workspaceMachine = createMachine(
},
},
requestingStop: {
entry: "clearBuildError",
entry: ["clearBuildError", "updateStatusToPending"],
invoke: {
src: "stopWorkspace",
id: "stopWorkspace",
@@ -363,7 +363,7 @@ export const workspaceMachine = createMachine(
},
},
requestingDelete: {
entry: "clearBuildError",
entry: ["clearBuildError", "updateStatusToPending"],
invoke: {
src: "deleteWorkspace",
id: "deleteWorkspace",
@@ -382,7 +382,11 @@ export const workspaceMachine = createMachine(
},
},
requestingCancel: {
entry: ["clearCancellationMessage", "clearCancellationError"],
entry: [
"clearCancellationMessage",
"clearCancellationError",
"updateStatusToPending",
],
invoke: {
src: "cancelWorkspace",
id: "cancelWorkspace",
@@ -430,9 +434,7 @@ export const workspaceMachine = createMachine(
on: {
REFRESH_TIMELINE: {
target: "#workspaceState.ready.timeline.gettingBuilds",
cond: {
type: "moreBuildsAvailable",
},
cond: "moreBuildsAvailable",
},
},
},
@@ -599,6 +601,24 @@ export const workspaceMachine = createMachine(
}),
{ to: "scheduleBannerMachine" },
),
// Optimistically update. So when the user clicks on stop, we can show
// the "pending" state right away without having to wait 0.5s ~ 2s to
// display the visual feedback to the user.
updateStatusToPending: assign({
workspace: ({ workspace }) => {
if (!workspace) {
throw new Error("Workspace not defined")
}
return {
...workspace,
latest_build: {
...workspace.latest_build,
status: "pending" as TypesGen.WorkspaceStatus,
},
}
},
}),
},
guards: {
moreBuildsAvailable,