Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions frontend/pages/policies/PolicyPage/PolicyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "interfaces/team";
import globalPoliciesAPI from "services/entities/global_policies";
import teamPoliciesAPI from "services/entities/team_policies";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import hostAPI from "services/entities/hosts";
import statusAPI from "services/entities/status";
import { DOCUMENT_TITLE_SUFFIX, LIVE_POLICY_STEPS } from "utilities/constants";
Expand Down Expand Up @@ -155,15 +156,20 @@ const PolicyPage = ({
false
);

// TODO: Remove team endpoint workaround once global policy endpoint populates patch_software.
// The global endpoint does not return patch_software for patch policies, but the team endpoint does.
const {
isLoading: isStoredPolicyLoading,
data: storedPolicy,
error: storedPolicyError,
} = useQuery<IStoredPolicyResponse, Error, IPolicy>(
["policy", policyId],
() => globalPoliciesAPI.load(policyId as number), // Note: Team users have access to policies through global API
["policy", policyId, teamIdForApi],
() =>
teamIdForApi && teamIdForApi > 0
? teamPoliciesAPI.load(teamIdForApi, policyId as number)
: globalPoliciesAPI.load(policyId as number),
{
enabled: isRouteOk && !!policyId, // Note: this justifies the number type assertions above
enabled: isRouteOk && !!policyId,
refetchOnWindowFocus: false,
retry: false,
select: (data: IStoredPolicyResponse) => data.policy,
Expand Down Expand Up @@ -233,6 +239,35 @@ const PolicyPage = ({
);
}

// Fetch team config to determine "Other" automations (webhooks/integrations)
const { data: teamData } = useQuery<ILoadTeamResponse, Error>(
["teams", teamIdForApi],
() => teamsAPI.load(teamIdForApi),
{
enabled:
isRouteOk &&
teamIdForApi !== undefined &&
teamIdForApi > 0 &&
storedPolicy?.type === "patch",
staleTime: 5000,
}
);

let currentAutomatedPolicies: number[] = [];
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 || [];
}
}

// this function is passed way down, wrapped and ultimately called by SaveNewPolicyModal
const { mutateAsync: createPolicy } = useMutation(
(formData: IPolicyFormData) => {
Expand Down Expand Up @@ -319,6 +354,7 @@ const PolicyPage = ({
onOpenSchemaSidebar,
renderLiveQueryWarning,
teamIdForApi,
currentAutomatedPolicies,
};

const step2Opts = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from "react";
import { Link } from "react-router";

import { IPolicy } from "interfaces/policy";
import { IconNames } from "components/icons";
import PATHS from "router/paths";
import { getPathWithQueryParams } from "utilities/url";
import Button from "components/buttons/Button";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import Icon from "components/Icon/Icon";
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";

const baseClass = "policy-automations";

interface IPolicyAutomationsProps {
storedPolicy: IPolicy;
currentAutomatedPolicies: number[];
onAddAutomation: () => void;
isAddingAutomation: boolean;
gitOpsModeEnabled: boolean;
}

interface IAutomationRow {
name: string;
type: string;
iconName?: IconNames;
isSoftware?: boolean;
link?: string;
sortOrder: number;
sortName: string;
}

const PolicyAutomations = ({
storedPolicy,
currentAutomatedPolicies,
onAddAutomation,
isAddingAutomation,
gitOpsModeEnabled,
}: IPolicyAutomationsProps): JSX.Element => {
const isPatchPolicy = storedPolicy.type === "patch";
const hasPatchSoftware = !!storedPolicy.patch_software;
const hasSoftwareAutomation = !!storedPolicy.install_software;
const showCtaCard =
isPatchPolicy && hasPatchSoftware && !hasSoftwareAutomation;

const automationRows: IAutomationRow[] = [];

if (storedPolicy.install_software) {
automationRows.push({
name: storedPolicy.install_software.name,
type: "Software",
isSoftware: true,
link: getPathWithQueryParams(
PATHS.SOFTWARE_TITLE_DETAILS(
storedPolicy.install_software.software_title_id.toString()
),
{ fleet_id: storedPolicy.team_id }
),
sortOrder: 0,
sortName: storedPolicy.install_software.name.toLowerCase(),
});
}

if (storedPolicy.run_script) {
automationRows.push({
name: storedPolicy.run_script.name,
type: "Script",
iconName: "text",
sortOrder: 1,
sortName: storedPolicy.run_script.name.toLowerCase(),
});
}

if (storedPolicy.calendar_events_enabled) {
automationRows.push({
name: "Maintenance window",
type: "Calendar",
iconName: "calendar",
sortOrder: 2,
sortName: "",
});
}

if (storedPolicy.conditional_access_enabled) {
automationRows.push({
name: "Block single sign-on",
type: "Conditional access",
iconName: "disable",
sortOrder: 3,
sortName: "",
});
}

if (currentAutomatedPolicies.includes(storedPolicy.id)) {
automationRows.push({
name: "Create ticket or send webhook",
type: "Other",
iconName: "external-link",
sortOrder: 4,
sortName: "",
});
}

automationRows.sort((a, b) => {
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
return a.sortName.localeCompare(b.sortName);
});

const patchSoftwareName =
storedPolicy.patch_software?.display_name ||
storedPolicy.patch_software?.name ||
"";

return (
<div className={baseClass}>
{showCtaCard && (
<div className={`${baseClass}__cta-card`}>
<span className={`${baseClass}__cta-label`}>
Automatically patch {patchSoftwareName}
</span>
<GitOpsModeTooltipWrapper
position="top"
renderChildren={(disableChildren) => (
<Button
onClick={onAddAutomation}
variant="text-icon"
disabled={disableChildren || isAddingAutomation}
>
{isAddingAutomation ? (
"Adding..."
) : (
<>
<Icon name="plus" /> Add automation
</>
)}
</Button>
)}
/>
</div>
)}
{automationRows.length > 0 && (
<div className={`${baseClass}__list`}>
<div className={`${baseClass}__list-header`}>Automations</div>
{automationRows.map((row) => (
<div
key={`${row.type}-${row.name}`}
className={`${baseClass}__row`}
>
<span className={`${baseClass}__row-name`}>
{row.isSoftware ? (
<SoftwareIcon name={row.name} size="small" />
) : (
row.iconName && (
<Icon
name={row.iconName}
size="small"
className={`${baseClass}__row-icon`}
/>
)
)}
{row.link ? <Link to={row.link}>{row.name}</Link> : row.name}
</span>
<span className={`${baseClass}__row-type`}>{row.type}</span>
</div>
))}
</div>
)}
</div>
);
};

export default PolicyAutomations;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.policy-automations {
margin-top: $pad-medium;
max-width: 600px;

&__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: $small;
font-weight: $bold;
color: $core-fleet-black;
}

&__list {
margin-top: $pad-small;
border: 1px solid $ui-fleet-black-10;
border-radius: 8px;
padding: 0 $pad-medium;
}

&__list-header {
font-size: $xx-small;
font-weight: $bold;
text-transform: uppercase;
color: $ui-fleet-black-50;
padding-top: $pad-medium;
padding-bottom: $pad-small;
}

&__row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-top: 1px solid $ui-fleet-black-10;
}

&__row-icon {
margin-right: $pad-small;
vertical-align: middle;
color: $ui-fleet-black-50;
}

.software-icon {
margin-right: $pad-small;
}

&__row-name {
display: flex;
align-items: center;
font-size: $x-small;
color: $core-fleet-black;

a {
color: $core-vibrant-blue;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}

&__row-type {
font-size: $x-small;
color: $ui-fleet-black-50;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./PolicyAutomations";
Loading
Loading