fleet/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
Ian Littman 2891904f31
🤖 Switch InputField + InputFieldWithIcon JSX components to TS, add more test coverage, fix Storybook build (#43307)
Zed + Opus 4.6; prompt: Convert the InputField JSX component to
TypeScript and remove the ts-ignore directives that we no longer need
after doing so.

- [x] Changes file added
- [x] Automated tests updated
2026-04-09 08:41:48 -05:00

556 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useMemo } from "react";
import { IInputFieldParseTarget } from "interfaces/form_field";
import validUrl from "components/forms/validators/valid_url";
import SettingsSection from "pages/admin/components/SettingsSection";
import PageDescription from "components/PageDescription";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import InputField from "components/forms/fields/InputField";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS } from "utilities/constants";
import { getCustomDropdownOptions } from "utilities/helpers";
import { isPremiumTier } from "utilities/permissions/permissions";
import { IAppConfigFormProps } from "../constants";
const baseClass = "app-config-form";
interface IAdvancedConfigFormData {
ssoUserURL: string;
mdmAppleServerURL: string;
domain: string;
verifySSLCerts: boolean;
enableStartTLS?: boolean;
enableHostExpiry: boolean;
hostExpiryWindow: string;
deleteActivities: boolean;
activityExpiryWindow: number;
disableLiveQuery: boolean;
disableScripts: boolean;
disableAIFeatures: boolean;
disableQueryReports: boolean;
requireHardwareAttestation: boolean;
}
interface IAdvancedConfigFormErrors {
ssoUserURL?: string | null;
mdmAppleServerURL?: string | null;
domain?: string | null;
hostExpiryWindow?: string | null;
}
const validateFormData = ({
ssoUserURL,
mdmAppleServerURL,
domain,
hostExpiryWindow,
enableHostExpiry,
}: IAdvancedConfigFormData) => {
const errors: Record<string, string> = {};
if (!ssoUserURL) {
delete errors.ssoUserURL;
} else if (!validUrl({ url: ssoUserURL })) {
errors.ssoUserURL = "SSO user URL is not a valid URL";
}
if (!mdmAppleServerURL) {
delete errors.mdmAppleServerURL;
} else if (!validUrl({ url: mdmAppleServerURL })) {
errors.mdmAppleServerURL = "Apple MDM server URL is not a valid URL";
}
if (!domain) {
delete errors.domain;
} else if (!validUrl({ url: domain })) {
errors.domain = "Domain is not a valid URL";
}
if (
enableHostExpiry &&
(!hostExpiryWindow || parseInt(hostExpiryWindow, 10) <= 0)
) {
errors.hostExpiryWindow = "Host expiry window must be a positive number";
}
return errors;
};
const Advanced = ({
appConfig,
handleSubmit,
isUpdatingSettings,
}: IAppConfigFormProps): JSX.Element => {
const [formData, setFormData] = useState<IAdvancedConfigFormData>({
ssoUserURL: appConfig.sso_settings?.sso_server_url || "",
mdmAppleServerURL: appConfig.mdm?.apple_server_url || "",
domain: appConfig.smtp_settings?.domain || "",
verifySSLCerts: appConfig.smtp_settings?.verify_ssl_certs || false,
enableStartTLS: appConfig.smtp_settings?.enable_start_tls,
enableHostExpiry:
appConfig.host_expiry_settings.host_expiry_enabled || false,
hostExpiryWindow:
(appConfig.host_expiry_settings.host_expiry_window &&
appConfig.host_expiry_settings.host_expiry_window.toString()) ||
"0",
deleteActivities:
appConfig.activity_expiry_settings?.activity_expiry_enabled || false,
activityExpiryWindow:
appConfig.activity_expiry_settings?.activity_expiry_window || 30,
disableLiveQuery: appConfig.server_settings.live_query_disabled || false,
disableScripts: appConfig.server_settings.scripts_disabled || false,
disableAIFeatures: appConfig.server_settings.ai_features_disabled || false,
disableQueryReports:
appConfig.server_settings.query_reports_disabled || false,
requireHardwareAttestation:
appConfig.mdm?.apple_require_hardware_attestation || false,
});
const {
ssoUserURL,
mdmAppleServerURL,
domain,
verifySSLCerts,
enableStartTLS,
enableHostExpiry,
hostExpiryWindow,
deleteActivities,
activityExpiryWindow,
disableLiveQuery,
disableScripts,
disableAIFeatures,
disableQueryReports,
requireHardwareAttestation,
} = formData;
const [formErrors, setFormErrors] = useState<IAdvancedConfigFormErrors>({});
const activityExpiryWindowOptions = useMemo(
() =>
getCustomDropdownOptions(
ACTIVITY_EXPIRY_WINDOW_DROPDOWN_OPTIONS,
activityExpiryWindow,
// it's safe to assume that frequency is a number
(frequency: number | string) => `${frequency as number} days`
),
// intentionally leave activityExpiryWindow out of the dependencies, so that the custom
// options are maintained even if the user changes the frequency in the UI
[deleteActivities]
);
const onInputChange = ({ name, value }: IInputFieldParseTarget) => {
const newFormData = { ...formData, [name]: value };
setFormData(newFormData);
const newErrs = validateFormData(newFormData);
// only set errors that are updates of existing errors
// new errors are only set onBlur
const errsToSet: Record<string, string> = {};
Object.keys(formErrors).forEach((k) => {
if (newErrs[k]) {
errsToSet[k] = newErrs[k];
}
});
setFormErrors(errsToSet);
};
const onInputBlur = () => {
setFormErrors(validateFormData(formData));
};
const onFormSubmit = (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
const errs = validateFormData(formData);
if (Object.keys(errs).length > 0) {
setFormErrors(errs);
return;
}
// Formatting of API not UI
const formDataToSubmit = {
server_settings: {
live_reporting_disabled: disableLiveQuery,
discard_reports_data: disableQueryReports,
scripts_disabled: disableScripts,
deferred_save_host: appConfig.server_settings.deferred_save_host,
ai_features_disabled: disableAIFeatures,
},
smtp_settings: {
domain,
verify_ssl_certs: verifySSLCerts,
enable_start_tls: enableStartTLS || false,
},
host_expiry_settings: {
host_expiry_enabled: enableHostExpiry,
host_expiry_window: parseInt(hostExpiryWindow, 10) || undefined,
},
activity_expiry_settings: {
activity_expiry_enabled: deleteActivities,
activity_expiry_window: activityExpiryWindow || undefined,
},
mdm: {
apple_server_url: mdmAppleServerURL,
apple_require_hardware_attestation: requireHardwareAttestation,
},
sso_settings: {
sso_server_url: ssoUserURL,
},
};
handleSubmit(formDataToSubmit);
};
return (
<SettingsSection title="Advanced options" className={baseClass}>
<PageDescription
variant="right-panel"
content="Most users do not need to modify these options."
/>
<form onSubmit={onFormSubmit} autoComplete="off">
<div className="form">
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<InputField
disabled={disableChildren}
label="SSO user URL"
onChange={onInputChange}
onBlur={onInputBlur}
name="ssoUserURL"
value={ssoUserURL}
parseTarget
error={formErrors.ssoUserURL}
tooltip={
!disableChildren &&
"Update this URL if you want your Fleet users (admins, maintainers, observers) to login via SSO using a URL that's different than the base URL of your Fleet instance. If not configured, login via SSO will use the base URL of the Fleet instance."
}
/>
)}
/>
{appConfig.mdm.enabled_and_configured && (
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<InputField
disabled={disableChildren}
label="Apple MDM server URL"
onChange={onInputChange}
onBlur={onInputBlur}
name="mdmAppleServerURL"
value={mdmAppleServerURL}
parseTarget
error={formErrors.mdmAppleServerURL}
tooltip={
!disableChildren &&
"Update this URL if you're self-hosting Fleet and you want your hosts to talk to this URL for MDM features. If not configured, hosts will use the base URL of the Fleet instance."
}
helpText="If this URL changes and hosts already have MDM turned on, the end users will have to turn MDM off and back on to use MDM features."
/>
)}
/>
)}
<InputField
label="Domain"
onChange={onInputChange}
onBlur={onInputBlur}
name="domain"
value={domain}
parseTarget
error={formErrors.domain}
tooltip={
<>
If you need to specify a HELO domain, <br />
you can do it here{" "}
<em>
(Default: <strong>Blank</strong>)
</em>
</>
}
/>
<Checkbox
onChange={onInputChange}
name="verifySSLCerts"
value={verifySSLCerts}
parseTarget
labelTooltipContent={
<>
Turn this off (not recommended) <br />
if you use a self-signed certificate{" "}
<em>
<br />
(Default: <strong>On</strong>)
</em>
</>
}
>
Verify SSL certs
</Checkbox>
<Checkbox
onChange={onInputChange}
name="enableStartTLS"
value={enableStartTLS}
parseTarget
labelTooltipContent={
<>
Detects if STARTTLS is enabled <br />
in your SMTP server and starts <br />
to use it.{" "}
<em>
(Default: <strong>On</strong>)
</em>
</>
}
>
Enable STARTTLS
</Checkbox>
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="enableHostExpiry"
value={enableHostExpiry}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
When enabled, allows automatic cleanup of
<br />
hosts that have not communicated with Fleet in
<br />
the number of days specified in the{" "}
<strong>
Host expiry
<br />
window
</strong>{" "}
setting.{" "}
<em>
(Default: <strong>Off</strong>)
</em>
</>
)
}
>
Host expiry
</Checkbox>
)}
/>
{enableHostExpiry && (
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<InputField
disabled={disableChildren}
label="Host expiry window"
type="number"
onChange={onInputChange}
name="hostExpiryWindow"
value={hostExpiryWindow}
parseTarget
error={formErrors.hostExpiryWindow}
/>
)}
/>
)}
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="deleteActivities"
value={deleteActivities}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
When enabled, allows automatic cleanup of audit logs older
than the number of days specified in the{" "}
<em>Audit log retention window</em> setting.
<em>
(Default: <strong>Off</strong>)
</em>
</>
)
}
>
Delete activities
</Checkbox>
)}
/>
{deleteActivities && (
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Dropdown
disabled={disableChildren}
searchable={false}
options={activityExpiryWindowOptions}
onChange={onInputChange}
placeholder="Select"
value={activityExpiryWindow}
label="Max activity age"
name="activityExpiryWindow"
parseTarget
/>
)}
/>
)}
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="disableLiveQuery"
value={disableLiveQuery}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
When enabled, disables the ability to run live reports{" "}
<br />
(ad hoc reports executed via the UI or fleetctl).{" "}
<em>
(Default: <strong>Off</strong>)
</em>
</>
)
}
>
Disable live reports
</Checkbox>
)}
/>
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="disableQueryReports"
value={disableQueryReports}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
<>
Disabling stored results will decrease database usage,{" "}
<br />
but will prevent you from accessing report results in
<br />
Fleet and will delete existing results. This can also be{" "}
<br />
disabled on a per-report basis by enabling &quot;Discard{" "}
<br />
data&quot;.{" "}
<em>
(Default: <b>Off</b>)
</em>
</>
</>
)
}
helpText="Enabling this setting will delete all existing report results in Fleet."
>
Disable stored results
</Checkbox>
)}
/>
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="disableScripts"
value={disableScripts}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
Disabling script execution will block access to run
scripts.
<br />
Scripts may still be added and removed in the UI and API.
<br />
<em>
(Default: <b>Off</b>)
</em>
</>
)
}
helpText="Features that run scripts under-the-hood (e.g. software install, lock/wipe, script-only packages) will still be available."
>
Disable script execution features
</Checkbox>
)}
/>
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="disableAIFeatures"
value={disableAIFeatures}
parseTarget
labelTooltipContent={
!disableChildren && (
<>
When enabled, disables AI features such as pre-filling
forms
<br />
with descriptions generated by a large language model
<br />
(LLM).{" "}
<em>
(Default: <strong>Off</strong>)
</em>
</>
)
}
helpText="If enabled, only policy queries (SQL) are sent to the LLM. Fleet doesnt use this data to train models."
>
Disable generative AI features
</Checkbox>
)}
/>
{isPremiumTier(appConfig) && (
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<Checkbox
disabled={disableChildren}
onChange={onInputChange}
name="requireHardwareAttestation"
value={requireHardwareAttestation}
parseTarget
helpText="Enabling this setting will require macOS hosts with Apple Silicon that automatically enroll (DEP) to use ACME with Managed Device Attestation"
>
Require hardware attestation
</Checkbox>
)}
/>
)}
</div>
<Button
type="submit"
disabled={Object.keys(formErrors).length > 0}
className="save-loading button-wrap"
isLoading={isUpdatingSettings}
>
Save
</Button>
</form>
</SettingsSection>
);
};
export default Advanced;