Allowed clearing Windows OS update deadline (#42272)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33418 (this OS update change is unrelated
to the bigger Windows delete part of the story)

<img width="598" height="438" alt="image"
src="https://github.com/user-attachments/assets/7dca50c6-5ca4-4c54-b57f-c98dda5fb4d1"
/>

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Windows update deadline and grace period settings can now be cleared,
allowing removal of update enforcement policies.

* **Bug Fixes**
* Updated validation logic to properly handle empty deadline and grace
period fields.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky 2026-03-25 16:02:54 -05:00 committed by GitHub
parent 34e0ace5f6
commit c6538bd434
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 70 deletions

View file

@ -0,0 +1 @@
- Allowed clearing Windows OS update deadline and grace period fields to remove enforcement.

View file

@ -1061,23 +1061,34 @@ const TAGGED_TEMPLATES = {
);
},
editedWindowsUpdates: (activity: IActivity) => {
const deadlineDays = activity.details?.deadline_days;
const gracePeriodDays = activity.details?.grace_period_days;
const isCleared = deadlineDays === undefined || deadlineDays === null;
const teamText = activity.details?.team_name ? (
<>
the <b>{activity.details.team_name}</b> fleet
</>
) : (
`unassigned`
);
if (isCleared) {
return (
<>
{" "}
removed the Windows OS update options on hosts assigned to {teamText}.
</>
);
}
return (
<>
{" "}
updated the Windows OS update options (
<b>
Deadline: {activity.details?.deadline_days} days / Grace period:{" "}
{activity.details?.grace_period_days} days
Deadline: {deadlineDays} days / Grace period: {gracePeriodDays} days
</b>
) on hosts assigned to{" "}
{activity.details?.team_name ? (
<>
the <b>{activity.details.team_name}</b> fleet
</>
) : (
`unassigned`
)}
.
) on hosts assigned to {teamText}.
</>
);
},

View file

