diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 13d9ff4e96c..47aa4cad513 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -61,6 +61,8 @@ import PageDescription from "components/PageDescription"; import LastUpdatedText from "components/LastUpdatedText"; import TooltipWrapper from "components/TooltipWrapper"; +import { getTicketOrWebhookInfo } from "pages/policies/components/PolicyAutomationsFields"; + import PoliciesTable from "./components/PoliciesTable"; import DeletePoliciesModal from "./components/DeletePoliciesModal"; import { DEFAULT_POLICY } from "../constants"; @@ -100,23 +102,6 @@ const AUTOMATION_TYPES: AutomationType[] = [ const GLOBAL_AUTOMATION_TYPES: GlobalPoliciesAutomationType[] = ["other"]; -// NOTE: backend uses webhook_settings to store automated policy ids for both -// webhooks and integrations. -const getWebhookOrTicketPolicyIds = ( - config: IConfig | ITeamConfig | undefined -): number[] => { - if (!config) return []; - const webhook = config.webhook_settings?.failing_policies_webhook; - const { jira, zendesk } = config.integrations ?? {}; - const isIntegrationEnabled = - !!jira?.some((j) => j.enable_failing_policies) || - !!zendesk?.some((z) => z.enable_failing_policies); - if (isIntegrationEnabled || webhook?.enable_failing_policies_webhook) { - return webhook?.policy_ids || []; - } - return []; -}; - const baseClass = "manage-policies-page"; const ManagePolicyPage = ({ @@ -630,42 +615,26 @@ const ManagePolicyPage = ({ const hasPoliciesToDelete = hasPoliciesToAutomate || (isPrimoMode && (teamPolicies?.length ?? 0) > 0); // in Primo mode, allow deleting inherited policies, which will be included in teamPolicies, from this view - // NOTE: backend uses webhook_settings to store automated policy ids for both webhooks and integrations - const getAutomationInfoFromConfig = ( - cfg: IConfig | ITeamConfig | undefined - ): { policyIds: number[]; type: OtherAutomationType | undefined } => { - if (!cfg) return { policyIds: [], type: undefined }; - const { - webhook_settings: { failing_policies_webhook: webhook } = {}, - integrations, - } = cfg; - const isIntegrationEnabled = - !!integrations?.jira?.find((j) => j.enable_failing_policies) || - !!integrations?.zendesk?.find((z) => z.enable_failing_policies); - const isWebhookEnabled = !!webhook?.enable_failing_policies_webhook; - const policyIds = - isIntegrationEnabled || isWebhookEnabled ? webhook?.policy_ids ?? [] : []; - let type: OtherAutomationType | undefined; - if (isIntegrationEnabled) type = "ticket"; - else if (isWebhookEnabled) type = "webhook"; - return { policyIds, type }; - }; - const fleetAutomationInfo = getAutomationInfoFromConfig(automationsConfig); + const fleetAutomationInfo = getTicketOrWebhookInfo(automationsConfig); // Inherited (global) policies are listed in team views, but their webhook - // membership lives on the *global* config — not the team's. - // Union both so an inherited policy with a global-config webhook/ticket - // still shows the correct data. + // membership lives on the *global* config — not the team's. Union both + // so an inherited policy with a global-config webhook/ticket still shows + // the correct data. const inheritedAutomationInfo = !isAllTeamsSelected - ? getAutomationInfoFromConfig(globalConfig) - : { policyIds: [], type: undefined as OtherAutomationType | undefined }; + ? getTicketOrWebhookInfo(globalConfig) + : { state: "disabled" as const, policyIds: [] }; const currentAutomatedPolicies: number[] = Array.from( new Set([ ...fleetAutomationInfo.policyIds, ...inheritedAutomationInfo.policyIds, ]) ); + const ticketOrWebhookState = + fleetAutomationInfo.state !== "disabled" + ? fleetAutomationInfo.state + : inheritedAutomationInfo.state; const otherAutomationType: OtherAutomationType | undefined = - fleetAutomationInfo.type ?? inheritedAutomationInfo.type; + ticketOrWebhookState === "disabled" ? undefined : ticketOrWebhookState; const renderPoliciesCountAndLastUpdated = ( count?: number, @@ -1011,9 +980,6 @@ const ManagePolicyPage = ({ teamIdForApi={teamIdForApi} automationsConfig={modalAutomationsConfig} globalConfig={globalConfig} - webhookOrTicketPolicyIds={getWebhookOrTicketPolicyIds( - modalAutomationsConfig - )} refetchPolicies={() => refetchPolicies(teamIdForApi)} onExit={onCloseManageAutomationsModal} /> diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index b1de6b0989c..e59dff3bdce 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -1,31 +1,18 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ - -import React, { useContext, useMemo, useState } from "react"; -import { SingleValue } from "react-select-5"; +import React, { useContext, useRef } from "react"; import { NotificationContext } from "context/notification"; import { IPolicyStats } from "interfaces/policy"; import { IConfig } from "interfaces/config"; -import { ITeamConfig, API_NO_TEAM_ID } from "interfaces/team"; +import { ITeamConfig } from "interfaces/team"; import { PLATFORM_DISPLAY_NAMES, QueryablePlatform } from "interfaces/platform"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; -import Checkbox from "components/forms/fields/Checkbox"; -import CustomLink from "components/CustomLink"; -import DropdownWrapper, { - CustomOptionType, -} from "components/forms/fields/DropdownWrapper/DropdownWrapper"; import Icon from "components/Icon"; -import TooltipWrapper from "components/TooltipWrapper"; - -import { getTicketOrWebhookLabel, getTicketOrWebhookState } from "./helpers"; -import { IAutomationRow } from "./types"; -import { - useScripts, - useSoftwareTitles, +import PolicyAutomationsFields, { + IPolicyAutomationsFieldsHandle, useUpdatePolicyAutomations, -} from "./hooks"; +} from "pages/policies/components/PolicyAutomationsFields"; const baseClass = "manage-automations-modal"; @@ -47,9 +34,6 @@ interface IManageAutomationsModalProps { teamIdForApi: number | undefined; automationsConfig: IConfig | ITeamConfig | undefined; globalConfig: IConfig | undefined; - /** Policy IDs that have webhook/ticket (a.k.a. "other workflow") - * automations configured on this fleet. */ - webhookOrTicketPolicyIds: number[]; refetchPolicies: () => void; onExit: () => void; } @@ -61,193 +45,12 @@ const ManageAutomationsModal = ({ teamIdForApi, automationsConfig, globalConfig, - webhookOrTicketPolicyIds, refetchPolicies, onExit, }: IManageAutomationsModalProps): JSX.Element => { const { renderFlash } = useContext(NotificationContext); - const ticketOrWebhookState = getTicketOrWebhookState(automationsConfig); - - const isCalendarEnabledForTeam = !isGlobalPolicy - ? (automationsConfig as ITeamConfig | undefined)?.integrations - ?.google_calendar?.enable_calendar_events ?? false - : false; - - const getIsConditionalAccessEnabledForTeam = () => { - if (isGlobalPolicy) return false; - if (teamIdForApi === API_NO_TEAM_ID) { - return globalConfig?.integrations?.conditional_access_enabled ?? false; - } - return ( - (automationsConfig as ITeamConfig | undefined)?.integrations - ?.conditional_access_enabled ?? false - ); - }; - const isConditionalAccessEnabledForTeam = getIsConditionalAccessEnabledForTeam(); - - const initialWebhookOrTicket = webhookOrTicketPolicyIds.includes(policy.id); - const initialInstallSoftware = !!policy.install_software; - const initialRunScript = !!policy.run_script; - const initialCalendar = policy.calendar_events_enabled; - const initialConditionalAccess = policy.conditional_access_enabled; - const initialContinuous = policy.continuous_automations_enabled ?? false; - - const [webhookOrTicketEnabled, setWebhookOrTicketEnabled] = useState( - initialWebhookOrTicket - ); - const [installSoftware, setInstallSoftware] = useState( - initialInstallSoftware - ); - const [runScript, setRunScript] = useState(initialRunScript); - const [calendarEvent, setCalendarEvent] = useState(initialCalendar); - const [conditionalAccess, setConditionalAccess] = useState( - initialConditionalAccess - ); - const [continuousEnabled, setContinuousEnabled] = useState(initialContinuous); - - const [softwareTitleId, setSoftwareTitleId] = useState( - policy.install_software?.software_title_id ?? null - ); - const [scriptId, setScriptId] = useState( - policy.run_script?.id ?? null - ); - - const canFetchTeamScopedLists = !isGlobalPolicy && teamIdForApi !== undefined; - const { data: softwareTitlesData } = useSoftwareTitles({ - fleetId: teamIdForApi ?? 0, - enabled: canFetchTeamScopedLists && installSoftware, - }); - const { data: scriptsData } = useScripts({ - fleetId: teamIdForApi ?? 0, - enabled: canFetchTeamScopedLists && runScript, - }); - - const softwareOptions: CustomOptionType[] = useMemo( - () => - (softwareTitlesData?.software_titles ?? []).map((t) => ({ - label: t.name, - value: String(t.id), - })), - [softwareTitlesData] - ); - - const scriptOptions: CustomOptionType[] = useMemo( - () => - (scriptsData?.scripts ?? []).map((s) => ({ - label: s.name, - value: String(s.id), - })), - [scriptsData] - ); - - const policyPlatforms = (policy.platform ?? "") - .split(",") - .map((p) => p.trim()) - .filter((p): p is QueryablePlatform => - (PLATFORM_DISPLAY_ORDER as string[]).includes(p) - ); - const displayedPlatforms = PLATFORM_DISPLAY_ORDER.filter((p) => - policyPlatforms.includes(p) - ); - - const isTicketWebhookEnabled = ticketOrWebhookState !== "disabled"; - const rows: IAutomationRow[] = [ - { - key: "ticket_webhook", - label: getTicketOrWebhookLabel(ticketOrWebhookState), - checked: webhookOrTicketEnabled && isTicketWebhookEnabled, - onToggle: setWebhookOrTicketEnabled, - isDisabled: !isTicketWebhookEnabled, - }, - ]; - if (!isGlobalPolicy) { - rows.push( - { - key: "install_software", - label: "Install software", - tooltip: ( - - ), - checked: installSoftware, - onToggle: setInstallSoftware, - isDisabled: false, - picker: installSoftware ? ( - o.value === String(softwareTitleId ?? "") - ) ?? null - } - options={softwareOptions} - placeholder="Select software" - onChange={(opt: SingleValue) => - setSoftwareTitleId(opt ? Number(opt.value) : null) - } - /> - ) : undefined, - }, - { - key: "run_script", - label: "Run script", - tooltip: ( - - ), - checked: runScript, - onToggle: setRunScript, - isDisabled: false, - picker: runScript ? ( - o.value === String(scriptId ?? "")) ?? - null - } - options={scriptOptions} - placeholder="Select script" - onChange={(opt: SingleValue) => - setScriptId(opt ? Number(opt.value) : null) - } - /> - ) : undefined, - }, - { - key: "calendar_event", - label: "Calendar event", - tooltip: ( - - ), - checked: calendarEvent && isCalendarEnabledForTeam, - onToggle: setCalendarEvent, - isDisabled: !isCalendarEnabledForTeam, - }, - { - key: "conditional_access", - label: "Conditional access", - tooltip: ( - - ), - checked: conditionalAccess && isConditionalAccessEnabledForTeam, - onToggle: setConditionalAccess, - isDisabled: !isConditionalAccessEnabledForTeam, - } - ); - } + const automationsRef = useRef(null); const { mutate: save, isLoading: isSaving } = useUpdatePolicyAutomations({ policy, @@ -262,59 +65,36 @@ const ManageAutomationsModal = ({ onError: () => renderFlash("error", ERR_MSG), }); - const onSave = () => { - // Block enabling install/run without a selection — saving would silently - // unset the automation. - if (installSoftware && softwareTitleId === null) { - renderFlash("error", "Please select software to install."); + const handleSubmit = (evt: React.FormEvent) => { + evt.preventDefault(); + const payload = automationsRef.current?.getAutomationsPayload(); + if (!payload) { return; } - if (runScript && scriptId === null) { - renderFlash("error", "Please select a script to run."); + if (payload.error) { + renderFlash("error", payload.error); return; } - - const perPolicyDirty = - !isGlobalPolicy && - (installSoftware !== initialInstallSoftware || - softwareTitleId !== - (policy.install_software?.software_title_id ?? null) || - runScript !== initialRunScript || - scriptId !== (policy.run_script?.id ?? null) || - calendarEvent !== initialCalendar || - conditionalAccess !== initialConditionalAccess || - continuousEnabled !== initialContinuous); - const webhookDirty = webhookOrTicketEnabled !== initialWebhookOrTicket; - - if (!perPolicyDirty && !webhookDirty) { + if (!payload.isDirty) { onExit(); return; } - save({ - policyUpdate: perPolicyDirty - ? { - software_title_id: installSoftware ? softwareTitleId : null, - script_id: runScript ? scriptId : null, - // When the feature disabled, the row is locked and the - // user can't toggle it — so we omit the field instead of carrying - // the stale state through to the PATCH. That preserves the policy's - // stored intent for if/when the fleet admin re-enables the feature. - ...(isCalendarEnabledForTeam && { - calendar_events_enabled: calendarEvent, - }), - ...(isConditionalAccessEnabledForTeam && { - conditional_access_enabled: conditionalAccess, - }), - continuous_automations_enabled: continuousEnabled, - } - : undefined, - webhookOrTicketUpdate: webhookDirty - ? { enabled: webhookOrTicketEnabled } - : undefined, + policyUpdate: payload.policyUpdate, + webhookOrTicketUpdate: payload.webhookOrTicketUpdate, }); }; + const policyPlatforms = (policy.platform ?? "") + .split(",") + .map((p) => p.trim()) + .filter((p): p is QueryablePlatform => + (PLATFORM_DISPLAY_ORDER as string[]).includes(p) + ); + const displayedPlatforms = PLATFORM_DISPLAY_ORDER.filter((p) => + policyPlatforms.includes(p) + ); + return ( -
-
- Manage automations for the {policy.name} policy on{" "} - {fleetName}. -
- - {displayedPlatforms.length > 0 && ( -
-

Platforms

-
- {displayedPlatforms.map((p) => ( - - - {PLATFORM_DISPLAY_NAMES[p]} - - ))} -
-
- )} - -
-

Automations

- - - {rows.map((row) => ( - - - - - ))} - -
- - {row.tooltip ? ( - - {row.label} - - ) : ( - row.label - )} - - - {row.isDisabled ? ( - - Not enabled for {fleetName} - - ) : ( - row.picker - )} -
-
- {" "} - about automation types and their supported platforms. +
+
+
+ Manage automations for the {policy.name} policy on{" "} + {fleetName}.
-
- {!isGlobalPolicy && ( + {displayedPlatforms.length > 0 && ( +
+

Platforms

+
+ {displayedPlatforms.map((p) => ( + + + {PLATFORM_DISPLAY_NAMES[p]} + + ))} +
+
+ )} +
- - - Continuous - {" "} - software & script automations - +

Automations

+
- )} -
+ -
- - -
+
+ + +
+
); }; -interface IAutomationRowTooltipProps { - text: string; - learnMoreUrl: string; -} - -function AutomationRowTooltip({ - text, - learnMoreUrl, -}: IAutomationRowTooltipProps): JSX.Element { - return ( - <> - {text}{" "} - - - ); -} - export default ManageAutomationsModal; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss index 724277f90b0..5af5644b151 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss @@ -31,48 +31,4 @@ align-items: center; gap: $pad-xsmall; } - - &__automations-table { - border-collapse: separate; - border-spacing: 0; - border: 1px solid $ui-fleet-black-10; - border-radius: 8px; - width: 100%; - font-size: $x-small; - } - - &__row td { - height: 40px; - padding: $pad-xsmall $pad-large; - border-top: 1px solid $ui-fleet-black-10; - vertical-align: middle; - } - - &__row:first-child td { - border-top: none; - } - - &__row--disabled &__row-label { - color: $ui-fleet-black-50; - } - - &__row-trailing { - text-align: right; - width: 50%; - } - - &__row-disabled-hint { - color: $ui-fleet-black-50; - } - - &__row-picker { - max-width: 300px; - margin-left: auto; - text-align: left; - } - - &__learn-more { - font-size: $x-small; - color: $ui-fleet-black-75; - } } diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/helpers.ts b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/helpers.ts deleted file mode 100644 index 1bcf98ecd01..00000000000 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IConfig } from "interfaces/config"; -import { ITeamConfig } from "interfaces/team"; -import { TicketOrWebhookState } from "./types"; - -/** Identifies whether the team has webhook automations or ticket integrations - * enabled for failing policies. */ -export const getTicketOrWebhookState = ( - automationsConfig: IConfig | ITeamConfig | undefined -): TicketOrWebhookState => { - if (!automationsConfig) return "disabled"; - - const webhookEnabled = - automationsConfig.webhook_settings?.failing_policies_webhook - ?.enable_failing_policies_webhook ?? false; - - const integrations = automationsConfig.integrations; - const ticketEnabled = - !!integrations?.jira?.some((j) => j.enable_failing_policies) || - !!integrations?.zendesk?.some((z) => z.enable_failing_policies); - - if (webhookEnabled) return "webhook"; - if (ticketEnabled) return "ticket"; - return "disabled"; -}; - -export const getTicketOrWebhookLabel = ( - state: TicketOrWebhookState -): string => { - if (state === "webhook") return "Send webhook"; - if (state === "ticket") return "Create ticket"; - return "Send webhook or create ticket"; -}; diff --git a/frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsx b/frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsx new file mode 100644 index 00000000000..7900890941a --- /dev/null +++ b/frontend/pages/policies/components/PolicyAutomationsFields/PolicyAutomationsFields.tsx @@ -0,0 +1,407 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ + +import React, { + forwardRef, + useImperativeHandle, + useMemo, + useState, +} from "react"; +import { SingleValue } from "react-select-5"; + +import { IConfig } from "interfaces/config"; +import { IPolicy } from "interfaces/policy"; +import { ITeamConfig, API_NO_TEAM_ID } from "interfaces/team"; + +import Checkbox from "components/forms/fields/Checkbox"; +import CustomLink from "components/CustomLink"; +import DropdownWrapper, { + CustomOptionType, +} from "components/forms/fields/DropdownWrapper/DropdownWrapper"; +import TooltipWrapper from "components/TooltipWrapper"; + +import { getTicketOrWebhookInfo, getTicketOrWebhookLabel } from "./helpers"; +import { IAutomationRow } from "./types"; +import { useScripts, useSoftwareTitles } from "./hooks"; +import { IPolicyAutomationUpdate } from "./hooks/useUpdatePolicyAutomations"; + +const baseClass = "policy-automations-fields"; + +/** Result returned to the parent on save, describing what (if anything) + * changed. The parent renders `error` and persists the update parts. */ +export interface IPolicyAutomationsPayload { + /** A validation message (e.g. a checked automation is missing its required + * selection), or null when the selection is valid. */ + error: string | null; + /** false when nothing changed from the policy's stored automations. */ + isDirty: boolean; + policyUpdate?: IPolicyAutomationUpdate; + webhookOrTicketUpdate?: { enabled: boolean }; +} + +export interface IPolicyAutomationsFieldsHandle { + /** Validates the current selection and returns the validation `error` (if + * any) plus the changed automation parts for the parent to persist. */ + getAutomationsPayload: () => IPolicyAutomationsPayload; +} + +interface IPolicyAutomationsFieldsProps { + policy: IPolicy; + /** When true, only the webhook/ticket row is shown and the continuous-retry + * option is hidden (global / "All fleets" policies). */ + isGlobalPolicy: boolean; + /** undefined for "All fleets", 0 for "Unassigned", positive for a fleet. */ + teamIdForApi: number | undefined; + /** Config that owns the policy's automations (team config, or global config + * for inherited/global policies). */ + automationsConfig: IConfig | ITeamConfig | undefined; + /** Global config — needed to read conditional access on the "Unassigned" + * view. */ + globalConfig: IConfig | undefined; + /** Fleet display name, used in the "Not enabled for " hints. */ + fleetName: string; +} + +const PolicyAutomationsFields = forwardRef< + IPolicyAutomationsFieldsHandle, + IPolicyAutomationsFieldsProps +>( + ( + { + policy, + isGlobalPolicy, + teamIdForApi, + automationsConfig, + globalConfig, + fleetName, + }, + ref + ) => { + const { + state: ticketOrWebhookState, + policyIds: webhookOrTicketPolicyIds, + } = getTicketOrWebhookInfo(automationsConfig); + const isTicketWebhookEnabled = ticketOrWebhookState !== "disabled"; + + const isCalendarEnabledForTeam = !isGlobalPolicy + ? (automationsConfig as ITeamConfig | undefined)?.integrations + ?.google_calendar?.enable_calendar_events ?? false + : false; + + const getIsConditionalAccessEnabledForTeam = () => { + if (isGlobalPolicy) return false; + if (teamIdForApi === API_NO_TEAM_ID) { + return globalConfig?.integrations?.conditional_access_enabled ?? false; + } + return ( + (automationsConfig as ITeamConfig | undefined)?.integrations + ?.conditional_access_enabled ?? false + ); + }; + const isConditionalAccessEnabledForTeam = getIsConditionalAccessEnabledForTeam(); + + const initialWebhookOrTicket = webhookOrTicketPolicyIds.includes(policy.id); + const initialInstallSoftware = !!policy.install_software; + const initialRunScript = !!policy.run_script; + const initialCalendar = policy.calendar_events_enabled; + const initialConditionalAccess = policy.conditional_access_enabled; + const initialContinuous = policy.continuous_automations_enabled ?? false; + + const [webhookOrTicketEnabled, setWebhookOrTicketEnabled] = useState( + initialWebhookOrTicket + ); + const [installSoftware, setInstallSoftware] = useState( + initialInstallSoftware + ); + const [runScript, setRunScript] = useState(initialRunScript); + const [calendarEvent, setCalendarEvent] = useState(initialCalendar); + const [conditionalAccess, setConditionalAccess] = useState( + initialConditionalAccess + ); + const [continuousEnabled, setContinuousEnabled] = useState( + initialContinuous + ); + + const [softwareTitleId, setSoftwareTitleId] = useState( + policy.install_software?.software_title_id ?? null + ); + const [scriptId, setScriptId] = useState( + policy.run_script?.id ?? null + ); + + const canFetchTeamScopedLists = + !isGlobalPolicy && teamIdForApi !== undefined; + const { data: softwareTitlesData } = useSoftwareTitles({ + fleetId: teamIdForApi ?? 0, + enabled: canFetchTeamScopedLists && installSoftware, + }); + const { data: scriptsData } = useScripts({ + fleetId: teamIdForApi ?? 0, + enabled: canFetchTeamScopedLists && runScript, + }); + + const softwareOptions: CustomOptionType[] = useMemo( + () => + (softwareTitlesData?.software_titles ?? []).map((t) => ({ + label: t.name, + value: String(t.id), + })), + [softwareTitlesData] + ); + + const scriptOptions: CustomOptionType[] = useMemo( + () => + (scriptsData?.scripts ?? []).map((s) => ({ + label: s.name, + value: String(s.id), + })), + [scriptsData] + ); + + useImperativeHandle(ref, () => ({ + getAutomationsPayload: () => { + // Block enabling install/run without a selection — saving would + // silently unset the automation. The caller renders the error. + if (installSoftware && softwareTitleId === null) { + return { + error: "Please select software to install.", + isDirty: false, + }; + } + if (runScript && scriptId === null) { + return { error: "Please select a script to run.", isDirty: false }; + } + + const perPolicyDirty = + !isGlobalPolicy && + (installSoftware !== initialInstallSoftware || + softwareTitleId !== + (policy.install_software?.software_title_id ?? null) || + runScript !== initialRunScript || + scriptId !== (policy.run_script?.id ?? null) || + calendarEvent !== initialCalendar || + conditionalAccess !== initialConditionalAccess || + continuousEnabled !== initialContinuous); + const webhookDirty = webhookOrTicketEnabled !== initialWebhookOrTicket; + + return { + error: null, + isDirty: perPolicyDirty || webhookDirty, + policyUpdate: perPolicyDirty + ? { + software_title_id: installSoftware ? softwareTitleId : null, + script_id: runScript ? scriptId : null, + // When the team has the feature disabled, the row is locked + // and the user can't toggle it — so we omit the field instead + // of carrying the stale state through to the PATCH. That + // preserves the policy's stored intent for if/when the team + // admin re-enables the feature. + ...(isCalendarEnabledForTeam && { + calendar_events_enabled: calendarEvent, + }), + ...(isConditionalAccessEnabledForTeam && { + conditional_access_enabled: conditionalAccess, + }), + continuous_automations_enabled: continuousEnabled, + } + : undefined, + webhookOrTicketUpdate: webhookDirty + ? { enabled: webhookOrTicketEnabled } + : undefined, + }; + }, + })); + + const rows: IAutomationRow[] = [ + { + key: "ticket_webhook", + label: getTicketOrWebhookLabel(ticketOrWebhookState), + checked: webhookOrTicketEnabled && isTicketWebhookEnabled, + onToggle: setWebhookOrTicketEnabled, + isDisabled: !isTicketWebhookEnabled, + }, + ]; + if (!isGlobalPolicy) { + rows.push( + { + key: "install_software", + label: "Install software", + tooltip: ( + + ), + checked: installSoftware, + onToggle: setInstallSoftware, + isDisabled: false, + picker: installSoftware ? ( + o.value === String(softwareTitleId ?? "") + ) ?? null + } + options={softwareOptions} + placeholder="Select software" + onChange={(opt: SingleValue) => + setSoftwareTitleId(opt ? Number(opt.value) : null) + } + /> + ) : undefined, + }, + { + key: "run_script", + label: "Run script", + tooltip: ( + + ), + checked: runScript, + onToggle: setRunScript, + isDisabled: false, + picker: runScript ? ( + o.value === String(scriptId ?? "")) ?? + null + } + options={scriptOptions} + placeholder="Select script" + onChange={(opt: SingleValue) => + setScriptId(opt ? Number(opt.value) : null) + } + /> + ) : undefined, + }, + { + key: "calendar_event", + label: "Calendar event", + tooltip: ( + + ), + checked: calendarEvent && isCalendarEnabledForTeam, + onToggle: setCalendarEvent, + isDisabled: !isCalendarEnabledForTeam, + }, + { + key: "conditional_access", + label: "Conditional access", + tooltip: ( + + ), + checked: conditionalAccess && isConditionalAccessEnabledForTeam, + onToggle: setConditionalAccess, + isDisabled: !isConditionalAccessEnabledForTeam, + } + ); + } + + return ( +
+
+ + + {rows.map((row) => ( + + + + + ))} + +
+ + {row.tooltip ? ( + + {row.label} + + ) : ( + row.label + )} + + + {row.isDisabled ? ( + + Not enabled for {fleetName} + + ) : ( + row.picker + )} +
+
+ {" "} + about automation types and their supported platforms. +
+
+ + {!isGlobalPolicy && ( +
+ + + Continuous + {" "} + software & script automations + +
+ )} +
+ ); + } +); + +interface IAutomationRowTooltipProps { + text: string; + learnMoreUrl: string; +} + +function AutomationRowTooltip({ + text, + learnMoreUrl, +}: IAutomationRowTooltipProps): JSX.Element { + return ( + <> + {text}{" "} + + + ); +} + +export default PolicyAutomationsFields; diff --git a/frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss b/frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss new file mode 100644 index 00000000000..225be25b664 --- /dev/null +++ b/frontend/pages/policies/components/PolicyAutomationsFields/_styles.scss @@ -0,0 +1,55 @@ +.policy-automations-fields { + display: flex; + flex-direction: column; + gap: $pad-medium; + + &__section { + display: flex; + flex-direction: column; + gap: $pad-small; + } + + &__table { + border-collapse: separate; + border-spacing: 0; + border: 1px solid $ui-fleet-black-10; + border-radius: 8px; + width: 100%; + font-size: $x-small; + } + + &__row td { + height: 40px; + padding: $pad-xsmall $pad-large; + border-top: 1px solid $ui-fleet-black-10; + vertical-align: middle; + } + + &__row:first-child td { + border-top: none; + } + + &__row--disabled &__row-label { + color: $ui-fleet-black-50; + } + + &__row-trailing { + text-align: right; + width: 50%; + } + + &__row-disabled-hint { + color: $ui-fleet-black-50; + } + + &__row-picker { + max-width: 300px; + margin-left: auto; + text-align: left; + } + + &__learn-more { + font-size: $x-small; + color: $ui-fleet-black-75; + } +} diff --git a/frontend/pages/policies/components/PolicyAutomationsFields/helpers.ts b/frontend/pages/policies/components/PolicyAutomationsFields/helpers.ts new file mode 100644 index 00000000000..39707e82686 --- /dev/null +++ b/frontend/pages/policies/components/PolicyAutomationsFields/helpers.ts @@ -0,0 +1,49 @@ +import { IConfig } from "interfaces/config"; +import { ITeamConfig } from "interfaces/team"; +import { TicketOrWebhookState } from "./types"; + +export interface ITicketOrWebhookInfo { + /** "webhook" or "ticket" when an "other workflow" automation is configured + * on the policy's fleet/global config; "disabled" otherwise. */ + state: TicketOrWebhookState; + /** Policy IDs configured for the active webhook/ticket automation, or `[]` + * when disabled. + * NOTE: the backend stores membership for both webhooks and integrations + * in webhook_settings.failing_policies_webhook.policy_ids. */ + policyIds: number[]; +} + +export const getTicketOrWebhookInfo = ( + automationsConfig: IConfig | ITeamConfig | undefined +): ITicketOrWebhookInfo => { + if (!automationsConfig) return { state: "disabled", policyIds: [] }; + + const webhookEnabled = + automationsConfig.webhook_settings?.failing_policies_webhook + ?.enable_failing_policies_webhook ?? false; + + const integrations = automationsConfig.integrations; + const ticketEnabled = + !!integrations?.jira?.some((j) => j.enable_failing_policies) || + !!integrations?.zendesk?.some((z) => z.enable_failing_policies); + + let state: TicketOrWebhookState = "disabled"; + if (webhookEnabled) state = "webhook"; + else if (ticketEnabled) state = "ticket"; + + const policyIds = + state === "disabled" + ? [] + : automationsConfig.webhook_settings?.failing_policies_webhook + ?.policy_ids ?? []; + + return { state, policyIds }; +}; + +export const getTicketOrWebhookLabel = ( + state: TicketOrWebhookState +): string => { + if (state === "webhook") return "Send webhook"; + if (state === "ticket") return "Create ticket"; + return "Send webhook or create ticket"; +}; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/index.ts b/frontend/pages/policies/components/PolicyAutomationsFields/hooks/index.ts similarity index 100% rename from frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/index.ts rename to frontend/pages/policies/components/PolicyAutomationsFields/hooks/index.ts diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useScripts.ts b/frontend/pages/policies/components/PolicyAutomationsFields/hooks/useScripts.ts similarity index 100% rename from frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useScripts.ts rename to frontend/pages/policies/components/PolicyAutomationsFields/hooks/useScripts.ts diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useSoftwareTitles.ts b/frontend/pages/policies/components/PolicyAutomationsFields/hooks/useSoftwareTitles.ts similarity index 100% rename from frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useSoftwareTitles.ts rename to frontend/pages/policies/components/PolicyAutomationsFields/hooks/useSoftwareTitles.ts diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useUpdatePolicyAutomations.ts b/frontend/pages/policies/components/PolicyAutomationsFields/hooks/useUpdatePolicyAutomations.ts similarity index 97% rename from frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useUpdatePolicyAutomations.ts rename to frontend/pages/policies/components/PolicyAutomationsFields/hooks/useUpdatePolicyAutomations.ts index 56f5cc5efec..d4d96bcc3fa 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/hooks/useUpdatePolicyAutomations.ts +++ b/frontend/pages/policies/components/PolicyAutomationsFields/hooks/useUpdatePolicyAutomations.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "react-query"; import { AppContext } from "context/app"; import { IConfig } from "interfaces/config"; -import { IPolicyFormData, IPolicyStats } from "interfaces/policy"; +import { IPolicy, IPolicyFormData } from "interfaces/policy"; import { ITeamConfig } from "interfaces/team"; import configAPI from "services/entities/config"; import teamPoliciesAPI from "services/entities/team_policies"; @@ -28,7 +28,7 @@ export interface IUpdatePolicyAutomationsVars { } interface IUseUpdatePolicyAutomationsArgs { - policy: IPolicyStats; + policy: IPolicy; teamIdForApi: number | undefined; isGlobalPolicy: boolean; automationsConfig: IConfig | ITeamConfig | undefined; diff --git a/frontend/pages/policies/components/PolicyAutomationsFields/index.ts b/frontend/pages/policies/components/PolicyAutomationsFields/index.ts new file mode 100644 index 00000000000..71103555f30 --- /dev/null +++ b/frontend/pages/policies/components/PolicyAutomationsFields/index.ts @@ -0,0 +1,13 @@ +export { default } from "./PolicyAutomationsFields"; +export type { + IPolicyAutomationsFieldsHandle, + IPolicyAutomationsPayload, +} from "./PolicyAutomationsFields"; + +export { default as useUpdatePolicyAutomations } from "./hooks/useUpdatePolicyAutomations"; +export type { + IPolicyAutomationUpdate, + IUpdatePolicyAutomationsVars, +} from "./hooks/useUpdatePolicyAutomations"; + +export { getTicketOrWebhookInfo } from "./helpers"; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/types.ts b/frontend/pages/policies/components/PolicyAutomationsFields/types.ts similarity index 100% rename from frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/types.ts rename to frontend/pages/policies/components/PolicyAutomationsFields/types.ts diff --git a/frontend/pages/policies/details/PolicyDetailsPage/PolicyDetailsPage.tsx b/frontend/pages/policies/details/PolicyDetailsPage/PolicyDetailsPage.tsx index 8a09af085da..84e4d600bb4 100644 --- a/frontend/pages/policies/details/PolicyDetailsPage/PolicyDetailsPage.tsx +++ b/frontend/pages/policies/details/PolicyDetailsPage/PolicyDetailsPage.tsx @@ -37,7 +37,10 @@ import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; import Avatar from "components/Avatar"; import ShowQueryModal from "components/modals/ShowQueryModal"; -import PolicyAutomations from "pages/policies/edit/components/PolicyAutomations"; +import { + PatchAutomationCta, + PolicyAutomationsList, +} from "pages/policies/edit/components/PolicyAutomations"; interface IPolicyDetailsPageProps { router: InjectedRouter; @@ -446,14 +449,19 @@ const PolicyDetailsPage = ({ {renderPlatforms()} {renderLabels()} {storedPolicy && ( - + <> + + + )} )} diff --git a/frontend/pages/policies/edit/EditPolicyPage.tsx b/frontend/pages/policies/edit/EditPolicyPage.tsx index 17296c3bf84..4ff5e8e15a3 100644 --- a/frontend/pages/policies/edit/EditPolicyPage.tsx +++ b/frontend/pages/policies/edit/EditPolicyPage.tsx @@ -10,13 +10,11 @@ import { IPolicyFormData, IPolicy, IStoredPolicyResponse, - OtherAutomationType, } from "interfaces/policy"; import { API_ALL_TEAMS_ID, APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; import globalPoliciesAPI from "services/entities/global_policies"; import teamPoliciesAPI from "services/entities/team_policies"; import policiesAPI from "services/entities/policies"; -import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; import statusAPI from "services/entities/status"; import PATHS from "router/paths"; import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants"; @@ -207,41 +205,6 @@ const PolicyPage = ({ ); } - // Fetch team config to determine "Other" automations (webhooks/integrations) - const { data: teamData } = useQuery( - ["teams", teamIdForApi], - () => teamsAPI.load(teamIdForApi), - { - enabled: - isRouteOk && - teamIdForApi !== undefined && - teamIdForApi > 0 && - storedPolicy?.type === "patch", - staleTime: 5000, - } - ); - - let currentAutomatedPolicies: number[] = []; - let otherAutomationType: OtherAutomationType | undefined; - if (teamData?.team) { - const { - webhook_settings: { failing_policies_webhook: webhook }, - integrations, - } = teamData.team; - const isIntegrationEnabled = - (integrations?.jira?.some((j: any) => j.enable_failing_policies) || - integrations?.zendesk?.some((z: any) => z.enable_failing_policies)) ?? - false; - if (isIntegrationEnabled || webhook?.enable_failing_policies_webhook) { - currentAutomatedPolicies = webhook?.policy_ids || []; - } - if (isIntegrationEnabled) { - otherAutomationType = "ticket"; - } else if (webhook?.enable_failing_policies_webhook) { - otherAutomationType = "webhook"; - } - } - // this function is passed way down, wrapped and ultimately called by SaveNewPolicyModal const { mutateAsync: createPolicy } = useMutation( (formData: IPolicyFormData) => { @@ -333,8 +296,6 @@ const PolicyPage = ({ onOpenSchemaSidebar, renderLiveQueryWarning, teamIdForApi, - currentAutomatedPolicies, - otherAutomationType, }; return ; diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tests.tsx b/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tests.tsx new file mode 100644 index 00000000000..818a83702e1 --- /dev/null +++ b/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tests.tsx @@ -0,0 +1,169 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { AppContext, initialState } from "context/app"; +import { IPolicy } from "interfaces/policy"; +import createMockConfig from "__mocks__/configMock"; +import PatchAutomationCta from "./PatchAutomationCta"; + +const createMockPatchPolicy = (overrides?: Partial): IPolicy => ({ + id: 10, + name: "macOS - Zoom up to date", + query: "SELECT 1;", + description: "Checks Zoom is up to date", + author_id: 1, + author_name: "Admin", + author_email: "admin@example.com", + resolution: "Install the latest version from self-service.", + platform: "darwin", + team_id: 1, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + critical: false, + calendar_events_enabled: false, + conditional_access_enabled: false, + type: "patch", + patch_software: { + name: "Zoom", + display_name: "Zoom", + software_title_id: 42, + }, + ...overrides, +}); + +// Wrap with AppContext so GitOpsModeTooltipWrapper's useGitOpsMode hook works +const renderWithAppContext = (ui: React.ReactElement) => { + return render( + + {ui} + + ); +}; + +describe("PatchAutomationCta", () => { + describe("renders when conditions are met (patch policy with patch_software, no install_software)", () => { + it("shows the CTA card and Add automation button when canEditPolicy is true", () => { + renderWithAppContext( + + ); + + expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Add automation/ }) + ).toBeInTheDocument(); + }); + + it("calls onAddAutomation when the button is clicked", async () => { + const user = userEvent.setup(); + const onAddAutomation = jest.fn(); + renderWithAppContext( + + ); + + await user.click(screen.getByRole("button", { name: /Add automation/ })); + expect(onAddAutomation).toHaveBeenCalledTimes(1); + }); + + it("does NOT render when canEditPolicy is false", () => { + renderWithAppContext( + + ); + + expect( + screen.queryByText(/Automatically patch Zoom/) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /Add automation/ }) + ).not.toBeInTheDocument(); + }); + + it("shows 'Adding...' text when isAddingAutomation is true", () => { + renderWithAppContext( + + ); + + expect(screen.getByText("Adding...")).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /Add automation/ }) + ).not.toBeInTheDocument(); + }); + }); + + describe("renders nothing when conditions are not met", () => { + it("for a dynamic (non-patch) policy", () => { + renderWithAppContext( + + ); + + expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); + }); + + it("when patch_software is not set", () => { + renderWithAppContext( + + ); + + expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); + }); + + it("renders for a no-team policy (team_id === 0)", () => { + renderWithAppContext( + + ); + + expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Add automation/ }) + ).toBeInTheDocument(); + }); + + it("when install_software is already set", () => { + renderWithAppContext( + + ); + + expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tsx b/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tsx new file mode 100644 index 00000000000..5e88c702b0c --- /dev/null +++ b/frontend/pages/policies/edit/components/PolicyAutomations/PatchAutomationCta.tsx @@ -0,0 +1,73 @@ +import React from "react"; + +import { IPolicy } from "interfaces/policy"; + +import Button from "components/buttons/Button"; +import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; +import Icon from "components/Icon"; + +const baseClass = "patch-automation-cta"; + +interface IPatchAutomationCtaProps { + storedPolicy: IPolicy; + /** Some users only have access to read-only view */ + canEditPolicy: boolean; + onAddAutomation: () => void; + isAddingAutomation?: boolean; +} + +/** CTA card shown above the automations section for patch policies that have + * a patch software target but haven't been wired to install it yet. Returns + * null when the conditions aren't met, so callers can render unconditionally. */ +const PatchAutomationCta = ({ + storedPolicy, + canEditPolicy, + onAddAutomation, + isAddingAutomation, +}: IPatchAutomationCtaProps): JSX.Element | null => { + const isPatchPolicy = storedPolicy.type === "patch"; + const hasPatchSoftware = !!storedPolicy.patch_software; + const hasSoftwareAutomation = !!storedPolicy.install_software; + + if ( + !isPatchPolicy || + !hasPatchSoftware || + hasSoftwareAutomation || + !canEditPolicy + ) { + return null; + } + + const patchSoftwareName = + storedPolicy.patch_software?.display_name || + storedPolicy.patch_software?.name || + ""; + + return ( +
+ + Automatically patch {patchSoftwareName} + + ( + + )} + /> +
+ ); +}; + +export default PatchAutomationCta; diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tests.tsx b/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tests.tsx deleted file mode 100644 index c9caf72838a..00000000000 --- a/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tests.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; - -import { AppContext, initialState } from "context/app"; -import { IPolicy } from "interfaces/policy"; -import createMockConfig from "__mocks__/configMock"; -import PolicyAutomations from "./PolicyAutomations"; - -// Stub SoftwareIcon to avoid asset resolution in tests -jest.mock("pages/SoftwarePage/components/icons/SoftwareIcon", () => { - return () => ; -}); - -const createMockPatchPolicy = (overrides?: Partial): IPolicy => ({ - id: 10, - name: "macOS - Zoom up to date", - query: "SELECT 1;", - description: "Checks Zoom is up to date", - author_id: 1, - author_name: "Admin", - author_email: "admin@example.com", - resolution: "Install the latest version from self-service.", - platform: "darwin", - team_id: 1, - created_at: "2026-01-01T00:00:00Z", - updated_at: "2026-01-01T00:00:00Z", - critical: false, - calendar_events_enabled: false, - conditional_access_enabled: false, - type: "patch", - patch_software: { - name: "Zoom", - display_name: "Zoom", - software_title_id: 42, - }, - ...overrides, -}); - -// Wrap with AppContext so GitOpsModeTooltipWrapper's useGitOpsMode hook works -const renderWithAppContext = (ui: React.ReactElement) => { - return render( - - {ui} - - ); -}; - -const createMockPolicy = (overrides?: Partial): IPolicy => ({ - id: 1, - name: "Test policy", - query: "SELECT 1;", - description: "", - author_id: 1, - author_name: "Admin", - author_email: "admin@example.com", - resolution: "", - platform: "darwin", - team_id: 1, - created_at: "2026-01-01T00:00:00Z", - updated_at: "2026-01-01T00:00:00Z", - critical: false, - calendar_events_enabled: false, - conditional_access_enabled: false, - type: "dynamic", - ...overrides, -}); - -const defaultProps = { - onAddAutomation: jest.fn(), - currentAutomatedPolicies: [] as number[], -}; - -describe("PolicyAutomations", () => { - describe("CTA card (patch policy with patch_software, no install_software)", () => { - it("shows the CTA card and Add automation button when canEditPolicy is true", () => { - renderWithAppContext( - - ); - - expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /Add automation/ }) - ).toBeInTheDocument(); - }); - - it("calls onAddAutomation when the button is clicked", async () => { - const user = userEvent.setup(); - const onAddAutomation = jest.fn(); - renderWithAppContext( - - ); - - await user.click(screen.getByRole("button", { name: /Add automation/ })); - expect(onAddAutomation).toHaveBeenCalledTimes(1); - }); - - it("does NOT show the CTA card when canEditPolicy is false", () => { - renderWithAppContext( - - ); - - expect( - screen.queryByText(/Automatically patch Zoom/) - ).not.toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: /Add automation/ }) - ).not.toBeInTheDocument(); - }); - - it("shows 'Adding...' text when isAddingAutomation is true", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Adding...")).toBeInTheDocument(); - expect( - screen.queryByRole("button", { name: /Add automation/ }) - ).not.toBeInTheDocument(); - }); - }); - - describe("CTA card is hidden when conditions are not met", () => { - it("hides the CTA card for a dynamic (non-patch) policy", () => { - renderWithAppContext( - - ); - - expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); - }); - - it("hides the CTA card when patch_software is not set", () => { - renderWithAppContext( - - ); - - expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); - }); - - it("shows the CTA card for a no-team policy (team_id === 0)", () => { - renderWithAppContext( - - ); - - expect(screen.getByText(/Automatically patch Zoom/)).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /Add automation/ }) - ).toBeInTheDocument(); - }); - - it("hides the CTA card when install_software is already set", () => { - renderWithAppContext( - - ); - - expect(screen.queryByText(/Automatically patch/)).not.toBeInTheDocument(); - }); - }); - - describe("automations list", () => { - it("shows empty state when no automations are configured", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("No automations")).toBeInTheDocument(); - }); - - it("shows software automation row", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Zoom")).toBeInTheDocument(); - expect(screen.queryByText("No automations")).not.toBeInTheDocument(); - }); - - it("shows script automation row", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("fix.sh")).toBeInTheDocument(); - }); - - it("shows calendar automation row", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Maintenance window")).toBeInTheDocument(); - }); - - it("shows conditional access automation row", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Block single sign-on")).toBeInTheDocument(); - }); - - it("shows 'Webhook' for other automation when otherAutomationType is webhook", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Webhook")).toBeInTheDocument(); - expect(screen.queryByText("Ticket")).not.toBeInTheDocument(); - }); - - it("shows 'Ticket' for other automation when otherAutomationType is ticket", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Ticket")).toBeInTheDocument(); - expect(screen.queryByText("Webhook")).not.toBeInTheDocument(); - }); - - it("shows 'Webhook or ticket' for other automation when otherAutomationType is not set", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("Webhook or ticket")).toBeInTheDocument(); - }); - }); - - describe("footer text", () => { - it("shows default footer text when continuous_automations_enabled is not set", () => { - renderWithAppContext( - - ); - - expect( - screen.getByText( - "Automations run on a host's first failure, or when a host's response changes from pass to fail." - ) - ).toBeInTheDocument(); - }); - - it("shows continuous footer text when continuous_automations_enabled is true", () => { - renderWithAppContext( - - ); - - expect( - screen.getByText(/Software and script automations run/) - ).toBeInTheDocument(); - expect(screen.getByText("every time")).toBeInTheDocument(); - expect( - screen.getByText(/All other automations run on a host's first failure/) - ).toBeInTheDocument(); - }); - - it("shows footer text even in the empty state", () => { - renderWithAppContext( - - ); - - expect(screen.getByText("No automations")).toBeInTheDocument(); - expect( - screen.getByText( - "Automations run on a host's first failure, or when a host's response changes from pass to fail." - ) - ).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tests.tsx b/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tests.tsx new file mode 100644 index 00000000000..e617a974a3a --- /dev/null +++ b/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tests.tsx @@ -0,0 +1,185 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { IPolicy } from "interfaces/policy"; +import PolicyAutomationsList from "./PolicyAutomationsList"; + +// Stub SoftwareIcon to avoid asset resolution in tests +jest.mock("pages/SoftwarePage/components/icons/SoftwareIcon", () => { + return () => ; +}); + +const createMockPolicy = (overrides?: Partial): IPolicy => ({ + id: 1, + name: "Test policy", + query: "SELECT 1;", + description: "", + author_id: 1, + author_name: "Admin", + author_email: "admin@example.com", + resolution: "", + platform: "darwin", + team_id: 1, + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + critical: false, + calendar_events_enabled: false, + conditional_access_enabled: false, + type: "dynamic", + ...overrides, +}); + +describe("PolicyAutomationsList", () => { + describe("automations list", () => { + it("shows empty state when no automations are configured", () => { + render( + + ); + + expect(screen.getByText("No automations")).toBeInTheDocument(); + }); + + it("shows software automation row", () => { + render( + + ); + + expect(screen.getByText("Zoom")).toBeInTheDocument(); + expect(screen.queryByText("No automations")).not.toBeInTheDocument(); + }); + + it("shows script automation row", () => { + render( + + ); + + expect(screen.getByText("fix.sh")).toBeInTheDocument(); + }); + + it("shows calendar automation row", () => { + render( + + ); + + expect(screen.getByText("Maintenance window")).toBeInTheDocument(); + }); + + it("shows conditional access automation row", () => { + render( + + ); + + expect(screen.getByText("Block single sign-on")).toBeInTheDocument(); + }); + + it("shows 'Webhook' for other automation when otherAutomationType is webhook", () => { + render( + + ); + + expect(screen.getByText("Webhook")).toBeInTheDocument(); + expect(screen.queryByText("Ticket")).not.toBeInTheDocument(); + }); + + it("shows 'Ticket' for other automation when otherAutomationType is ticket", () => { + render( + + ); + + expect(screen.getByText("Ticket")).toBeInTheDocument(); + expect(screen.queryByText("Webhook")).not.toBeInTheDocument(); + }); + + it("shows 'Webhook or ticket' for other automation when otherAutomationType is not set", () => { + render( + + ); + + expect(screen.getByText("Webhook or ticket")).toBeInTheDocument(); + }); + }); + + describe("footer text", () => { + it("shows default footer text when continuous_automations_enabled is not set", () => { + render( + + ); + + expect( + screen.getByText( + "Automations run on a host's first failure, or when a host's response changes from pass to fail." + ) + ).toBeInTheDocument(); + }); + + it("shows continuous footer text when continuous_automations_enabled is true", () => { + render( + + ); + + expect( + screen.getByText(/Software and script automations run/) + ).toBeInTheDocument(); + expect(screen.getByText("every time")).toBeInTheDocument(); + expect( + screen.getByText(/All other automations run on a host's first failure/) + ).toBeInTheDocument(); + }); + + it("shows footer text even in the empty state", () => { + render( + + ); + + expect(screen.getByText("No automations")).toBeInTheDocument(); + expect( + screen.getByText( + "Automations run on a host's first failure, or when a host's response changes from pass to fail." + ) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tsx b/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tsx similarity index 70% rename from frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tsx rename to frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tsx index 8155c403df2..05cd03674b8 100644 --- a/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomations.tsx +++ b/frontend/pages/policies/edit/components/PolicyAutomations/PolicyAutomationsList.tsx @@ -5,24 +5,11 @@ import { IPolicy, OtherAutomationType } from "interfaces/policy"; import PATHS from "router/paths"; import { getPathWithQueryParams } from "utilities/url"; -import Button from "components/buttons/Button"; -import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; import Graphic from "components/Graphic"; import { GraphicNames } from "components/graphics"; -import Icon from "components/Icon"; import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; -const baseClass = "policy-automations"; - -interface IPolicyAutomationsProps { - storedPolicy: IPolicy; - currentAutomatedPolicies: number[]; - /** Some users only have access to read-only view */ - canEditPolicy: boolean; - onAddAutomation: () => void; - isAddingAutomation?: boolean; - otherAutomationType?: OtherAutomationType; -} +const baseClass = "policy-automations-list"; const OTHER_AUTOMATION_NAMES: Record = { webhook: "Webhook", @@ -39,23 +26,20 @@ interface IAutomationRow { sortName: string; } -const PolicyAutomations = ({ +interface IPolicyAutomationsListProps { + storedPolicy: IPolicy; + currentAutomatedPolicies: number[]; + otherAutomationType?: OtherAutomationType; +} + +/** Read-only summary of the automations currently configured on a policy: + * the "Automations" header, a row per active automation (or an empty state), + * and the footer text explaining when they run. */ +const PolicyAutomationsList = ({ storedPolicy, currentAutomatedPolicies, - canEditPolicy, - onAddAutomation, - isAddingAutomation, otherAutomationType, -}: IPolicyAutomationsProps): JSX.Element => { - const isPatchPolicy = storedPolicy.type === "patch"; - const hasPatchSoftware = !!storedPolicy.patch_software; - const hasSoftwareAutomation = !!storedPolicy.install_software; - const showCtaCard = - isPatchPolicy && - hasPatchSoftware && - !hasSoftwareAutomation && - canEditPolicy; - +}: IPolicyAutomationsListProps): JSX.Element => { const automationRows: IAutomationRow[] = []; if (storedPolicy.install_software) { @@ -124,39 +108,9 @@ const PolicyAutomations = ({ return a.sortName.localeCompare(b.sortName); }); - const patchSoftwareName = - storedPolicy.patch_software?.display_name || - storedPolicy.patch_software?.name || - ""; - return (
- {showCtaCard && ( -
- - Automatically patch {patchSoftwareName} - - ( - - )} - /> -
- )} -
Automations
+
Automations
{automationRows.length > 0 ? (
{automationRows.map((row) => ( @@ -206,4 +160,4 @@ const PolicyAutomations = ({ ); }; -export default PolicyAutomations; +export default PolicyAutomationsList; diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/_styles.scss b/frontend/pages/policies/edit/components/PolicyAutomations/_styles.scss index 7cef3a462d8..d4ff3ae23a1 100644 --- a/frontend/pages/policies/edit/components/PolicyAutomations/_styles.scss +++ b/frontend/pages/policies/edit/components/PolicyAutomations/_styles.scss @@ -1,23 +1,29 @@ -.policy-automations { +.patch-automation-cta { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + width: 100%; + padding: $pad-medium; + background-color: $ui-fleet-black-5; + border: 1px solid $ui-fleet-black-10; + border-radius: 8px; + + &__label { + font-size: $x-small; + color: $ui-fleet-black-75; + } +} + +.policy-automations-list { display: flex; flex-direction: column; gap: 0.5rem; font-size: $x-small; - &__cta-card { - display: flex; - align-items: center; - justify-content: space-between; - padding: $pad-medium; - background-color: $ui-fleet-black-5; - border: 1px solid $ui-fleet-black-10; - border-radius: 8px; - margin-bottom: $pad-medium; - } - - &__cta-label { - font-size: $x-small; - color: $ui-fleet-black-75; + &__header { + color: $core-fleet-black; + font-weight: $bold; } &__list { @@ -26,11 +32,6 @@ border-radius: 8px; } - &__list-label { - color: $core-fleet-black; - font-weight: $bold; - } - &__row { display: flex; align-items: center; @@ -52,7 +53,6 @@ display: flex; align-items: center; gap: $pad-small; - font-size: $x-small; color: $ui-fleet-black-75; a { diff --git a/frontend/pages/policies/edit/components/PolicyAutomations/index.ts b/frontend/pages/policies/edit/components/PolicyAutomations/index.ts index 4c857907a68..5e52b78bc4c 100644 --- a/frontend/pages/policies/edit/components/PolicyAutomations/index.ts +++ b/frontend/pages/policies/edit/components/PolicyAutomations/index.ts @@ -1 +1,2 @@ -export { default } from "./PolicyAutomations"; +export { default as PatchAutomationCta } from "./PatchAutomationCta"; +export { default as PolicyAutomationsList } from "./PolicyAutomationsList"; diff --git a/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx b/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx index 85adf928365..e3c9eed1f39 100644 --- a/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx +++ b/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx @@ -62,7 +62,6 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription: false, isFetchingAutofillResolution: false, resetAiAutofillData: jest.fn(), - currentAutomatedPolicies: [], }; it("should not show the target selector in the free tier", async () => { @@ -150,7 +149,6 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription={false} isFetchingAutofillResolution={false} resetAiAutofillData={jest.fn()} - currentAutomatedPolicies={[]} /> ); @@ -220,7 +218,6 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription={false} isFetchingAutofillResolution={false} resetAiAutofillData={jest.fn()} - currentAutomatedPolicies={[]} /> ); @@ -303,7 +300,6 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription={false} isFetchingAutofillResolution={false} resetAiAutofillData={jest.fn()} - currentAutomatedPolicies={[]} /> ); @@ -382,12 +378,11 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription={false} isFetchingAutofillResolution={false} resetAiAutofillData={jest.fn()} - currentAutomatedPolicies={[]} /> ); expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "Run" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Run policy" })).toBeDisabled(); await user.hover(screen.getByRole("button", { name: "Save" })); await waitFor(() => { @@ -461,15 +456,14 @@ describe("PolicyForm - component", () => { isFetchingAutofillDescription={false} isFetchingAutofillResolution={false} resetAiAutofillData={jest.fn()} - currentAutomatedPolicies={[]} /> ); - expect(screen.getByRole("button", { name: "Run" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Run policy" })).toBeDisabled(); await waitFor(() => { waitFor(() => { - user.hover(screen.getByRole("button", { name: "Run" })); + user.hover(screen.getByRole("button", { name: "Run policy" })); }); expect( diff --git a/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx b/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx index 4ecf66e2e1d..0cabbbf4a5f 100644 --- a/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx +++ b/frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ /* eslint-disable jsx-a11y/interactive-supports-focus */ -import React, { useState, useContext, useEffect, useMemo } from "react"; +import React, { useState, useContext, useEffect, useMemo, useRef } from "react"; import { useQuery, useQueryClient } from "react-query"; import { Ace } from "ace-builds"; @@ -20,11 +20,7 @@ import { } from "components/TargetLabelSelector/labelScopes"; import { getPathWithQueryParams } from "utilities/url"; -import { - IPolicy, - IPolicyFormData, - OtherAutomationType, -} from "interfaces/policy"; +import { IPolicy, IPolicyFormData } from "interfaces/policy"; import { APP_CONTEXT_ALL_TEAMS_SUMMARY, APP_CONTEXT_NO_TEAM_SUMMARY, @@ -59,9 +55,16 @@ import labelsAPI, { } from "services/entities/labels"; import teamPoliciesAPI from "services/entities/team_policies"; +import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; + +import PolicyAutomationsFields, { + IPolicyAutomationsFieldsHandle, + IPolicyAutomationsPayload, + useUpdatePolicyAutomations, +} from "pages/policies/components/PolicyAutomationsFields"; import SaveNewPolicyModal from "../SaveNewPolicyModal"; -import PolicyAutomations from "../PolicyAutomations"; +import { PatchAutomationCta } from "../PolicyAutomations"; const baseClass = "policy-form"; @@ -77,7 +80,9 @@ interface IPolicyFormProps { onCreatePolicy: (formData: IPolicyFormData) => void; onOsqueryTableSelect: (tableName: string) => void; goToSelectTargets: () => void; - onUpdate: (formData: IPolicyFormData) => void; + // Returns a Promise so the form can sequence the automations save AFTER the + // core update completes (see the patch-policy save flow in promptSavePolicy). + onUpdate: (formData: IPolicyFormData) => Promise; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; backendValidators: { [key: string]: string }; @@ -86,9 +91,6 @@ interface IPolicyFormProps { onClickAutofillDescription: () => Promise; onClickAutofillResolution: () => Promise; resetAiAutofillData: () => void; - currentAutomatedPolicies: number[]; - otherAutomationType?: OtherAutomationType; - onCancel?: () => void; } const validateQuerySQL = (query: string) => { @@ -124,15 +126,11 @@ const PolicyForm = ({ onClickAutofillDescription, onClickAutofillResolution, resetAiAutofillData, - currentAutomatedPolicies, - otherAutomationType, - onCancel, }: IPolicyFormProps): JSX.Element => { const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined const [isSaveNewPolicyModalOpen, setIsSaveNewPolicyModalOpen] = useState( false ); - const [showQueryEditor, setShowQueryEditor] = useState(false); const [selectedTargetType, setSelectedTargetType] = useState("All hosts"); const [selectedCustomTarget, setSelectedCustomTarget] = useState( @@ -262,6 +260,49 @@ const PolicyForm = ({ !policyIdForEdit && DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName); + const isGlobalPolicy = storedPolicy?.team_id == null; + const automationsTeamId = storedPolicy?.team_id ?? undefined; + + const { data: automationsTeamData } = useQuery( + ["teams", automationsTeamId], + () => teamsAPI.load(automationsTeamId as number), + { + enabled: isEditMode && !!storedPolicy && !isGlobalPolicy, + staleTime: 5000, + } + ); + + const automationsConfig = + (isGlobalPolicy ? config : automationsTeamData?.team) ?? undefined; + + let automationsFleetName = ""; + if (isGlobalPolicy) { + automationsFleetName = APP_CONTEXT_ALL_TEAMS_SUMMARY.name; + } else if (storedPolicy?.team_id === 0) { + automationsFleetName = APP_CONTEXT_NO_TEAM_SUMMARY.name; + } else { + automationsFleetName = + automationsTeamData?.team?.name ?? currentTeam?.name ?? ""; + } + + const automationsRef = useRef(null); + + const { + mutate: saveAutomations, + isLoading: isSavingAutomations, + } = useUpdatePolicyAutomations({ + // storedPolicy is guaranteed present once the form (and this section) + // renders — the page shows a spinner while it loads. + policy: storedPolicy as IPolicy, + teamIdForApi: automationsTeamId, + isGlobalPolicy, + automationsConfig, + onSuccess: () => { + queryClient.invalidateQueries(["policy", policyIdForEdit]); + }, + onError: () => renderFlash("error", "Could not update policy automations."), + }); + /* - Observer/Observer+ and Technicians cannot edit existing policies - Team users cannot edit inherited policies Reroute edit existing policy page (/:policyId/edit) to policy details page (/:policyId) */ @@ -403,23 +444,49 @@ const PolicyForm = ({ } }; - const promptSavePolicy = () => (evt: React.MouseEvent) => { + const promptSavePolicy = () => async ( + evt: React.MouseEvent + ) => { evt.preventDefault(); if (isEditMode && !lastEditedQueryName) { - return setErrors({ - ...errors, - name: "Policy name must be present", - }); + setErrors({ ...errors, name: "Policy name must be present" }); + return; } if (isEditMode && !isPatchPolicy && !isAnyPlatformSelected) { - return setErrors({ + setErrors({ ...errors, name: "At least one platform must be selected", }); + return; + } + + // Capture + validate automation changes up front so an invalid selection + // blocks the whole save before anything is persisted. + let automations: IPolicyAutomationsPayload | undefined; + if (isEditMode) { + automations = automationsRef.current?.getAutomationsPayload(); + if (automations?.error) { + renderFlash("error", automations.error); + return; + } } + // The core update (onUpdate) and the automations update both PATCH the policy. + // We `await` the core update before firing the automations one so the + // automations write is always the LAST write to the policy. This matters + // for patch policies, where the backend re-links install_software to + // patch_software whenever a patch policy is updated. + const persistAutomations = () => { + if (automations?.isDirty) { + saveAutomations({ + policyUpdate: automations.policyUpdate, + webhookOrTicketUpdate: automations.webhookOrTicketUpdate, + }); + } + }; + if (isPatchPolicy && isEditMode) { // Patch policies: only send editable fields, not query/platform const payload: IPolicyFormData = { @@ -430,7 +497,8 @@ const PolicyForm = ({ if (isPremiumTier) { payload.critical = lastEditedQueryCritical; } - onUpdate(payload); + await onUpdate(payload); + persistAutomations(); return; } @@ -439,10 +507,8 @@ const PolicyForm = ({ // fires could otherwise submit an empty query. Mirrors the guard in // EditQueryForm's handleSaveQuery (see #38348). if (!lastEditedQueryBody?.trim()) { - return setErrors({ - ...errors, - query: EMPTY_QUERY_ERR, - }); + setErrors({ ...errors, query: EMPTY_QUERY_ERR }); + return; } let selectedPlatforms = getSelectedPlatforms(); @@ -485,7 +551,8 @@ const PolicyForm = ({ selectedCustomTarget === "labelsExcludeAny" ? customLabelNames : []; payload.critical = lastEditedQueryCritical; } - onUpdate(payload); + await onUpdate(payload); + persistAutomations(); } }; @@ -695,15 +762,26 @@ const PolicyForm = ({ suppressTitle /> )} - {isEditMode && storedPolicy && ( - + {isEditMode && !!storedPolicy && !!automationsConfig && ( +
+
Automations
+ + +
)} {isEditMode && isPremiumTier && @@ -740,11 +818,6 @@ const PolicyForm = ({ {renderPlatformCompatibility()} {renderLiveQueryWarning()}
- {isEditMode && onCancel && ( - - )} ( Save @@ -807,7 +880,7 @@ const PolicyForm = ({ } variant="inverse" > - Run + Run policy diff --git a/frontend/pages/policies/edit/screens/QueryEditor.tsx b/frontend/pages/policies/edit/screens/QueryEditor.tsx index a325ff359eb..addfd1fc3c6 100644 --- a/frontend/pages/policies/edit/screens/QueryEditor.tsx +++ b/frontend/pages/policies/edit/screens/QueryEditor.tsx @@ -12,11 +12,7 @@ import debounce from "utilities/debounce"; import deepDifference from "utilities/deep_difference"; import { getPathWithQueryParams } from "utilities/url"; import { getErrorReason } from "interfaces/errors"; -import { - IPolicyFormData, - IPolicy, - OtherAutomationType, -} from "interfaces/policy"; +import { IPolicyFormData, IPolicy } from "interfaces/policy"; import BackButton from "components/BackButton"; import PolicyForm from "pages/policies/edit/components/PolicyForm"; @@ -37,8 +33,6 @@ interface IQueryEditorProps { onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; teamIdForApi?: number; - currentAutomatedPolicies?: number[]; - otherAutomationType?: OtherAutomationType; } const QueryEditor = ({ @@ -56,8 +50,6 @@ const QueryEditor = ({ onOpenSchemaSidebar, renderLiveQueryWarning, teamIdForApi, - currentAutomatedPolicies, - otherAutomationType, }: IQueryEditorProps): JSX.Element | null => { const { currentUser, isPremiumTier, filteredPoliciesPath } = useContext( AppContext @@ -301,19 +293,6 @@ const QueryEditor = ({ onClickAutofillDescription={onClickAutofillDescription} onClickAutofillResolution={onClickAutofillResolution} resetAiAutofillData={() => setPolicyAutofillData(null)} - currentAutomatedPolicies={currentAutomatedPolicies || []} - otherAutomationType={otherAutomationType} - onCancel={ - policyIdForEdit - ? () => - router.push( - getPathWithQueryParams( - PATHS.POLICY_DETAILS(policyIdForEdit), - { fleet_id: teamIdForApi } - ) - ) - : undefined - } />
);