Skip to content

Commit f4026ed

Browse files
authored
feat: add frontend support for enabling automatic workspace updates (#10375)
1 parent 3200b85 commit f4026ed

File tree

11 files changed

+137
-24
lines changed

11 files changed

+137
-24
lines changed

site/src/api/api.ts

+15
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,21 @@ export const updateWorkspaceDormancy = async (
576576
return response.data;
577577
};
578578

579+
export const updateWorkspaceAutomaticUpdates = async (
580+
workspaceId: string,
581+
automaticUpdates: TypesGen.AutomaticUpdates,
582+
): Promise<void> => {
583+
const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
584+
automatic_updates: automaticUpdates,
585+
};
586+
587+
const response = await axios.put(
588+
`/api/v2/workspaces/${workspaceId}/autoupdates`,
589+
req,
590+
);
591+
return response.data;
592+
};
593+
579594
export const restartWorkspace = async ({
580595
workspace,
581596
buildParameters,

site/src/components/Dashboard/DashboardProvider.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,11 @@ export const useIsWorkspaceActionsEnabled = (): boolean => {
121121
const allowWorkspaceActions = experiments.includes("workspace_actions");
122122
return allowWorkspaceActions && allowAdvancedScheduling;
123123
};
124+
125+
export const useTemplatePoliciesEnabled = (): boolean => {
126+
const { entitlements, experiments } = useDashboard();
127+
return (
128+
entitlements.features.access_control.enabled &&
129+
experiments.includes("template_update_policies")
130+
);
131+
};

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,15 @@ import { useTemplateSettings } from "../TemplateSettingsLayout";
1010
import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
1111
import { templateByNameKey } from "api/queries/templates";
1212
import { useOrganizationId } from "hooks";
13-
import { useDashboard } from "components/Dashboard/DashboardProvider";
13+
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
1414

1515
export const TemplateSettingsPage: FC = () => {
1616
const { template: templateName } = useParams() as { template: string };
1717
const navigate = useNavigate();
1818
const orgId = useOrganizationId();
1919
const { template } = useTemplateSettings();
2020
const queryClient = useQueryClient();
21-
const { entitlements, experiments } = useDashboard();
22-
const accessControlEnabled =
23-
entitlements.features["advanced_template_scheduling"].enabled &&
24-
experiments.includes("template_update_policies");
21+
const accessControlEnabled = useTemplatePoliciesEnabled();
2522

2623
const {
2724
mutate: updateTemplate,

site/src/pages/WorkspacePage/Workspace.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
191191
quotaBudget={quotaBudget}
192192
handleUpdate={handleUpdate}
193193
canUpdateWorkspace={canUpdateWorkspace}
194+
canChangeVersions={canChangeVersions}
194195
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
195196
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
196197
onDeadlineMinus={scheduleProps.onDeadlineMinus}

site/src/pages/WorkspacePage/WorkspaceActions/constants.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Workspace, WorkspaceStatus } from "api/typesGenerated";
22
import { ReactNode } from "react";
3+
import { workspaceUpdatePolicy } from "utils/workspace";
34

45
// the button types we have
56
export enum ButtonTypesEnum {
@@ -43,9 +44,8 @@ export const actionsByWorkspaceStatus = (
4344
};
4445
}
4546
if (
46-
workspace.template_require_active_version &&
4747
workspace.outdated &&
48-
!canChangeVersions
48+
workspaceUpdatePolicy(workspace, canChangeVersions)
4949
) {
5050
if (status === "running") {
5151
return {

site/src/pages/WorkspacePage/WorkspaceStats.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getDisplayWorkspaceBuildInitiatedBy,
88
getDisplayWorkspaceTemplateName,
99
isWorkspaceOn,
10+
workspaceUpdatePolicy,
1011
} from "utils/workspace";
1112
import { Workspace } from "api/typesGenerated";
1213
import { Stats, StatsItem } from "components/Stats/Stats";
@@ -26,6 +27,12 @@ import {
2627
PopoverTrigger,
2728
usePopover,
2829
} from "components/Popover/Popover";
30+
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
31+
import {
32+
HelpTooltip,
33+
HelpTooltipText,
34+
} from "components/HelpTooltip/HelpTooltip";
35+
import { Stack } from "components/Stack/Stack";
2936

3037
const Language = {
3138
workspaceDetails: "Workspace Details",
@@ -37,13 +44,15 @@ const Language = {
3744
upToDate: "Up to date",
3845
byLabel: "Last built by",
3946
costLabel: "Daily cost",
47+
updatePolicy: "Update policy",
4048
};
4149

4250
export interface WorkspaceStatsProps {
4351
workspace: Workspace;
4452
maxDeadlineIncrease: number;
4553
maxDeadlineDecrease: number;
4654
canUpdateWorkspace: boolean;
55+
canChangeVersions: boolean;
4756
quotaBudget?: number;
4857
onDeadlinePlus: (hours: number) => void;
4958
onDeadlineMinus: (hours: number) => void;
@@ -56,6 +65,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
5665
maxDeadlineDecrease,
5766
maxDeadlineIncrease,
5867
canUpdateWorkspace,
68+
canChangeVersions,
5969
handleUpdate,
6070
onDeadlineMinus,
6171
onDeadlinePlus,
@@ -67,6 +77,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
6777
const styles = useStyles();
6878
const deadlinePlusEnabled = maxDeadlineIncrease >= 1;
6979
const deadlineMinusEnabled = maxDeadlineDecrease >= 1;
80+
const templatePoliciesEnabled = useTemplatePoliciesEnabled();
7081

7182
return (
7283
<>
@@ -198,6 +209,27 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
198209
}`}
199210
/>
200211
)}
212+
{templatePoliciesEnabled && (
213+
<Stack direction="row" spacing={0.5}>
214+
<StatsItem
215+
className={styles.statsItem}
216+
label={Language.updatePolicy}
217+
value={upperFirst(
218+
workspaceUpdatePolicy(workspace, canChangeVersions),
219+
)}
220+
/>
221+
{workspace.automatic_updates === "never" &&
222+
workspace.template_require_active_version &&
223+
!canChangeVersions && (
224+
<HelpTooltip>
225+
<HelpTooltipText>
226+
Your workspace has not opted in to automatic updates but
227+
your template requires updating to the active version.
228+
</HelpTooltipText>
229+
</HelpTooltip>
230+
)}
231+
</Stack>
232+
)}
201233
</Stats>
202234
</>
203235
);

site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx

+41-6
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,36 @@ import {
1313
getFormHelpers,
1414
onChangeTrimmed,
1515
} from "utils/formUtils";
16-
import { Workspace } from "api/typesGenerated";
16+
import {
17+
AutomaticUpdates,
18+
AutomaticUpdateses,
19+
Workspace,
20+
} from "api/typesGenerated";
1721
import { Alert } from "components/Alert/Alert";
22+
import MenuItem from "@mui/material/MenuItem";
23+
import upperFirst from "lodash/upperFirst";
1824

1925
export type WorkspaceSettingsFormValues = {
2026
name: string;
27+
automatic_updates: AutomaticUpdates;
2128
};
2229

2330
export const WorkspaceSettingsForm: FC<{
24-
isSubmitting: boolean;
2531
workspace: Workspace;
2632
error: unknown;
33+
templatePoliciesEnabled: boolean;
2734
onCancel: () => void;
28-
onSubmit: (values: WorkspaceSettingsFormValues) => void;
29-
}> = ({ onCancel, onSubmit, workspace, error, isSubmitting }) => {
35+
onSubmit: (values: WorkspaceSettingsFormValues) => Promise<void>;
36+
}> = ({ onCancel, onSubmit, workspace, error, templatePoliciesEnabled }) => {
3037
const form = useFormik<WorkspaceSettingsFormValues>({
3138
onSubmit,
3239
initialValues: {
3340
name: workspace.name,
41+
automatic_updates: workspace.automatic_updates,
3442
},
3543
validationSchema: Yup.object({
3644
name: nameValidator("Name"),
45+
automatic_updates: Yup.string().oneOf(AutomaticUpdateses),
3746
}),
3847
});
3948
const getFieldHelpers = getFormHelpers<WorkspaceSettingsFormValues>(
@@ -43,7 +52,10 @@ export const WorkspaceSettingsForm: FC<{
4352

4453
return (
4554
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
46-
<FormSection title="General" description="The name of your workspace.">
55+
<FormSection
56+
title="Workspace Name"
57+
description="Update the name of your workspace."
58+
>
4759
<FormFields>
4860
<TextField
4961
{...getFieldHelpers("name")}
@@ -61,7 +73,30 @@ export const WorkspaceSettingsForm: FC<{
6173
)}
6274
</FormFields>
6375
</FormSection>
64-
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
76+
{templatePoliciesEnabled && (
77+
<FormSection
78+
title="Automatic Updates"
79+
description="Configure your workspace to automatically update when started."
80+
>
81+
<FormFields>
82+
<TextField
83+
{...getFieldHelpers("automatic_updates")}
84+
id="automatic_updates"
85+
label="Update Policy"
86+
value={form.values.automatic_updates}
87+
select
88+
disabled={form.isSubmitting}
89+
>
90+
{AutomaticUpdateses.map((value) => (
91+
<MenuItem value={value} key={value}>
92+
{upperFirst(value)}
93+
</MenuItem>
94+
))}
95+
</TextField>
96+
</FormFields>
97+
</FormSection>
98+
)}
99+
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
65100
</HorizontalForm>
66101
);
67102
};

site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx

+15-5
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { useWorkspaceSettings } from "./WorkspaceSettingsLayout";
55
import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView";
66
import { useMutation } from "react-query";
77
import { displaySuccess } from "components/GlobalSnackbar/utils";
8-
import { patchWorkspace } from "api/api";
8+
import { patchWorkspace, updateWorkspaceAutomaticUpdates } from "api/api";
99
import { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm";
10+
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
1011

1112
const WorkspaceSettingsPage = () => {
1213
const params = useParams() as {
@@ -17,9 +18,18 @@ const WorkspaceSettingsPage = () => {
1718
const username = params.username.replace("@", "");
1819
const workspace = useWorkspaceSettings();
1920
const navigate = useNavigate();
21+
const templatePoliciesEnabled = useTemplatePoliciesEnabled();
22+
2023
const mutation = useMutation({
21-
mutationFn: (formValues: WorkspaceSettingsFormValues) =>
22-
patchWorkspace(workspace.id, { name: formValues.name }),
24+
mutationFn: async (formValues: WorkspaceSettingsFormValues) => {
25+
await Promise.all([
26+
patchWorkspace(workspace.id, { name: formValues.name }),
27+
updateWorkspaceAutomaticUpdates(
28+
workspace.id,
29+
formValues.automatic_updates,
30+
),
31+
]);
32+
},
2333
onSuccess: (_, formValues) => {
2434
displaySuccess("Workspace updated successfully");
2535
navigate(`/@${username}/${formValues.name}/settings`);
@@ -34,10 +44,10 @@ const WorkspaceSettingsPage = () => {
3444

3545
<WorkspaceSettingsPageView
3646
error={mutation.error}
37-
isSubmitting={mutation.isLoading}
3847
workspace={workspace}
3948
onCancel={() => navigate(`/@${username}/${workspaceName}`)}
40-
onSubmit={mutation.mutate}
49+
onSubmit={mutation.mutateAsync}
50+
templatePoliciesEnabled={templatePoliciesEnabled}
4151
/>
4252
</>
4353
);

site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ const meta: Meta<typeof WorkspaceSettingsPageView> = {
77
component: WorkspaceSettingsPageView,
88
args: {
99
error: undefined,
10-
isSubmitting: false,
1110
workspace: MockWorkspace,
1211
},
1312
};
1413

1514
export default meta;
1615
type Story = StoryObj<typeof WorkspaceSettingsPageView>;
1716

18-
const Example: Story = {};
17+
export const Example: Story = {};
1918

20-
export { Example as WorkspaceSettingsPageView };
19+
export const AutoUpdates: Story = {
20+
args: {
21+
templatePoliciesEnabled: true,
22+
},
23+
};

site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import { Workspace } from "api/typesGenerated";
55

66
export type WorkspaceSettingsPageViewProps = {
77
error: unknown;
8-
isSubmitting: boolean;
98
workspace: Workspace;
109
onCancel: () => void;
1110
onSubmit: ComponentProps<typeof WorkspaceSettingsForm>["onSubmit"];
11+
templatePoliciesEnabled: boolean;
1212
};
1313

1414
export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
1515
onCancel,
1616
onSubmit,
17-
isSubmitting,
1817
error,
1918
workspace,
19+
templatePoliciesEnabled,
2020
}) => {
2121
return (
2222
<>
@@ -30,10 +30,10 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
3030

3131
<WorkspaceSettingsForm
3232
error={error}
33-
isSubmitting={isSubmitting}
3433
workspace={workspace}
3534
onCancel={onCancel}
3635
onSubmit={onSubmit}
36+
templatePoliciesEnabled={templatePoliciesEnabled}
3737
/>
3838
</>
3939
);

site/src/utils/workspace.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,15 @@ export const getMatchingAgentOrFirst = (
303303
})
304304
.filter((a) => a)[0];
305305
};
306+
307+
export const workspaceUpdatePolicy = (
308+
workspace: TypesGen.Workspace,
309+
canChangeVersions: boolean,
310+
): TypesGen.AutomaticUpdates => {
311+
// If a template requires the active version and you cannot change versions
312+
// (restricted to template admins), then your policy must be "Always".
313+
if (workspace.template_require_active_version && !canChangeVersions) {
314+
return "always";
315+
}
316+
return workspace.automatic_updates;
317+
};

0 commit comments

Comments
 (0)