mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Add new feature: Team policies (#2142)
This commit is contained in:
parent
29e79eb9bb
commit
0a423f4c7b
6 changed files with 128 additions and 137 deletions
|
|
@ -69,37 +69,20 @@ export default (currentUser) => {
|
|||
},
|
||||
];
|
||||
|
||||
const globalMaintainerNavItems = [
|
||||
{
|
||||
icon: "policies",
|
||||
name: "Policies",
|
||||
iconName: "policies",
|
||||
location: {
|
||||
regex: new RegExp(`^${URL_PREFIX}/(policies)/`),
|
||||
pathname: PATHS.MANAGE_POLICIES,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (permissionUtils.isGlobalAdmin(currentUser)) {
|
||||
return [
|
||||
...userNavItems,
|
||||
...teamMaintainerNavItems,
|
||||
...globalMaintainerNavItems,
|
||||
...policiesTab,
|
||||
...adminNavItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (permissionUtils.isGlobalMaintainer(currentUser)) {
|
||||
return [
|
||||
...userNavItems,
|
||||
...teamMaintainerNavItems,
|
||||
...globalMaintainerNavItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (permissionUtils.isAnyTeamMaintainer(currentUser)) {
|
||||
return [...userNavItems, ...teamMaintainerNavItems];
|
||||
if (
|
||||
permissionUtils.isGlobalMaintainer(currentUser) ||
|
||||
permissionUtils.isAnyTeamMaintainer(currentUser)
|
||||
) {
|
||||
return [...userNavItems, ...teamMaintainerNavItems, ...policiesTab];
|
||||
}
|
||||
|
||||
return [...userNavItems, ...policiesTab];
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ interface IManageHostsProps {
|
|||
interface ILabelsResponse {
|
||||
labels: ILabel[];
|
||||
}
|
||||
interface IPolicyResponse {
|
||||
policy: IPolicy;
|
||||
}
|
||||
|
||||
interface ITeamsResponse {
|
||||
teams: ITeam[];
|
||||
|
|
@ -222,7 +225,7 @@ const ManageHostsPage = ({
|
|||
}
|
||||
);
|
||||
|
||||
useQuery<IPolicy, Error>(
|
||||
useQuery<IPolicyResponse, Error>(
|
||||
["policy"],
|
||||
() => {
|
||||
const request = currentTeam
|
||||
|
|
@ -232,8 +235,8 @@ const ManageHostsPage = ({
|
|||
},
|
||||
{
|
||||
enabled: !!policyId,
|
||||
onSuccess: ({ query_name }) => {
|
||||
setPolicyName(query_name);
|
||||
onSuccess: ({ policy }) => {
|
||||
setPolicyName(policy.query_name);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,13 +37,16 @@ const baseClass = "manage-policies-page";
|
|||
const DOCS_LINK =
|
||||
"https://fleetdm.com/docs/deploying/configuration#osquery_detail_update_interval";
|
||||
|
||||
const INHERITED_POLICIES_COUNT_HTML = (
|
||||
<span>
|
||||
{" "}
|
||||
inherited from{" "}
|
||||
<span className={`${baseClass}__vibrant-blue`}>All teams policy</span>
|
||||
</span>
|
||||
);
|
||||
const renderInheritedPoliciesButtonText = (
|
||||
showPolicies: boolean,
|
||||
policies: IPolicy[]
|
||||
) => {
|
||||
const count = policies.length;
|
||||
|
||||
return `${showPolicies ? "Hide" : "Show"} ${count} inherited ${
|
||||
count > 1 ? "policies" : "policy"
|
||||
}`;
|
||||
};
|
||||
|
||||
const ManagePolicyPage = (managePoliciesPageProps: {
|
||||
router: any;
|
||||
|
|
@ -150,16 +153,15 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
[getGlobalPolicies, getTeamPolicies]
|
||||
);
|
||||
|
||||
const handleChangeSelectedTeam = useCallback(
|
||||
(id: number) => {
|
||||
const { MANAGE_POLICIES } = PATHS;
|
||||
const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES;
|
||||
router.replace(path);
|
||||
setShowInheritedPolicies(false);
|
||||
setSelectedPolicyIds([]);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
const handleChangeSelectedTeam = (id: number) => {
|
||||
const { MANAGE_POLICIES } = PATHS;
|
||||
const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES;
|
||||
router.replace(path);
|
||||
setShowInheritedPolicies(false);
|
||||
setSelectedPolicyIds([]);
|
||||
setGlobalPolicies([]);
|
||||
setTeamPolicies([]);
|
||||
};
|
||||
|
||||
const toggleAddPolicyModal = () => setShowAddPolicyModal(!showAddPolicyModal);
|
||||
|
||||
|
|
@ -240,8 +242,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
unsortedTeams = currentUser.teams;
|
||||
}
|
||||
if (unsortedTeams !== null) {
|
||||
const sortedTeams = unsortedTeams.sort(
|
||||
(a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name) // TODO: confirm that sortUtils refactor in #2105 has been merged first
|
||||
const sortedTeams = unsortedTeams.sort((a, b) =>
|
||||
sortUtils.caseInsensitiveAsc(a.name, b.name)
|
||||
);
|
||||
setUserTeams(sortedTeams);
|
||||
}
|
||||
|
|
@ -280,7 +282,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
// so a falsiness check cannot be used here. Null case here allows us to skip API call
|
||||
// that would be triggered on a change to selectedTeamId.
|
||||
teamId !== null && setSelectedTeamId(teamId);
|
||||
}, [handleChangeSelectedTeam, isOnGlobalTeam, location, userTeams]);
|
||||
}, [isOnGlobalTeam, location, userTeams]);
|
||||
|
||||
// Watch for selected team changes and call getPolicies to make new policies API request.
|
||||
useEffect(() => {
|
||||
|
|
@ -294,7 +296,13 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
getTeamPolicies(selectedTeamId);
|
||||
}
|
||||
}
|
||||
}, [getGlobalPolicies, getTeamPolicies, isOnGlobalTeam, selectedTeamId]);
|
||||
}, [
|
||||
getGlobalPolicies,
|
||||
getTeamPolicies,
|
||||
isAnyTeamMaintainer,
|
||||
isOnGlobalTeam,
|
||||
selectedTeamId,
|
||||
]);
|
||||
|
||||
// Pull osquery detail update interval value from config, reformat, and set as updateInterval.
|
||||
useEffect(() => {
|
||||
|
|
@ -305,6 +313,28 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
}
|
||||
}, [config]);
|
||||
|
||||
// If the user is free tier or if there is no selected team, we show the default description.
|
||||
// We also want to check selectTeamId for the null case so that we don't render the element prematurely.
|
||||
const showDefaultDescription =
|
||||
isFreeTier || (isPremiumTier && !selectedTeamId && selectedTeamId !== null);
|
||||
|
||||
// If there aren't any policies of if there are loading errors, we don't show the update interval info banner.
|
||||
// We also want to check selectTeamId for the null case so that we don't render the element prematurely.
|
||||
const showInfoBanner =
|
||||
(selectedTeamId && !isTeamPoliciesError && !!teamPolicies?.length) ||
|
||||
(!selectedTeamId &&
|
||||
selectedTeamId !== null &&
|
||||
!isGlobalPoliciesError &&
|
||||
!!globalPolicies?.length);
|
||||
|
||||
// If there aren't any policies of if there are loading errors, we don't show the inherited policies button.
|
||||
const showInheritedPoliciesButton =
|
||||
!!selectedTeamId &&
|
||||
!!teamPolicies?.length &&
|
||||
!!globalPolicies?.length &&
|
||||
!isGlobalPoliciesError &&
|
||||
!isTeamPoliciesError;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
|
|
@ -344,35 +374,29 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
.
|
||||
</p>
|
||||
)}
|
||||
{isFreeTier ||
|
||||
(isPremiumTier && !selectedTeamId && selectedTeamId !== null && (
|
||||
<p>
|
||||
Add policies for <b>all of your hosts</b> to see which pass your
|
||||
organization’s standards.{" "}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{updateInterval &&
|
||||
((selectedTeamId && !isTeamPoliciesError && !!teamPolicies?.length) ||
|
||||
(!selectedTeamId &&
|
||||
selectedTeamId !== null &&
|
||||
!isGlobalPoliciesError &&
|
||||
!!globalPolicies?.length)) && (
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your policies are checked every <b>{updateInterval.trim()}</b>.{" "}
|
||||
{isGlobalAdmin && (
|
||||
<span>
|
||||
Check out the Fleet documentation on{" "}
|
||||
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
|
||||
<b>how to edit this frequency</b>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</InfoBanner>
|
||||
{showDefaultDescription && (
|
||||
<p>
|
||||
Add policies for <b>all of your hosts</b> to see which pass your
|
||||
organization’s standards.{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!!updateInterval && showInfoBanner && (
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your policies are checked every <b>{updateInterval.trim()}</b>.{" "}
|
||||
{isGlobalAdmin && (
|
||||
<span>
|
||||
Check out the Fleet documentation on{" "}
|
||||
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
|
||||
<b>how to edit this frequency</b>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</InfoBanner>
|
||||
)}
|
||||
<div>
|
||||
{!!selectedTeamId &&
|
||||
(isTeamPoliciesError ? (
|
||||
|
|
@ -407,59 +431,48 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
{!!selectedTeamId &&
|
||||
!!teamPolicies?.length &&
|
||||
!!globalPolicies?.length &&
|
||||
!isGlobalPoliciesError &&
|
||||
!isTeamPoliciesError && (
|
||||
<span>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${showInheritedPolicies ? "upcarat" : "rightcarat"}
|
||||
{showInheritedPoliciesButton && (
|
||||
<span>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${showInheritedPolicies ? "upcarat" : "rightcarat"}
|
||||
${baseClass}__inherited-policies-button`}
|
||||
onClick={toggleShowInheritedPolicies}
|
||||
>
|
||||
{`${showInheritedPolicies ? "Hide" : "Show"} ${
|
||||
globalPolicies.length
|
||||
} inherited ${
|
||||
globalPolicies.length > 1 ? "policies" : "policy"
|
||||
}`}
|
||||
</Button>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<IconToolTip
|
||||
isHtml
|
||||
text={
|
||||
"\
|
||||
onClick={toggleShowInheritedPolicies}
|
||||
>
|
||||
{renderInheritedPoliciesButtonText(
|
||||
showInheritedPolicies,
|
||||
globalPolicies
|
||||
)}
|
||||
</Button>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<IconToolTip
|
||||
isHtml
|
||||
text={
|
||||
"\
|
||||
<center><p>“All teams” policies are checked <br/> for this team’s hosts.</p></center>\
|
||||
"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
{!!selectedTeamId &&
|
||||
!!teamPolicies?.length &&
|
||||
!!globalPolicies?.length &&
|
||||
!isGlobalPoliciesError &&
|
||||
!isTeamPoliciesError &&
|
||||
showInheritedPolicies && (
|
||||
<div className={`${baseClass}__inherited-policies-table`}>
|
||||
<PoliciesListWrapper
|
||||
isLoading={isLoadingGlobalPolicies}
|
||||
policiesList={globalPolicies}
|
||||
onRemovePoliciesClick={noop}
|
||||
toggleAddPolicyModal={noop}
|
||||
resultsTitle="policies"
|
||||
resultsHtml={INHERITED_POLICIES_COUNT_HTML}
|
||||
selectedTeamId={null}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy(
|
||||
currentUser,
|
||||
selectedTeamId
|
||||
)}
|
||||
tableType="inheritedPolicies"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{showInheritedPoliciesButton && showInheritedPolicies && (
|
||||
<div className={`${baseClass}__inherited-policies-table`}>
|
||||
<PoliciesListWrapper
|
||||
isLoading={isLoadingGlobalPolicies}
|
||||
policiesList={globalPolicies}
|
||||
onRemovePoliciesClick={noop}
|
||||
toggleAddPolicyModal={noop}
|
||||
resultsTitle="policies"
|
||||
selectedTeamId={null}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy(
|
||||
currentUser,
|
||||
selectedTeamId
|
||||
)}
|
||||
tableType="inheritedPolicies"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAddPolicyModal && (
|
||||
<AddPolicyModal
|
||||
onCancel={toggleAddPolicyModal}
|
||||
|
|
|
|||
|
|
@ -110,11 +110,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.data-table-container {
|
||||
padding-top: $pad-small;
|
||||
margin-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__inherited-policies-button {
|
||||
padding-top: $pad-small;
|
||||
margin: $pad-large 0 0 0;
|
||||
|
|
@ -175,6 +170,10 @@
|
|||
th {
|
||||
border-right: 1px solid #e2e4ea !important;
|
||||
}
|
||||
|
||||
.table-container__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ interface IPoliciesListWrapperProps {
|
|||
onRemovePoliciesClick: (selectedTableIds: number[]) => void;
|
||||
toggleAddPolicyModal: () => void;
|
||||
resultsTitle?: string;
|
||||
resultsHtml?: JSX.Element;
|
||||
selectedTeamId?: number | undefined | null;
|
||||
selectedTeamId?: number | null;
|
||||
canAddOrRemovePolicy?: boolean;
|
||||
tableType?: string;
|
||||
}
|
||||
|
|
@ -28,7 +27,6 @@ const PoliciesListWrapper = (props: IPoliciesListWrapperProps): JSX.Element => {
|
|||
onRemovePoliciesClick,
|
||||
toggleAddPolicyModal,
|
||||
resultsTitle,
|
||||
resultsHtml,
|
||||
selectedTeamId,
|
||||
canAddOrRemovePolicy,
|
||||
tableType,
|
||||
|
|
@ -69,7 +67,6 @@ const PoliciesListWrapper = (props: IPoliciesListWrapperProps): JSX.Element => {
|
|||
>
|
||||
<TableContainer
|
||||
resultsTitle={resultsTitle || "policies"}
|
||||
resultsHtml={resultsHtml}
|
||||
columns={generateTableHeaders({
|
||||
selectedTeamId,
|
||||
showSelectionColumn: canAddOrRemovePolicy,
|
||||
|
|
@ -89,6 +86,7 @@ const PoliciesListWrapper = (props: IPoliciesListWrapperProps): JSX.Element => {
|
|||
primarySelectActionButtonText={"Remove"}
|
||||
emptyComponent={NoPolicies}
|
||||
onQueryChange={noop}
|
||||
disableCount={tableType === "inheritedPolicies"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -126,11 +126,6 @@ const routes = (
|
|||
<Route path="edit" component={EditPackPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="policies" component={PoliciesPageWrapper}>
|
||||
<Route path="manage" component={ManagePoliciesPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route component={AuthAnyMaintainerGlobalAdminRoutes}>
|
||||
<Route path="schedule" component={SchedulePageWrapper}>
|
||||
<Route path="manage" component={ManageSchedulePage} />
|
||||
<Route
|
||||
|
|
|
|||
Loading…
Reference in a new issue