mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: add frontend support for enabling automatic workspace updates (#10375)
This commit is contained in:
@ -576,6 +576,21 @@ export const updateWorkspaceDormancy = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateWorkspaceAutomaticUpdates = async (
|
||||
workspaceId: string,
|
||||
automaticUpdates: TypesGen.AutomaticUpdates,
|
||||
): Promise<void> => {
|
||||
const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = {
|
||||
automatic_updates: automaticUpdates,
|
||||
};
|
||||
|
||||
const response = await axios.put(
|
||||
`/api/v2/workspaces/${workspaceId}/autoupdates`,
|
||||
req,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const restartWorkspace = async ({
|
||||
workspace,
|
||||
buildParameters,
|
||||
|
@ -121,3 +121,11 @@ export const useIsWorkspaceActionsEnabled = (): boolean => {
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||
return allowWorkspaceActions && allowAdvancedScheduling;
|
||||
};
|
||||
|
||||
export const useTemplatePoliciesEnabled = (): boolean => {
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
return (
|
||||
entitlements.features.access_control.enabled &&
|
||||
experiments.includes("template_update_policies")
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ import { useTemplateSettings } from "../TemplateSettingsLayout";
|
||||
import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
|
||||
import { templateByNameKey } from "api/queries/templates";
|
||||
import { useOrganizationId } from "hooks";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
|
||||
|
||||
export const TemplateSettingsPage: FC = () => {
|
||||
const { template: templateName } = useParams() as { template: string };
|
||||
@ -18,10 +18,7 @@ export const TemplateSettingsPage: FC = () => {
|
||||
const orgId = useOrganizationId();
|
||||
const { template } = useTemplateSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const accessControlEnabled =
|
||||
entitlements.features["advanced_template_scheduling"].enabled &&
|
||||
experiments.includes("template_update_policies");
|
||||
const accessControlEnabled = useTemplatePoliciesEnabled();
|
||||
|
||||
const {
|
||||
mutate: updateTemplate,
|
||||
|
@ -191,6 +191,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
quotaBudget={quotaBudget}
|
||||
handleUpdate={handleUpdate}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
|
||||
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
|
||||
onDeadlineMinus={scheduleProps.onDeadlineMinus}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Workspace, WorkspaceStatus } from "api/typesGenerated";
|
||||
import { ReactNode } from "react";
|
||||
import { workspaceUpdatePolicy } from "utils/workspace";
|
||||
|
||||
// the button types we have
|
||||
export enum ButtonTypesEnum {
|
||||
@ -43,9 +44,8 @@ export const actionsByWorkspaceStatus = (
|
||||
};
|
||||
}
|
||||
if (
|
||||
workspace.template_require_active_version &&
|
||||
workspace.outdated &&
|
||||
!canChangeVersions
|
||||
workspaceUpdatePolicy(workspace, canChangeVersions)
|
||||
) {
|
||||
if (status === "running") {
|
||||
return {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
getDisplayWorkspaceTemplateName,
|
||||
isWorkspaceOn,
|
||||
workspaceUpdatePolicy,
|
||||
} from "utils/workspace";
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import { Stats, StatsItem } from "components/Stats/Stats";
|
||||
@ -26,6 +27,12 @@ import {
|
||||
PopoverTrigger,
|
||||
usePopover,
|
||||
} from "components/Popover/Popover";
|
||||
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipText,
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
|
||||
const Language = {
|
||||
workspaceDetails: "Workspace Details",
|
||||
@ -37,6 +44,7 @@ const Language = {
|
||||
upToDate: "Up to date",
|
||||
byLabel: "Last built by",
|
||||
costLabel: "Daily cost",
|
||||
updatePolicy: "Update policy",
|
||||
};
|
||||
|
||||
export interface WorkspaceStatsProps {
|
||||
@ -44,6 +52,7 @@ export interface WorkspaceStatsProps {
|
||||
maxDeadlineIncrease: number;
|
||||
maxDeadlineDecrease: number;
|
||||
canUpdateWorkspace: boolean;
|
||||
canChangeVersions: boolean;
|
||||
quotaBudget?: number;
|
||||
onDeadlinePlus: (hours: number) => void;
|
||||
onDeadlineMinus: (hours: number) => void;
|
||||
@ -56,6 +65,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
maxDeadlineDecrease,
|
||||
maxDeadlineIncrease,
|
||||
canUpdateWorkspace,
|
||||
canChangeVersions,
|
||||
handleUpdate,
|
||||
onDeadlineMinus,
|
||||
onDeadlinePlus,
|
||||
@ -67,6 +77,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
const styles = useStyles();
|
||||
const deadlinePlusEnabled = maxDeadlineIncrease >= 1;
|
||||
const deadlineMinusEnabled = maxDeadlineDecrease >= 1;
|
||||
const templatePoliciesEnabled = useTemplatePoliciesEnabled();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -198,6 +209,27 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{templatePoliciesEnabled && (
|
||||
<Stack direction="row" spacing={0.5}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label={Language.updatePolicy}
|
||||
value={upperFirst(
|
||||
workspaceUpdatePolicy(workspace, canChangeVersions),
|
||||
)}
|
||||
/>
|
||||
{workspace.automatic_updates === "never" &&
|
||||
workspace.template_require_active_version &&
|
||||
!canChangeVersions && (
|
||||
<HelpTooltip>
|
||||
<HelpTooltipText>
|
||||
Your workspace has not opted in to automatic updates but
|
||||
your template requires updating to the active version.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stats>
|
||||
</>
|
||||
);
|
||||
|
@ -13,27 +13,36 @@ import {
|
||||
getFormHelpers,
|
||||
onChangeTrimmed,
|
||||
} from "utils/formUtils";
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import {
|
||||
AutomaticUpdates,
|
||||
AutomaticUpdateses,
|
||||
Workspace,
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
|
||||
export type WorkspaceSettingsFormValues = {
|
||||
name: string;
|
||||
automatic_updates: AutomaticUpdates;
|
||||
};
|
||||
|
||||
export const WorkspaceSettingsForm: FC<{
|
||||
isSubmitting: boolean;
|
||||
workspace: Workspace;
|
||||
error: unknown;
|
||||
templatePoliciesEnabled: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: WorkspaceSettingsFormValues) => void;
|
||||
}> = ({ onCancel, onSubmit, workspace, error, isSubmitting }) => {
|
||||
onSubmit: (values: WorkspaceSettingsFormValues) => Promise<void>;
|
||||
}> = ({ onCancel, onSubmit, workspace, error, templatePoliciesEnabled }) => {
|
||||
const form = useFormik<WorkspaceSettingsFormValues>({
|
||||
onSubmit,
|
||||
initialValues: {
|
||||
name: workspace.name,
|
||||
automatic_updates: workspace.automatic_updates,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: nameValidator("Name"),
|
||||
automatic_updates: Yup.string().oneOf(AutomaticUpdateses),
|
||||
}),
|
||||
});
|
||||
const getFieldHelpers = getFormHelpers<WorkspaceSettingsFormValues>(
|
||||
@ -43,7 +52,10 @@ export const WorkspaceSettingsForm: FC<{
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
|
||||
<FormSection title="General" description="The name of your workspace.">
|
||||
<FormSection
|
||||
title="Workspace Name"
|
||||
description="Update the name of your workspace."
|
||||
>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
@ -61,7 +73,30 @@ export const WorkspaceSettingsForm: FC<{
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
{templatePoliciesEnabled && (
|
||||
<FormSection
|
||||
title="Automatic Updates"
|
||||
description="Configure your workspace to automatically update when started."
|
||||
>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("automatic_updates")}
|
||||
id="automatic_updates"
|
||||
label="Update Policy"
|
||||
value={form.values.automatic_updates}
|
||||
select
|
||||
disabled={form.isSubmitting}
|
||||
>
|
||||
{AutomaticUpdateses.map((value) => (
|
||||
<MenuItem value={value} key={value}>
|
||||
{upperFirst(value)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -5,8 +5,9 @@ import { useWorkspaceSettings } from "./WorkspaceSettingsLayout";
|
||||
import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView";
|
||||
import { useMutation } from "react-query";
|
||||
import { displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { patchWorkspace } from "api/api";
|
||||
import { patchWorkspace, updateWorkspaceAutomaticUpdates } from "api/api";
|
||||
import { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm";
|
||||
import { useTemplatePoliciesEnabled } from "components/Dashboard/DashboardProvider";
|
||||
|
||||
const WorkspaceSettingsPage = () => {
|
||||
const params = useParams() as {
|
||||
@ -17,9 +18,18 @@ const WorkspaceSettingsPage = () => {
|
||||
const username = params.username.replace("@", "");
|
||||
const workspace = useWorkspaceSettings();
|
||||
const navigate = useNavigate();
|
||||
const templatePoliciesEnabled = useTemplatePoliciesEnabled();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (formValues: WorkspaceSettingsFormValues) =>
|
||||
patchWorkspace(workspace.id, { name: formValues.name }),
|
||||
mutationFn: async (formValues: WorkspaceSettingsFormValues) => {
|
||||
await Promise.all([
|
||||
patchWorkspace(workspace.id, { name: formValues.name }),
|
||||
updateWorkspaceAutomaticUpdates(
|
||||
workspace.id,
|
||||
formValues.automatic_updates,
|
||||
),
|
||||
]);
|
||||
},
|
||||
onSuccess: (_, formValues) => {
|
||||
displaySuccess("Workspace updated successfully");
|
||||
navigate(`/@${username}/${formValues.name}/settings`);
|
||||
@ -34,10 +44,10 @@ const WorkspaceSettingsPage = () => {
|
||||
|
||||
<WorkspaceSettingsPageView
|
||||
error={mutation.error}
|
||||
isSubmitting={mutation.isLoading}
|
||||
workspace={workspace}
|
||||
onCancel={() => navigate(`/@${username}/${workspaceName}`)}
|
||||
onSubmit={mutation.mutate}
|
||||
onSubmit={mutation.mutateAsync}
|
||||
templatePoliciesEnabled={templatePoliciesEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -7,7 +7,6 @@ const meta: Meta<typeof WorkspaceSettingsPageView> = {
|
||||
component: WorkspaceSettingsPageView,
|
||||
args: {
|
||||
error: undefined,
|
||||
isSubmitting: false,
|
||||
workspace: MockWorkspace,
|
||||
},
|
||||
};
|
||||
@ -15,6 +14,10 @@ const meta: Meta<typeof WorkspaceSettingsPageView> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkspaceSettingsPageView>;
|
||||
|
||||
const Example: Story = {};
|
||||
export const Example: Story = {};
|
||||
|
||||
export { Example as WorkspaceSettingsPageView };
|
||||
export const AutoUpdates: Story = {
|
||||
args: {
|
||||
templatePoliciesEnabled: true,
|
||||
},
|
||||
};
|
||||
|
@ -5,18 +5,18 @@ import { Workspace } from "api/typesGenerated";
|
||||
|
||||
export type WorkspaceSettingsPageViewProps = {
|
||||
error: unknown;
|
||||
isSubmitting: boolean;
|
||||
workspace: Workspace;
|
||||
onCancel: () => void;
|
||||
onSubmit: ComponentProps<typeof WorkspaceSettingsForm>["onSubmit"];
|
||||
templatePoliciesEnabled: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
error,
|
||||
workspace,
|
||||
templatePoliciesEnabled,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@ -30,10 +30,10 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
|
||||
|
||||
<WorkspaceSettingsForm
|
||||
error={error}
|
||||
isSubmitting={isSubmitting}
|
||||
workspace={workspace}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
templatePoliciesEnabled={templatePoliciesEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -303,3 +303,15 @@ export const getMatchingAgentOrFirst = (
|
||||
})
|
||||
.filter((a) => a)[0];
|
||||
};
|
||||
|
||||
export const workspaceUpdatePolicy = (
|
||||
workspace: TypesGen.Workspace,
|
||||
canChangeVersions: boolean,
|
||||
): TypesGen.AutomaticUpdates => {
|
||||
// If a template requires the active version and you cannot change versions
|
||||
// (restricted to template admins), then your policy must be "Always".
|
||||
if (workspace.template_require_active_version && !canChangeVersions) {
|
||||
return "always";
|
||||
}
|
||||
return workspace.automatic_updates;
|
||||
};
|
||||
|
Reference in New Issue
Block a user