mirror of https://github.com/coder/coder.git
This commit is contained in:
parent
57c9d88703
commit
1372bf82f5
|
@ -50,6 +50,18 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
|||
isTemplateSchedulingOptionsSet := failureTTL != 0 || inactivityTTL != 0 || maxTTL != 0
|
||||
|
||||
if isTemplateSchedulingOptionsSet || requireActiveVersion {
|
||||
if failureTTL != 0 || inactivityTTL != 0 {
|
||||
// This call can be removed when workspace_actions is no longer experimental
|
||||
experiments, exErr := client.Experiments(inv.Context())
|
||||
if exErr != nil {
|
||||
return xerrors.Errorf("get experiments: %w", exErr)
|
||||
}
|
||||
|
||||
if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
|
||||
return xerrors.Errorf("--failure-ttl and --inactivity-ttl are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.")
|
||||
}
|
||||
}
|
||||
|
||||
entitlements, err := client.Entitlements(inv.Context())
|
||||
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
|
||||
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
|
||||
|
@ -59,7 +71,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
|||
|
||||
if isTemplateSchedulingOptionsSet {
|
||||
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
|
||||
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl or --inactivityTTL")
|
||||
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivity-ttl, or --max-ttl")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,18 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
|||
),
|
||||
Short: "Edit the metadata of a template by name.",
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
// This clause can be removed when workspace_actions is no longer experimental
|
||||
if failureTTL != 0 || inactivityTTL != 0 {
|
||||
experiments, exErr := client.Experiments(inv.Context())
|
||||
if exErr != nil {
|
||||
return xerrors.Errorf("get experiments: %w", exErr)
|
||||
}
|
||||
|
||||
if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
|
||||
return xerrors.Errorf("--failure-ttl and --inactivity-ttl are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.")
|
||||
}
|
||||
}
|
||||
|
||||
unsetAutostopRequirementDaysOfWeek := len(autostopRequirementDaysOfWeek) == 1 && autostopRequirementDaysOfWeek[0] == "none"
|
||||
requiresScheduling := (len(autostopRequirementDaysOfWeek) > 0 && !unsetAutostopRequirementDaysOfWeek) ||
|
||||
autostopRequirementWeeks > 0 ||
|
||||
|
|
|
@ -8537,6 +8537,7 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"enum": [
|
||||
"moons",
|
||||
"workspace_actions",
|
||||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_autostop_requirement",
|
||||
|
@ -8546,6 +8547,7 @@ const docTemplate = `{
|
|||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
"ExperimentWorkspaceActions",
|
||||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateAutostopRequirement",
|
||||
|
|
|
@ -7649,6 +7649,7 @@
|
|||
"type": "string",
|
||||
"enum": [
|
||||
"moons",
|
||||
"workspace_actions",
|
||||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_autostop_requirement",
|
||||
|
@ -7658,6 +7659,7 @@
|
|||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
"ExperimentWorkspaceActions",
|
||||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateAutostopRequirement",
|
||||
|
|
|
@ -1973,6 +1973,9 @@ const (
|
|||
// feature is not yet complete in functionality.
|
||||
ExperimentMoons Experiment = "moons"
|
||||
|
||||
// https://github.com/coder/coder/milestone/19
|
||||
ExperimentWorkspaceActions Experiment = "workspace_actions"
|
||||
|
||||
// ExperimentTailnetPGCoordinator enables the PGCoord in favor of the pubsub-
|
||||
// only Coordinator
|
||||
ExperimentTailnetPGCoordinator Experiment = "tailnet_pg_coordinator"
|
||||
|
|
|
@ -2845,6 +2845,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| Value |
|
||||
| ------------------------------- |
|
||||
| `moons` |
|
||||
| `workspace_actions` |
|
||||
| `tailnet_pg_coordinator` |
|
||||
| `single_tailnet` |
|
||||
| `template_autostop_requirement` |
|
||||
|
|
|
@ -1708,7 +1708,8 @@ export type Experiment =
|
|||
| "single_tailnet"
|
||||
| "tailnet_pg_coordinator"
|
||||
| "template_autostop_requirement"
|
||||
| "template_update_policies";
|
||||
| "template_update_policies"
|
||||
| "workspace_actions";
|
||||
export const Experiments: Experiment[] = [
|
||||
"dashboard_theme",
|
||||
"deployment_health_page",
|
||||
|
@ -1717,6 +1718,7 @@ export const Experiments: Experiment[] = [
|
|||
"tailnet_pg_coordinator",
|
||||
"template_autostop_requirement",
|
||||
"template_update_policies",
|
||||
"workspace_actions",
|
||||
];
|
||||
|
||||
// From codersdk/deployment.go
|
||||
|
|
|
@ -112,3 +112,13 @@ export const useDashboard = (): DashboardProviderValue => {
|
|||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useIsWorkspaceActionsEnabled = (): boolean => {
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||
return allowWorkspaceActions && allowAdvancedScheduling;
|
||||
};
|
||||
|
|
|
@ -14,11 +14,20 @@ interface DormantDeletionStatProps {
|
|||
export const DormantDeletionStat: FC<DormantDeletionStatProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const { entitlements } = useDashboard();
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||
|
||||
if (!displayDormantDeletion(workspace, allowAdvancedScheduling)) {
|
||||
if (
|
||||
!displayDormantDeletion(
|
||||
workspace,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,20 @@ export const DormantDeletionText = ({
|
|||
}: {
|
||||
workspace: Workspace;
|
||||
}): JSX.Element | null => {
|
||||
const { entitlements } = useDashboard();
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||
|
||||
if (!displayDormantDeletion(workspace, allowAdvancedScheduling)) {
|
||||
if (
|
||||
!displayDormantDeletion(
|
||||
workspace,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return <StyledSpan role="status">Impending deletion</StyledSpan>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Workspace } from "api/typesGenerated";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Link from "@mui/material/Link";
|
||||
|
@ -21,9 +21,7 @@ export const DormantWorkspaceBanner = ({
|
|||
shouldRedisplayBanner: boolean;
|
||||
count?: Count;
|
||||
}): JSX.Element | null => {
|
||||
const { entitlements } = useDashboard();
|
||||
const schedulingEnabled =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
const experimentEnabled = useIsWorkspaceActionsEnabled();
|
||||
|
||||
if (!workspaces) {
|
||||
return null;
|
||||
|
@ -39,7 +37,7 @@ export const DormantWorkspaceBanner = ({
|
|||
|
||||
if (
|
||||
// Only show this if the experiment is included.
|
||||
!schedulingEnabled ||
|
||||
!experimentEnabled ||
|
||||
!hasDormantWorkspaces ||
|
||||
// Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion
|
||||
!shouldRedisplayBanner
|
||||
|
|
|
@ -4,39 +4,53 @@ import { displayDormantDeletion } from "./utils";
|
|||
|
||||
describe("displayDormantDeletion", () => {
|
||||
const today = new Date();
|
||||
it.each<[string, boolean, boolean]>([
|
||||
it.each<[string, boolean, boolean, boolean]>([
|
||||
[
|
||||
new Date(new Date().setDate(today.getDate() + 15)).toISOString(),
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
], // today + 15 days out
|
||||
[
|
||||
new Date(new Date().setDate(today.getDate() + 14)).toISOString(),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
], // today + 14
|
||||
[
|
||||
new Date(new Date().setDate(today.getDate() + 13)).toISOString(),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
], // today + 13
|
||||
[
|
||||
new Date(new Date().setDate(today.getDate() + 1)).toISOString(),
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
], // today + 1
|
||||
[new Date().toISOString(), true, true], // today + 0
|
||||
[new Date().toISOString(), false, false], // Advanced Scheduling off
|
||||
[new Date().toISOString(), true, true, true], // today + 0
|
||||
[new Date().toISOString(), false, true, false], // Advanced Scheduling off
|
||||
[new Date().toISOString(), true, false, false], // Workspace Actions off
|
||||
])(
|
||||
`deleting_at=%p, allowAdvancedScheduling=%p, shouldDisplay=%p`,
|
||||
(deleting_at, allowAdvancedScheduling, shouldDisplay) => {
|
||||
`deleting_at=%p, allowAdvancedScheduling=%p, AllowWorkspaceActions=%p, shouldDisplay=%p`,
|
||||
(
|
||||
deleting_at,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
shouldDisplay,
|
||||
) => {
|
||||
const workspace: TypesGen.Workspace = {
|
||||
...Mocks.MockWorkspace,
|
||||
deleting_at,
|
||||
};
|
||||
expect(displayDormantDeletion(workspace, allowAdvancedScheduling)).toBe(
|
||||
shouldDisplay,
|
||||
);
|
||||
expect(
|
||||
displayDormantDeletion(
|
||||
workspace,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
),
|
||||
).toBe(shouldDisplay);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -14,9 +14,14 @@ const IMPENDING_DELETION_DISPLAY_THRESHOLD = 14; // 14 days
|
|||
export const displayDormantDeletion = (
|
||||
workspace: Workspace,
|
||||
allowAdvancedScheduling: boolean,
|
||||
allowWorkspaceActions: boolean,
|
||||
) => {
|
||||
const today = new Date();
|
||||
if (!workspace.deleting_at || !allowAdvancedScheduling) {
|
||||
if (
|
||||
!workspace.deleting_at ||
|
||||
!allowAdvancedScheduling ||
|
||||
!allowWorkspaceActions
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -54,6 +54,7 @@ export interface TemplateScheduleForm {
|
|||
isSubmitting: boolean;
|
||||
error?: unknown;
|
||||
allowAdvancedScheduling: boolean;
|
||||
allowWorkspaceActions: boolean;
|
||||
allowAutostopRequirement: boolean;
|
||||
// Helpful to show field errors on Storybook
|
||||
initialTouched?: FormikTouched<UpdateTemplateMeta>;
|
||||
|
@ -65,6 +66,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
onCancel,
|
||||
error,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
isSubmitting,
|
||||
initialTouched,
|
||||
|
@ -491,7 +493,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
|||
</Stack>
|
||||
</Stack>
|
||||
</FormSection>
|
||||
{allowAdvancedScheduling && (
|
||||
{allowAdvancedScheduling && allowWorkspaceActions && (
|
||||
<>
|
||||
<FormSection
|
||||
title="Failure Cleanup"
|
||||
|
|
|
@ -133,6 +133,9 @@ describe("TemplateSchedulePage", () => {
|
|||
jest
|
||||
.spyOn(API, "getEntitlements")
|
||||
.mockResolvedValue(MockEntitlementsWithScheduling);
|
||||
|
||||
// remove when https://github.com/coder/coder/milestone/19 is completed.
|
||||
jest.spyOn(API, "getExperiments").mockResolvedValue(["workspace_actions"]);
|
||||
});
|
||||
|
||||
it("Calls the API when user fills in and submits a form", async () => {
|
||||
|
|
|
@ -18,11 +18,12 @@ const TemplateSchedulePage: FC = () => {
|
|||
const queryClient = useQueryClient();
|
||||
const orgId = useOrganizationId();
|
||||
const { template } = useTemplateSettings();
|
||||
const { entitlements } = useDashboard();
|
||||
const { entitlements, experiments } = useDashboard();
|
||||
const allowAdvancedScheduling =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||
// is merged up
|
||||
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||
const allowAutostopRequirement =
|
||||
entitlements.features["template_autostop_requirement"].enabled;
|
||||
const { clearLocal } = useLocalStorage();
|
||||
|
@ -53,6 +54,7 @@ const TemplateSchedulePage: FC = () => {
|
|||
</Helmet>
|
||||
<TemplateSchedulePageView
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
isSubmitting={isSubmitting}
|
||||
template={template}
|
||||
|
|
|
@ -31,6 +31,7 @@ type Story = StoryObj<typeof TemplateSchedulePageView>;
|
|||
|
||||
const defaultArgs = {
|
||||
allowAdvancedScheduling: true,
|
||||
allowWorkspaceActions: true,
|
||||
template: MockTemplate,
|
||||
onSubmit: action("onSubmit"),
|
||||
onCancel: action("cancel"),
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface TemplateSchedulePageViewProps {
|
|||
typeof TemplateScheduleForm
|
||||
>["initialTouched"];
|
||||
allowAdvancedScheduling: boolean;
|
||||
allowWorkspaceActions: boolean;
|
||||
allowAutostopRequirement: boolean;
|
||||
}
|
||||
|
||||
|
@ -22,6 +23,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
onSubmit,
|
||||
isSubmitting,
|
||||
allowAdvancedScheduling,
|
||||
allowWorkspaceActions,
|
||||
allowAutostopRequirement,
|
||||
submitError,
|
||||
initialTouched,
|
||||
|
@ -34,6 +36,7 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
|||
|
||||
<TemplateScheduleForm
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
allowWorkspaceActions={allowWorkspaceActions}
|
||||
allowAutostopRequirement={allowAutostopRequirement}
|
||||
initialTouched={initialTouched}
|
||||
isSubmitting={isSubmitting}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { usePagination } from "hooks/usePagination";
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import {
|
||||
useDashboard,
|
||||
useIsWorkspaceActionsEnabled,
|
||||
} from "components/Dashboard/DashboardProvider";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
|
@ -58,17 +61,14 @@ const WorkspacesPage: FC = () => {
|
|||
query: filterProps.filter.query,
|
||||
});
|
||||
|
||||
const { entitlements } = useDashboard();
|
||||
const schedulingEnabled =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
|
||||
const experimentEnabled = useIsWorkspaceActionsEnabled();
|
||||
// If workspace actions are enabled we need to fetch the dormant
|
||||
// workspaces as well. This lets us determine whether we should
|
||||
// show a banner to the user indicating that some of their workspaces
|
||||
// are at risk of being deleted.
|
||||
useEffect(() => {
|
||||
if (schedulingEnabled) {
|
||||
const includesDormant = filterProps.filter.query.includes("is-dormant");
|
||||
if (experimentEnabled) {
|
||||
const includesDormant = filterProps.filter.query.includes("dormant_at");
|
||||
const dormantQuery = includesDormant
|
||||
? filterProps.filter.query
|
||||
: filterProps.filter.query + " is-dormant:true";
|
||||
|
@ -89,11 +89,12 @@ const WorkspacesPage: FC = () => {
|
|||
// like dormant workspaces don't exist.
|
||||
setDormantWorkspaces([]);
|
||||
}
|
||||
}, [schedulingEnabled, data, filterProps.filter.query]);
|
||||
}, [experimentEnabled, data, filterProps.filter.query]);
|
||||
const updateWorkspace = useWorkspaceUpdate(queryKey);
|
||||
const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([]);
|
||||
const [isDeletingAll, setIsDeletingAll] = useState(false);
|
||||
const [urlSearchParams] = searchParamsResult;
|
||||
const { entitlements } = useDashboard();
|
||||
const canCheckWorkspaces =
|
||||
entitlements.features["workspace_batch_actions"].enabled;
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { FC } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
|
||||
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
|
||||
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
|
||||
import { Palette, PaletteColor } from "@mui/material/styles";
|
||||
import { TemplateFilterMenu, StatusFilterMenu } from "./menus";
|
||||
|
@ -76,10 +75,8 @@ export const WorkspacesFilter = ({
|
|||
error,
|
||||
menus,
|
||||
}: WorkspaceFilterProps) => {
|
||||
const { entitlements } = useDashboard();
|
||||
const actionsEnabled =
|
||||
entitlements.features["advanced_template_scheduling"].enabled;
|
||||
const presets = actionsEnabled ? PRESET_FILTERS : PRESETS_WITH_DORMANT;
|
||||
const actionsEnabled = useIsWorkspaceActionsEnabled();
|
||||
const presets = actionsEnabled ? PRESETS_WITH_DORMANT : PRESET_FILTERS;
|
||||
|
||||
return (
|
||||
<Filter
|
||||
|
|
Loading…
Reference in New Issue