mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: Optimistically update the UI when a workspace action is triggered (#4929)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user