feat: add frontend support for enabling automatic workspace updates (#10375)

This commit is contained in:
Jon Ayers
2023-10-31 17:06:36 -05:00
committed by GitHub
parent 3200b85d87
commit f4026edd71
11 changed files with 137 additions and 24 deletions

View File

@ -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,

View File

@ -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")
);
};

View File

@ -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,

View File

@ -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}

View File

@ -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 {

View File

@ -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>
</>
);

View File

@ -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>
);
};

View File

@ -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}
/>
</>
);

View File

@ -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,
},
};

View File

@ -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}
/>
</>
);

View File

@ -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;
};