@ -12,7 +12,6 @@ import teamsAPI from "services/entities/teams";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import validatePresence from "components/forms/validators/validate_presence";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
const baseClass = "windows-target-form";
@ -45,14 +44,22 @@ const validateGracePeriodDays = (value: string) => {
const validateForm = (formData: IWindowsTargetFormData) => {
const errors: IWindowsTargetFormErrors = {};
const deadlineEmpty = formData.deadlineDays.trim() === "";
const graceEmpty = formData.gracePeriodDays.trim() === "";
if (!validatePresence(formData.deadlineDays)) {
errors.deadlineDays = "The deadline days is required.";
} else if (!validateDeadlineDays(formData.deadlineDays)) {
// Both empty is valid (clears enforcement)
if (deadlineEmpty && graceEmpty) {
return errors;
}
if (!deadlineEmpty && !validateDeadlineDays(formData.deadlineDays)) {
errors.deadlineDays = "Deadline must meet criteria below.";
}
if (!validatePresence(formData.gracePeriodDays)) {
if (deadlineEmpty && !graceEmpty) {
errors.gracePeriodDays =
"Grace period must be empty if no deadline is set.";
} else if (!deadlineEmpty && graceEmpty) {
errors.gracePeriodDays = "The grace period days is required.";
} else if (!validateGracePeriodDays(formData.gracePeriodDays)) {
errors.gracePeriodDays = "Grace period must meet criteria below.";
@ -64,8 +71,8 @@ const validateForm = (formData: IWindowsTargetFormData) => {
interface IWindowsMdmConfigData {
mdm: {
windows_updates: {
deadline_days: number;
grace_period_days: number;
deadline_days: number | null;
grace_period_days: number | null;
};
};
}
@ -77,8 +84,10 @@ const createMdmConfigData = (
return {
mdm: {
windows_updates: {
deadline_days: parseInt(deadlineDays, 10),
grace_period_days: parseInt(gracePeriodDays, 10),
deadline_days:
deadlineDays.trim() === "" ? null : parseInt(deadlineDays, 10),
grace_period_days:
gracePeriodDays.trim() === "" ? null : parseInt(gracePeriodDays, 10),
},
},
};
@ -104,59 +113,61 @@ const WindowsTargetForm = ({
.gitops_mode_enabled;
const [isSaving, setIsSaving] = useState(false);
const [deadlineDays, setDeadlineDays] = useState(
defaultDeadlineDays.toString()
);
const [gracePeriodDays, setGracePeriodDays] = useState(
defaultGracePeriodDays.toString()
);
const [deadlineDaysError, setDeadlineDaysError] = useState<
string | undefined
>();
const [gracePeriodDaysError, setGracePeriodDaysError] = useState<
string | undefined
>();
const [formData, setFormData] = useState<IWindowsTargetFormData>({
deadlineDays: defaultDeadlineDays.toString(),
gracePeriodDays: defaultGracePeriodDays.toString(),
});
const [formErrors, setFormErrors] = useState<IWindowsTargetFormErrors>({});
// FIXME: This behaves unexpectedly when a user switches tabs or changes the teams dropdown while the form is
// submitting because this component is unmounted.
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errors = validateForm({
deadlineDays,
gracePeriodDays,
});
const errors = validateForm(formData);
if (!isEmpty(errors)) {
setFormErrors(errors);
return;
}
setDeadlineDaysError(errors.deadlineDays);
setGracePeriodDaysError(errors.gracePeriodDays);
if (isEmpty(errors)) {
setIsSaving(true);
const updateData = createMdmConfigData(deadlineDays, gracePeriodDays);
try {
currentTeamId === APP_CONTEXT_NO_TEAM_ID
? await configAPI.update(updateData)
: await teamsAPI.update(updateData, currentTeamId);
renderFlash(
"success",
"Successfully updated Windows OS update options."
);
} catch {
renderFlash("error", "Couldnt update. Please try again.");
} finally {
currentTeamId === APP_CONTEXT_NO_TEAM_ID
? refetchAppConfig()
: refetchTeamConfig();
setIsSaving(false);
}
setIsSaving(true);
const updateData = createMdmConfigData(
formData.deadlineDays,
formData.gracePeriodDays
);
try {
currentTeamId === APP_CONTEXT_NO_TEAM_ID
? await configAPI.update(updateData)
: await teamsAPI.update(updateData, currentTeamId);
renderFlash("success", "Successfully updated Windows OS update options.");
} catch {
renderFlash("error", "Couldnt update. Please try again.");
} finally {
currentTeamId === APP_CONTEXT_NO_TEAM_ID
? refetchAppConfig()
: refetchTeamConfig();
setIsSaving(false);
}
};
const handleDeadlineDaysChange = (val: string) => {
setDeadlineDays(val);
const handleChange = (field: keyof IWindowsTargetFormData) => (
val: string
) => {
const newFormData = { ...formData, [field]: val };
setFormData(newFormData);
// On change, only update/clear existing errors (optimistic UX)
const newErrors = validateForm(newFormData);
const updatedErrors: IWindowsTargetFormErrors = {};
Object.keys(formErrors).forEach((key) => {
const k = key as keyof IWindowsTargetFormErrors;
if (newErrors[k]) {
updatedErrors[k] = newErrors[k];
}
});
setFormErrors(updatedErrors);
};
const handleGracePeriodDays = (val: string) => {
setGracePeriodDays(val);
const handleBlur = () => {
setFormErrors(validateForm(formData));
};
return (
@ -166,18 +177,20 @@ const WindowsTargetForm = ({
label="Deadline"
tooltip="Number of days the end user has before updates are installed and the host is forced to restart."
helpText="Number of days from 0 to 30."
value={deadlineDays}
error={deadlineDaysError}
onChange={handleDeadlineDaysChange}
value={formData.deadlineDays}
error={formErrors.deadlineDays}
onChange={handleChange("deadlineDays")}
onBlur={handleBlur}
/>
<InputField
disabled={gitOpsModeEnabled}
label="Grace period"
tooltip="Number of days after the deadline the end user has before the host is forced to restart (only if end user was offline when deadline passed)."
helpText="Number of days from 0 to 7."
value={gracePeriodDays}
error={gracePeriodDaysError}
onChange={handleGracePeriodDays}
value={formData.gracePeriodDays}
error={formErrors.gracePeriodDays}
onChange={handleChange("gracePeriodDays")}
onBlur={handleBlur}
/>{" "}
<div className="button-wrap">
<GitOpsModeTooltipWrapper

View file

@ -54,8 +54,8 @@ export interface IUpdateTeamFormData {
deadline: string;
};
windows_updates?: {
deadline_days: number;
grace_period_days: number;
deadline_days: number | null;
grace_period_days: number | null;
};
};
host_expiry_settings: {