Skip to content

fix: display explicit 'retry' button(s) when a workspace fails #10720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: restore granularity to retry operations (debug vs normal)
  • Loading branch information
Parkreiner committed Nov 16, 2023
commit edecd14e50c6966137daf205bdadbdde9518ab69
18 changes: 15 additions & 3 deletions site/src/pages/WorkspacePage/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export interface WorkspaceProps {
sshPrefix?: string;
template?: TypesGen.Template;
quotaBudget?: number;
canRetryDebugMode: boolean;
handleBuildRetry: () => void;
handleBuildRetryDebug: () => void;
buildLogs?: React.ReactNode;
builds: TypesGen.WorkspaceBuild[] | undefined;
onLoadMoreBuilds: () => void;
Expand All @@ -88,7 +90,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleCancel,
handleSettings,
handleChangeVersion,
handleDormantActivate: handleDormantActivate,
handleDormantActivate,
workspace,
isUpdating,
isRestarting,
Expand All @@ -103,7 +105,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
buildInfo,
sshPrefix,
template,
canRetryDebugMode,
handleBuildRetry,
handleBuildRetryDebug,
buildLogs,
onLoadMoreBuilds,
isLoadingMoreBuilds,
Expand Down Expand Up @@ -200,8 +204,10 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleCancel={handleCancel}
handleSettings={handleSettings}
handleRetry={handleBuildRetry}
handleRetryDebug={handleBuildRetryDebug}
handleChangeVersion={handleChangeVersion}
handleDormantActivate={handleDormantActivate}
canRetryDebug={canRetryDebugMode}
canChangeVersions={canChangeVersions}
isUpdating={isUpdating}
isRestarting={isRestarting}
Expand Down Expand Up @@ -305,8 +311,14 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
<Alert
severity="error"
actions={
<Button onClick={handleBuildRetry} variant="text" size="small">
Retry
<Button
onClick={
canRetryDebugMode ? handleBuildRetryDebug : handleBuildRetry
}
variant="text"
size="small"
>
Retry{canRetryDebugMode && " in debug mode"}
</Button>
}
>
Expand Down
16 changes: 11 additions & 5 deletions site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import BlockIcon from "@mui/icons-material/Block";
import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import RetryIcon from "@mui/icons-material/BuildOutlined";
import RetryDebugIcon from "@mui/icons-material/BugReportOutlined";

interface WorkspaceActionProps {
loading?: boolean;
Expand Down Expand Up @@ -168,12 +169,17 @@ export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
);
};

export function RetryButton({
handleAction,
}: Omit<WorkspaceActionProps, "loading">) {
type DebugButtonProps = Omit<WorkspaceActionProps, "loading"> & {
debug?: boolean;
};

export function RetryButton({ handleAction, debug = false }: DebugButtonProps) {
return (
<Button startIcon={<RetryIcon />} onClick={handleAction}>
Retry
<Button
startIcon={debug ? <RetryDebugIcon /> : <RetryIcon />}
onClick={handleAction}
>
Retry{debug && " (Debug)"}
</Button>
);
}
20 changes: 11 additions & 9 deletions site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ export interface WorkspaceActionsProps {
handleSettings: () => void;
handleChangeVersion: () => void;
handleRetry: () => void;
handleRetryDebug: () => void;
handleDormantActivate: () => void;
isUpdating: boolean;
isRestarting: boolean;
children?: ReactNode;
canChangeVersions: boolean;
canRetryDebug: boolean;
}

export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
Expand All @@ -56,11 +58,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
handleCancel,
handleSettings,
handleRetry,
handleRetryDebug,
handleChangeVersion,
handleDormantActivate: handleDormantActivate,
handleDormantActivate,
isUpdating,
isRestarting,
canChangeVersions,
canRetryDebug,
}) => {
const { duplicateWorkspace, isDuplicationReady } =
useWorkspaceDuplication(workspace);
Expand Down Expand Up @@ -92,26 +96,24 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
activate: <ActivateButton handleAction={handleDormantActivate} />,
activating: <ActivateButton loading handleAction={handleDormantActivate} />,
retry: <RetryButton handleAction={handleRetry} />,
retryDebug: <RetryButton debug handleAction={handleRetryDebug} />,
};

const { actions, canCancel, canAcceptJobs } = actionsByWorkspaceStatus(
workspace,
workspace.latest_build.status,
canChangeVersions,
{ canChangeVersions, canRetryDebug },
);

const canBeUpdated = workspace.outdated && canAcceptJobs;

