Command palette: deep-link Manage policy automations to AutomationsModal (#46884)
This commit is contained in:
@@ -157,12 +157,13 @@ const CommandPalette = (): JSX.Element | null => {
|
||||
window.removeEventListener("fleet-theme-change", onThemeChange);
|
||||
}, []);
|
||||
|
||||
// Policy automations: same as canAddOrDeletePolicies in ManagePoliciesPage
|
||||
const canManagePolicyAutomations =
|
||||
isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isAnyTeamAdmin ||
|
||||
isAnyTeamMaintainer;
|
||||
// Policy automations: mirrors ManagePoliciesPage.canEditAutomationsSettings.
|
||||
// Maintainers can add/delete policies but the in-page Automations button
|
||||
// is hidden for them, and the deep-link useEffect re-checks the same
|
||||
// gate — so the palette item must match, otherwise it's a dead link for
|
||||
// maintainers. isTeamAdmin is scoped to currentTeam by AppContext, so a
|
||||
// user who's team admin of A but viewing B correctly won't see this.
|
||||
const canManagePolicyAutomations = isGlobalAdmin || isTeamAdmin;
|
||||
|
||||
// Software automations require global admin (all fleets view)
|
||||
const canManageSoftwareAutomations = isGlobalAdmin;
|
||||
|
||||
@@ -13,11 +13,10 @@ const buildAutomationsItems = (
|
||||
canManagePolicyAutomations,
|
||||
canManageReportAutomations,
|
||||
hasTeamSelected,
|
||||
isPremiumTier,
|
||||
isPrimoMode,
|
||||
withTeamId,
|
||||
} = ctx;
|
||||
const { isUnassigned, switchesFromUnassigned, hasTeamOrUnassigned } = derived;
|
||||
const { isUnassigned, switchesFromUnassigned } = derived;
|
||||
|
||||
return [
|
||||
// Manage automations — software. Normally All-fleets-only, but in
|
||||
@@ -82,109 +81,41 @@ const buildAutomationsItems = (
|
||||
]
|
||||
: []),
|
||||
|
||||
// Manage automations — policies (admins and maintainers)
|
||||
// Manage automations — policies (admins and maintainers). Mirrors
|
||||
// the reports pattern: ManagePoliciesPage reads ?manage_automations=1
|
||||
// and opens AutomationsModal. The page re-checks role +
|
||||
// hasPoliciesToAutomate before opening, then strips the param.
|
||||
//
|
||||
// Keywords match the sections AutomationsModal actually renders for
|
||||
// the current fleet scope (see AutomationsModal.tsx:230, 244, 281):
|
||||
// - All fleets: Webhooks/tickets only
|
||||
// - Unassigned: Webhooks/tickets + Conditional access
|
||||
// - Team: Webhooks/tickets + Calendar + Conditional access
|
||||
// Hiding the inapplicable keywords keeps the palette from matching
|
||||
// (e.g.) "calendar" to a fleet where the section isn't rendered.
|
||||
...(canManagePolicyAutomations
|
||||
? [
|
||||
{
|
||||
id: "manage-policy-automations",
|
||||
label: "Manage policy automations",
|
||||
group: "Automations" as const,
|
||||
path: withTeamId(paths.MANAGE_POLICIES),
|
||||
path: withTeamId(`${paths.MANAGE_POLICIES}?manage_automations=1`),
|
||||
keywords: [
|
||||
"manage automations",
|
||||
"failing",
|
||||
"tickets",
|
||||
"webhook",
|
||||
"jira",
|
||||
"zendesk",
|
||||
],
|
||||
subItems: [
|
||||
{
|
||||
id: "manage-policy-automations-webhooks",
|
||||
label: "Tickets & webhooks",
|
||||
path: withTeamId(
|
||||
`${paths.MANAGE_POLICIES}?manage_automations=webhooks`
|
||||
),
|
||||
keywords: [
|
||||
"manage policy automations",
|
||||
"manage automations",
|
||||
"jira",
|
||||
"zendesk",
|
||||
"failing",
|
||||
],
|
||||
},
|
||||
// Team-scoped policy automations (Premium-only). The
|
||||
// policies page allows install_software / run_script /
|
||||
// conditional_access on No team / Unassigned, so those
|
||||
// three use `hasTeamOrUnassigned`. Calendar events stay
|
||||
// on `hasTeamSelected` — the page disables them when
|
||||
// there's no specific team.
|
||||
...(isPremiumTier && hasTeamOrUnassigned
|
||||
...(hasTeamSelected
|
||||
? [
|
||||
{
|
||||
id: "manage-policy-automations-install-software",
|
||||
label: "Install software",
|
||||
path: withTeamId(
|
||||
`${paths.MANAGE_POLICIES}?manage_automations=install_software`
|
||||
),
|
||||
keywords: [
|
||||
"manage policy automations",
|
||||
"manage automations",
|
||||
"resolve",
|
||||
"remediate",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "manage-policy-automations-run-script",
|
||||
label: "Run script",
|
||||
path: withTeamId(
|
||||
`${paths.MANAGE_POLICIES}?manage_automations=run_script`
|
||||
),
|
||||
keywords: [
|
||||
"manage policy automations",
|
||||
"manage automations",
|
||||
"resolve",
|
||||
"remediate",
|
||||
],
|
||||
},
|
||||
"calendar",
|
||||
"google calendar",
|
||||
"scheduled maintenance windows",
|
||||
]
|
||||
: []),
|
||||
...(isPremiumTier && hasTeamSelected
|
||||
? [
|
||||
{
|
||||
id: "manage-policy-automations-calendar",
|
||||
label: "Calendar events",
|
||||
path: withTeamId(
|
||||
`${paths.MANAGE_POLICIES}?manage_automations=calendar`
|
||||
),
|
||||
keywords: [
|
||||
"manage policy automations",
|
||||
"manage automations",
|
||||
"reserve time",
|
||||
"maintenance window",
|
||||
"google calendar",
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isPremiumTier && hasTeamOrUnassigned
|
||||
? [
|
||||
{
|
||||
id: "manage-policy-automations-conditional-access",
|
||||
label: "Conditional access",
|
||||
path: withTeamId(
|
||||
`${paths.MANAGE_POLICIES}?manage_automations=conditional_access`
|
||||
),
|
||||
keywords: [
|
||||
"manage policy automations",
|
||||
"manage automations",
|
||||
"sso",
|
||||
"okta",
|
||||
"entra",
|
||||
"intune",
|
||||
"zero trust",
|
||||
],
|
||||
},
|
||||
]
|
||||
...(hasTeamSelected || isUnassigned
|
||||
? ["conditional access", "sso", "okta", "entra", "intune"]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -287,7 +287,17 @@ describe("CommandPalette helpers", () => {
|
||||
expect(vpp?.label).toContain("Edit");
|
||||
});
|
||||
|
||||
it("shows team-scoped policy automations when premium and team selected", () => {
|
||||
it("exposes Manage policy automations as a flat entry that deep-links into AutomationsModal", () => {
|
||||
// Previously the palette listed Tickets & webhooks / Install
|
||||
// software / Run script / Calendar / Conditional access as
|
||||
// sub-items, each with its own ?manage_automations=<section> URL.
|
||||
// ManagePoliciesPage never parsed those params, so all five were
|
||||
// dead links. AutomationsModal also dropped its Install software
|
||||
// / Run script sections (those are per-policy now), and the modal
|
||||
// has no per-section URL trigger. The palette now mirrors the
|
||||
// reports pattern: a single entry whose path is
|
||||
// /policies?manage_automations=1, which the page reads to open
|
||||
// the modal at its single shared body.
|
||||
const items = buildPaletteItems({
|
||||
...BASE_CONTEXT,
|
||||
hasTeamSelected: true,
|
||||
@@ -297,47 +307,56 @@ describe("CommandPalette helpers", () => {
|
||||
const policyAutomations = items.find(
|
||||
(i) => i.id === "manage-policy-automations"
|
||||
);
|
||||
expect(policyAutomations?.subItems?.length).toBeGreaterThan(1);
|
||||
|
||||
const subIds = policyAutomations?.subItems?.map((s) => s.id) ?? [];
|
||||
expect(subIds).toContain("manage-policy-automations-install-software");
|
||||
expect(subIds).toContain("manage-policy-automations-calendar");
|
||||
expect(policyAutomations).toBeDefined();
|
||||
expect(policyAutomations?.subItems).toBeUndefined();
|
||||
expect(policyAutomations?.path).toContain("manage_automations=1");
|
||||
});
|
||||
|
||||
it("excludes team-scoped policy automations when no team selected", () => {
|
||||
it("hides calendar + conditional-access keywords on All fleets (modal renders only Webhooks/tickets there)", () => {
|
||||
const items = buildPaletteItems(BASE_CONTEXT);
|
||||
|
||||
const policyAutomations = items.find(
|
||||
(i) => i.id === "manage-policy-automations"
|
||||
);
|
||||
// Only webhooks should be present (no team-scoped items)
|
||||
expect(policyAutomations?.subItems?.length).toBe(1);
|
||||
expect(policyAutomations?.subItems?.[0].id).toBe(
|
||||
"manage-policy-automations-webhooks"
|
||||
);
|
||||
const keywords = policyAutomations?.keywords ?? [];
|
||||
expect(keywords).toContain("webhook");
|
||||
expect(keywords).not.toContain("calendar");
|
||||
expect(keywords).not.toContain("google calendar");
|
||||
expect(keywords).not.toContain("conditional access");
|
||||
expect(keywords).not.toContain("sso");
|
||||
});
|
||||
|
||||
it("on Unassigned, shows install-software / run-script / conditional-access but NOT calendar", () => {
|
||||
// ManagePoliciesPage allows these three automations on No team
|
||||
// but disables Calendar events without a specific fleet. The
|
||||
// palette must match — earlier all four were gated together on
|
||||
// hasTeamSelected, which dropped them all on Unassigned.
|
||||
it("hides calendar keywords on Unassigned (Calendar section is disabled there) but keeps conditional access", () => {
|
||||
const items = buildPaletteItems({
|
||||
...BASE_CONTEXT,
|
||||
hasTeamSelected: false,
|
||||
currentTeam: { id: 0, name: "No team" },
|
||||
});
|
||||
|
||||
const policyAutomations = items.find(
|
||||
(i) => i.id === "manage-policy-automations"
|
||||
);
|
||||
const subIds = policyAutomations?.subItems?.map((s) => s.id) ?? [];
|
||||
const keywords = policyAutomations?.keywords ?? [];
|
||||
expect(keywords).toContain("webhook");
|
||||
expect(keywords).not.toContain("calendar");
|
||||
expect(keywords).not.toContain("google calendar");
|
||||
expect(keywords).toContain("conditional access");
|
||||
expect(keywords).toContain("sso");
|
||||
});
|
||||
|
||||
expect(subIds).toContain("manage-policy-automations-webhooks");
|
||||
expect(subIds).toContain("manage-policy-automations-install-software");
|
||||
expect(subIds).toContain("manage-policy-automations-run-script");
|
||||
expect(subIds).toContain("manage-policy-automations-conditional-access");
|
||||
expect(subIds).not.toContain("manage-policy-automations-calendar");
|
||||
it("includes all keywords on a specific team", () => {
|
||||
const items = buildPaletteItems({
|
||||
...BASE_CONTEXT,
|
||||
hasTeamSelected: true,
|
||||
currentTeam: { id: 1, name: "Engineering" },
|
||||
});
|
||||
const policyAutomations = items.find(
|
||||
(i) => i.id === "manage-policy-automations"
|
||||
);
|
||||
const keywords = policyAutomations?.keywords ?? [];
|
||||
expect(keywords).toContain("webhook");
|
||||
expect(keywords).toContain("calendar");
|
||||
expect(keywords).toContain("google calendar");
|
||||
expect(keywords).toContain("conditional access");
|
||||
expect(keywords).toContain("sso");
|
||||
});
|
||||
|
||||
it("excludes certificates and passwords for technicians", () => {
|
||||
|
||||
@@ -285,6 +285,7 @@ const ManagePolicyPage = ({
|
||||
const {
|
||||
data: globalPoliciesCount,
|
||||
isFetching: isFetchingGlobalCount,
|
||||
isError: isErrorGlobalPoliciesCount,
|
||||
refetch: refetchGlobalPoliciesCount,
|
||||
} = useQuery<IPoliciesCountResponse, Error, number, IPoliciesCountQueryKey[]>(
|
||||
[
|
||||
@@ -342,6 +343,7 @@ const ManagePolicyPage = ({
|
||||
const {
|
||||
data: teamPoliciesCountResponse,
|
||||
isFetching: isFetchingTeamCountMergeInherited,
|
||||
isError: isErrorTeamPoliciesCount,
|
||||
refetch: refetchTeamPoliciesCountMergeInherited,
|
||||
} = useQuery<
|
||||
IPoliciesCountResponse,
|
||||
@@ -616,6 +618,38 @@ 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
|
||||
|
||||
// Open the Manage automations modal via deep-link (e.g. from the
|
||||
// command palette). Gate on the same predicate the in-page button
|
||||
// uses — the param alone must not surface a privileged modal to
|
||||
// non-admins or when there's nothing to automate. Wait for the
|
||||
// relevant count query to settle (data OR error) so
|
||||
// `hasPoliciesToAutomate` is meaningful; then always strip the param
|
||||
// so a refresh doesn't reopen and the URL doesn't get stuck if the
|
||||
// count call errored.
|
||||
useEffect(() => {
|
||||
if (location.query.manage_automations !== "1") return;
|
||||
const countSettled = isAllTeamsSelected
|
||||
? globalPoliciesCount !== undefined || isErrorGlobalPoliciesCount
|
||||
: teamPoliciesCountResponse !== undefined || isErrorTeamPoliciesCount;
|
||||
if (!countSettled) return;
|
||||
if (canEditAutomationsSettings && hasPoliciesToAutomate) {
|
||||
setShowAutomationsModal(true);
|
||||
}
|
||||
const { manage_automations, ...rest } = location.query;
|
||||
router.replace({ pathname: location.pathname, query: rest });
|
||||
}, [
|
||||
location.query,
|
||||
location.pathname,
|
||||
router,
|
||||
canEditAutomationsSettings,
|
||||
hasPoliciesToAutomate,
|
||||
isAllTeamsSelected,
|
||||
globalPoliciesCount,
|
||||
teamPoliciesCountResponse,
|
||||
isErrorGlobalPoliciesCount,
|
||||
isErrorTeamPoliciesCount,
|
||||
]);
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user