Skip to content

Commit

Permalink
UI - Enable "No team" for Policies (#21885)
Browse files Browse the repository at this point in the history
## #21468 

- Memoize `app` and `table` context for improved stability
- Update policies page dependencies to reference now stable context
values
- Widespread updates to logic to enable No teams on the Manage polices
page, PolicyPage, and related pages and flows

_Outstanding bugs to address:_
- [x] When navigating from another page with "No team" to Policies, team
is reset to "All teams"
- [x] same after saving or editing a no-team policy_


![ezgif-4-7675c92400](https://github.com/user-attachments/assets/205cf6e4-750e-4f87-9a6b-33b6b1edb7b3)


- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Lucas Rodriguez <[email protected]>
Co-authored-by: Jacob Shandling <[email protected]>
  • Loading branch information
3 people committed Sep 16, 2024
1 parent 970b7b6 commit 549e9c8
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 267 deletions.
1 change: 1 addition & 0 deletions changes/21468-no-teams-policies
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Enable 'No teams' funcitonality for the policies page and associated workflows.
2 changes: 0 additions & 2 deletions frontend/components/TeamsDropdown/TeamsDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {

// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";

const generateDropdownOptions = (
teams: ITeamSummary[] | undefined,
Expand Down
35 changes: 7 additions & 28 deletions frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classnames from "classnames";

import { AppContext } from "context/app";
import { IConfig } from "interfaces/config";
import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
import { API_ALL_TEAMS_ID, APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
import { IUser } from "interfaces/user";
import { QueryParams } from "utilities/url";

Expand Down Expand Up @@ -54,11 +54,6 @@ const REGEX_GLOBAL_PAGES = {
PROFILE: /\/profile/i,
};

const REGEX_EXCLUDE_NO_TEAM_PAGES = {
MANAGE_POLICIES: /\/policies\/manage/i,
MANAGE_QUERIES: /\/queries\/manage/i,
};

const testDetailPage = (path: string, re: RegExp) => {
if (re === REGEX_DETAIL_PAGES.LABEL_EDIT) {
// we want to match "/labels/10" but not "/hosts/manage/labels/10"
Expand All @@ -77,12 +72,6 @@ const isGlobalPage = (path: string) => {
return Object.values(REGEX_GLOBAL_PAGES).some((re) => path.match(re));
};

const isExcludeNoTeamPage = (path: string) => {
return Object.values(REGEX_EXCLUDE_NO_TEAM_PAGES).some((re) =>
path.match(re)
);
};

const SiteTopNav = ({
config,
currentUser,
Expand All @@ -103,16 +92,13 @@ const SiteTopNav = ({
const isActiveGlobalPage = isGlobalPage(currentPath);

const currentQueryParams = { ...query };
if (
isActiveGlobalPage ||
(isActiveDetailPage && !currentPath.match(REGEX_DETAIL_PAGES.POLICY_EDIT))
) {
// detail pages (e.g., host details) and some manage pages (e.g., queries) don't have team_id
// query params that we can simply append to the top nav links so instead we need grab the team
// id from context (note that policy edit page does support team_id param so we exclude that one)
if (isActiveGlobalPage || isActiveDetailPage) {
// detail pages (e.g., host details) and some manage pages (e.g., queries) aren't guaranteed to
// have a team_id in the URL that we can simply append to the top nav links so instead we need grab the team
// id from context
currentQueryParams.team_id =
currentTeam?.id === APP_CONTEXT_ALL_TEAMS_ID
? undefined
? API_ALL_TEAMS_ID
: currentTeam?.id;
}

Expand Down Expand Up @@ -150,7 +136,7 @@ const SiteTopNav = ({
: currentPath;

const includeTeamId = (activePath: string) => {
if (currentQueryParams.team_id) {
if (currentQueryParams.team_id !== API_ALL_TEAMS_ID) {
return `${path}?team_id=${currentQueryParams.team_id}`;
}
return activePath;
Expand All @@ -174,13 +160,6 @@ const SiteTopNav = ({
);
}

if (
isExcludeNoTeamPage(navItem.location.pathname) &&
(currentQueryParams.team_id === "0" || currentQueryParams.team_id === 0)
) {
currentQueryParams.team_id = undefined;
}

return (
<li className={navItemClasses} key={`nav-item-${name}`}>
{withParams ? (
Expand Down
263 changes: 155 additions & 108 deletions frontend/context/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useReducer, ReactNode } from "react";
import React, { createContext, useReducer, useMemo, ReactNode } from "react";

import { IConfig } from "interfaces/config";
import { IEnrollSecret } from "interfaces/enroll_secret";
Expand Down Expand Up @@ -420,10 +420,9 @@ const reducer = (state: InitialStateType, action: IAction) => {
}
case ACTIONS.SET_FILTERED_POLICIES_PATH: {
const { filteredPoliciesPath } = action;
// TODO: if policies page is updated to support team_id=0, remove the replace below
return {
...state,
filteredPoliciesPath: filteredPoliciesPath.replace("team_id=0", ""),
filteredPoliciesPath,
};
}
default:
Expand All @@ -436,111 +435,159 @@ export const AppContext = createContext<InitialStateType>(initialState);
const AppProvider = ({ children }: Props): JSX.Element => {
const [state, dispatch] = useReducer(reducer, initialState);

const value = {
availableTeams: state.availableTeams,
config: state.config,
currentUser: state.currentUser,
currentTeam: state.currentTeam,
enrollSecret: state.enrollSecret,
sandboxExpiry: state.sandboxExpiry,
abmExpiry: state.abmExpiry,
apnsExpiry: state.apnsExpiry,
vppExpiry: state.vppExpiry,
isAppleBmExpired: state.isAppleBmExpired,
isApplePnsExpired: state.isApplePnsExpired,
isVppExpired: state.isVppExpired,
needsAbmTermsRenewal: state.needsAbmTermsRenewal,
willAppleBmExpire: state.willAppleBmExpire,
willApplePnsExpire: state.willApplePnsExpire,
willVppExpire: state.willVppExpire,
noSandboxHosts: state.noSandboxHosts,
filteredHostsPath: state.filteredHostsPath,
filteredSoftwarePath: state.filteredSoftwarePath,
filteredQueriesPath: state.filteredQueriesPath,
filteredPoliciesPath: state.filteredPoliciesPath,
isPreviewMode: detectPreview(),
isSandboxMode: state.isSandboxMode,
isFreeTier: state.isFreeTier,
isPremiumTier: state.isPremiumTier,
isMacMdmEnabledAndConfigured: state.isMacMdmEnabledAndConfigured,
isWindowsMdmEnabledAndConfigured: state.isWindowsMdmEnabledAndConfigured,
isGlobalAdmin: state.isGlobalAdmin,
isGlobalMaintainer: state.isGlobalMaintainer,
isGlobalObserver: state.isGlobalObserver,
isOnGlobalTeam: state.isOnGlobalTeam,
isAnyTeamObserverPlus: state.isAnyTeamObserverPlus,
isAnyTeamMaintainer: state.isAnyTeamMaintainer,
isAnyTeamMaintainerOrTeamAdmin: state.isAnyTeamMaintainerOrTeamAdmin,
isTeamObserver: state.isTeamObserver,
isTeamMaintainer: state.isTeamMaintainer,
isTeamAdmin: state.isTeamAdmin,
isTeamMaintainerOrTeamAdmin: state.isTeamMaintainerOrTeamAdmin,
isAnyTeamAdmin: state.isAnyTeamAdmin,
isOnlyObserver: state.isOnlyObserver,
isObserverPlus: state.isObserverPlus,
isNoAccess: state.isNoAccess,
setAvailableTeams: (user: IUser | null, availableTeams: ITeamSummary[]) => {
dispatch({
type: ACTIONS.SET_AVAILABLE_TEAMS,
user,
availableTeams,
});
},
setCurrentUser: (currentUser: IUser) => {
dispatch({ type: ACTIONS.SET_CURRENT_USER, currentUser });
},
setCurrentTeam: (currentTeam: ITeamSummary | undefined) => {
dispatch({ type: ACTIONS.SET_CURRENT_TEAM, currentTeam });
},
setConfig: (config: IConfig) => {
dispatch({ type: ACTIONS.SET_CONFIG, config });
},
setEnrollSecret: (enrollSecret: IEnrollSecret[]) => {
dispatch({ type: ACTIONS.SET_ENROLL_SECRET, enrollSecret });
},
setABMExpiry: (abmExpiry: IAbmExpiry) => {
dispatch({ type: ACTIONS.SET_ABM_EXPIRY, abmExpiry });
},
setAPNsExpiry: (apnsExpiry: string) => {
dispatch({ type: ACTIONS.SET_APNS_EXPIRY, apnsExpiry });
},
setVppExpiry: (vppExpiry: string) => {
dispatch({
type: ACTIONS.SET_VPP_EXPIRY,
vppExpiry,
});
},
setSandboxExpiry: (sandboxExpiry: string) => {
dispatch({ type: ACTIONS.SET_SANDBOX_EXPIRY, sandboxExpiry });
},
setNoSandboxHosts: (noSandboxHosts: boolean) => {
dispatch({
type: ACTIONS.SET_NO_SANDBOX_HOSTS,
noSandboxHosts,
});
},
setFilteredHostsPath: (filteredHostsPath: string) => {
dispatch({ type: ACTIONS.SET_FILTERED_HOSTS_PATH, filteredHostsPath });
},
setFilteredSoftwarePath: (filteredSoftwarePath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_SOFTWARE_PATH,
filteredSoftwarePath,
});
},
setFilteredQueriesPath: (filteredQueriesPath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_QUERIES_PATH,
filteredQueriesPath,
});
},
setFilteredPoliciesPath: (filteredPoliciesPath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_POLICIES_PATH,
filteredPoliciesPath,
});
},
};
const value = useMemo(
() => ({
availableTeams: state.availableTeams,
config: state.config,
currentUser: state.currentUser,
currentTeam: state.currentTeam,
enrollSecret: state.enrollSecret,
sandboxExpiry: state.sandboxExpiry,
abmExpiry: state.abmExpiry,
apnsExpiry: state.apnsExpiry,
vppExpiry: state.vppExpiry,
isAppleBmExpired: state.isAppleBmExpired,
isApplePnsExpired: state.isApplePnsExpired,
isVppExpired: state.isVppExpired,
needsAbmTermsRenewal: state.needsAbmTermsRenewal,
willAppleBmExpire: state.willAppleBmExpire,
willApplePnsExpire: state.willApplePnsExpire,
willVppExpire: state.willVppExpire,
noSandboxHosts: state.noSandboxHosts,
filteredHostsPath: state.filteredHostsPath,
filteredSoftwarePath: state.filteredSoftwarePath,
filteredQueriesPath: state.filteredQueriesPath,
filteredPoliciesPath: state.filteredPoliciesPath,
isPreviewMode: detectPreview(),
isSandboxMode: state.isSandboxMode,
isFreeTier: state.isFreeTier,
isPremiumTier: state.isPremiumTier,
isMacMdmEnabledAndConfigured: state.isMacMdmEnabledAndConfigured,
isWindowsMdmEnabledAndConfigured: state.isWindowsMdmEnabledAndConfigured,
isGlobalAdmin: state.isGlobalAdmin,
isGlobalMaintainer: state.isGlobalMaintainer,
isGlobalObserver: state.isGlobalObserver,
isOnGlobalTeam: state.isOnGlobalTeam,
isAnyTeamObserverPlus: state.isAnyTeamObserverPlus,
isAnyTeamMaintainer: state.isAnyTeamMaintainer,
isAnyTeamMaintainerOrTeamAdmin: state.isAnyTeamMaintainerOrTeamAdmin,
isTeamObserver: state.isTeamObserver,
isTeamMaintainer: state.isTeamMaintainer,
isTeamAdmin: state.isTeamAdmin,
isTeamMaintainerOrTeamAdmin: state.isTeamMaintainerOrTeamAdmin,
isAnyTeamAdmin: state.isAnyTeamAdmin,
isOnlyObserver: state.isOnlyObserver,
isObserverPlus: state.isObserverPlus,
isNoAccess: state.isNoAccess,
setAvailableTeams: (
user: IUser | null,
availableTeams: ITeamSummary[]
) => {
dispatch({
type: ACTIONS.SET_AVAILABLE_TEAMS,
user,
availableTeams,
});
},
setCurrentUser: (currentUser: IUser) => {
dispatch({ type: ACTIONS.SET_CURRENT_USER, currentUser });
},
setCurrentTeam: (currentTeam: ITeamSummary | undefined) => {
dispatch({ type: ACTIONS.SET_CURRENT_TEAM, currentTeam });
},
setConfig: (config: IConfig) => {
dispatch({ type: ACTIONS.SET_CONFIG, config });
},
setEnrollSecret: (enrollSecret: IEnrollSecret[]) => {
dispatch({ type: ACTIONS.SET_ENROLL_SECRET, enrollSecret });
},
setABMExpiry: (abmExpiry: IAbmExpiry) => {
dispatch({ type: ACTIONS.SET_ABM_EXPIRY, abmExpiry });
},
setAPNsExpiry: (apnsExpiry: string) => {
dispatch({ type: ACTIONS.SET_APNS_EXPIRY, apnsExpiry });
},
setVppExpiry: (vppExpiry: string) => {
dispatch({
type: ACTIONS.SET_VPP_EXPIRY,
vppExpiry,
});
},
setSandboxExpiry: (sandboxExpiry: string) => {
dispatch({ type: ACTIONS.SET_SANDBOX_EXPIRY, sandboxExpiry });
},
setNoSandboxHosts: (noSandboxHosts: boolean) => {
dispatch({
type: ACTIONS.SET_NO_SANDBOX_HOSTS,
noSandboxHosts,
});
},
setFilteredHostsPath: (filteredHostsPath: string) => {
dispatch({ type: ACTIONS.SET_FILTERED_HOSTS_PATH, filteredHostsPath });
},
setFilteredSoftwarePath: (filteredSoftwarePath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_SOFTWARE_PATH,
filteredSoftwarePath,
});
},
setFilteredQueriesPath: (filteredQueriesPath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_QUERIES_PATH,
filteredQueriesPath,
});
},
setFilteredPoliciesPath: (filteredPoliciesPath: string) => {
dispatch({
type: ACTIONS.SET_FILTERED_POLICIES_PATH,
filteredPoliciesPath,
});
},
}),
[
state.abmExpiry,
state.apnsExpiry,
state.availableTeams,
state.config,
state.currentTeam,
state.currentUser,
state.enrollSecret,
state.filteredHostsPath,
state.filteredPoliciesPath,
state.filteredQueriesPath,
state.filteredSoftwarePath,
state.isAnyTeamAdmin,
state.isAnyTeamMaintainer,
state.isAnyTeamMaintainerOrTeamAdmin,
state.isAnyTeamObserverPlus,
state.isAppleBmExpired,
state.isApplePnsExpired,
state.isFreeTier,
state.isGlobalAdmin,
state.isGlobalMaintainer,
state.isGlobalObserver,
state.isMacMdmEnabledAndConfigured,
state.isNoAccess,
state.isObserverPlus,
state.isOnGlobalTeam,
state.isOnlyObserver,
state.isPremiumTier,
state.isSandboxMode,
state.isTeamAdmin,
state.isTeamMaintainer,
state.isTeamMaintainerOrTeamAdmin,
state.isTeamObserver,
state.isVppExpired,
state.isWindowsMdmEnabledAndConfigured,
state.needsAbmTermsRenewal,
state.noSandboxHosts,
state.sandboxExpiry,
state.vppExpiry,
state.willAppleBmExpire,
state.willApplePnsExpire,
state.willVppExpire,
]
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

Expand Down
Loading

0 comments on commit 549e9c8

Please sign in to comment.