return (
<div
css={{ display: "flex", alignItems: "center", gap: 12 }}
data-testid="workspace-actions"
>
{/*
* Parentheses important – if canBeUpdated is false, nothing should
* appear in the UI
*/}
{canBeUpdated &&
(isUpdating ? buttonMapping.updating : buttonMapping.update)}
{canBeUpdated && (
<>{isUpdating ? buttonMapping.updating : buttonMapping.update}</>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge change, but I wanted to make it super explicit that the logic was accounting for three cases:

  1. Render nothing if canBeUpdated is false
  2. Render updating component
  3. Render update component

)}

{isRestarting
? buttonMapping.restarting
Expand Down
31 changes: 24 additions & 7 deletions site/src/pages/WorkspacePage/WorkspaceActions/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const buttonTypes = [
// into one of the starting, stopping, or deleting states (based on the
// WorkspaceTransition type)
"retry",
"retryDebug",

// These are buttons that should be used with disabled UI elements
"canceling",
Expand All @@ -33,16 +34,20 @@ const buttonTypes = [
*/
export type ButtonType = (typeof buttonTypes)[number];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this syntax...what is going on here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a little bit to unpack, but:

  1. We take the runtime buttonTypes value and turn it into a type definition via typeof
  2. And because buttonTypes is defined via as const, it's treated as a fixed-length tuple. So, TypeScript knows there are 16 elements in the array exactly, and it also knows the string literal that goes in each slot
  3. Then, when we index that type definition via [number], we're saying to grab the type values at each slot. (typeof buttonTypes)[0] would give us "start", while (typeof buttonTypes)[0 | 1 | 2] would give us "start" | "starting" | "stop". So the [number] index basically says "give us everything at every numeric index you have"
  4. At the end, we have a union of all the types, but the types are defined runtime-first via the const array, so we're free to use it as a value (like combining it with Array.includes)

I did forget to actually export the array, though, so I'll fix that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the explanation. It would be nice to use this syntax with TypesGen.WorkspaceTransitions but idk how to make that happen on the Go side


interface WorkspaceAbilities {
type WorkspaceAbilities = {
actions: readonly ButtonType[];
canCancel: boolean;
canAcceptJobs: boolean;
}
};

type UserInfo = Readonly<{
canChangeVersions: boolean;
canRetryDebug: boolean;
}>;

export const actionsByWorkspaceStatus = (
workspace: Workspace,
status: WorkspaceStatus,
canChangeVersions: boolean,
userInfo: UserInfo,
): WorkspaceAbilities => {
if (workspace.dormant_at) {
return {
Expand All @@ -51,17 +56,21 @@ export const actionsByWorkspaceStatus = (
canAcceptJobs: false,
};
}
if (

const status = workspace.latest_build.status;
const mustUpdate =
workspace.outdated &&
workspaceUpdatePolicy(workspace, canChangeVersions) === "always"
) {
workspaceUpdatePolicy(workspace, userInfo.canChangeVersions) === "always";

if (mustUpdate) {
if (status === "running") {
return {
actions: ["stop"],
canCancel: false,
canAcceptJobs: true,
};
}

if (status === "stopped") {
return {
actions: [],
Expand All @@ -70,6 +79,14 @@ export const actionsByWorkspaceStatus = (
};
}
}

if (status === "failed" && userInfo.canRetryDebug) {
return {
...statusToActions.failed,
actions: ["retry", "retryDebug"],
};
}

return statusToActions[status];
};

Expand Down
38 changes: 21 additions & 17 deletions site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ export const WorkspaceReadyPage = ({
...deploymentConfig(),
enabled: permissions?.viewDeploymentValues,
});
const canRetryDebugMode = Boolean(
deploymentValues?.config.enable_terraform_debug_mode,
);

// Build logs
const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id);
Expand Down Expand Up @@ -196,6 +193,22 @@ export const WorkspaceReadyPage = ({
// Cancel build
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));

const handleBuildRetry = (debug = false) => {
const logLevel = debug ? "debug" : undefined;

switch (workspace.latest_build.transition) {
case "start":
startWorkspaceMutation.mutate({ logLevel });
break;
case "stop":
stopWorkspaceMutation.mutate({ logLevel });
break;
case "delete":
deleteWorkspaceMutation.mutate({ logLevel });
break;
}
};

return (
<>
<Helmet>
Expand Down Expand Up @@ -242,20 +255,11 @@ export const WorkspaceReadyPage = ({
}}
handleCancel={cancelBuildMutation.mutate}
handleSettings={() => navigate("settings")}
handleBuildRetry={() => {
const logLevel = canRetryDebugMode ? "debug" : undefined;
switch (workspace.latest_build.transition) {
case "start":
startWorkspaceMutation.mutate({ logLevel });
break;
case "stop":
stopWorkspaceMutation.mutate({ logLevel });
break;
case "delete":
deleteWorkspaceMutation.mutate({ logLevel });
break;
}
}}
handleBuildRetry={() => handleBuildRetry(false)}
handleBuildRetryDebug={() => handleBuildRetry(true)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing 2 props here, can we just have handleBuildRetry and then pass it a boolean in the Workspace component?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would work – my main concern with setting up the functions the way I did was insulating the downstream components from changes, or having implementation details leak out

Right now, handleBuildRetry and handleBuildRetryDebug happen to use the same implementation under the hood, but that's just kind of by coincidence. If WorkspaceReadyPage ever gets to the point where the two functions need to look drastically different, we can change things in the one file, and none of the other components have to be any the wiser

I am aware that it does kind of increase the number of props (when we already have a lot), but keeping the other components blind to the implementation details seemed like the lesser of two evils to me

Copy link
Member

@Kira-Pilot Kira-Pilot Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If WorkspaceReadyPage ever gets to the point where the two functions need to look drastically different, we can change things in the one file, and none of the other components have to be any the wiser

I think we might be pre-optimizing here, but I'm happy to keep as-is!

canRetryDebugMode={
deploymentValues?.config.enable_terraform_debug_mode ?? false
}
handleChangeVersion={() => {
setChangeVersionDialogOpen(true);
}}
Expand Down