Update policies and queries empty states (#26124)

For #23312 

# Overview

Update to previous work on Policies page to match expedited design.

## Details

* Remove the "all teams" verbiage in free edition
* Move the CTA (Add policy / Add query) buttons to the top right (where
they are in the non-empty state)
* Make Query page match Policies page with new verbiage and button
placement
* Remove "0 queries" label in Queries page header when there are no
queries, to be consistent with Policies.

## Screenshots

Policies page on All Teams w/ no results:
<img width="1055" alt="image"
src="https://github.com/user-attachments/assets/2e25bff4-db58-448c-b573-cc55425f1e46"
/>

"Add" button moved to top right.

---

Policies page on single team w/ no results:
<img width="1073" alt="image"
src="https://github.com/user-attachments/assets/1a9d69cf-c228-44f2-825a-ceab69e62075"
/>

"Add" button moved to top right.

---

Policies page on free tier w/ no results:
<img width="1055" alt="image"
src="https://github.com/user-attachments/assets/d557f139-3890-42b8-9fc9-1f943d5a3f26"
/>

"Add" button moved to top right, language now reads "You don't have any
policies".

---

Queries page on on All Teams w/ no results:
<img width="1057" alt="image"
src="https://github.com/user-attachments/assets/fc707460-37b1-465b-8e9b-32d14cfd2287"
/>

"Add" button moved to top right, no query count, language says "You
don't have any queries that apply to all teams"

---

Queries page on on single team w/ no results:
<img width="1051" alt="image"
src="https://github.com/user-attachments/assets/9c30502c-5ecb-4473-80c5-142419c7e676"
/>

"Add" button moved to top right, no query count, language says "You
don't have any queries that apply to this team"

---

Queries page on free tier w/ no results:
<img width="1050" alt="image"
src="https://github.com/user-attachments/assets/def51c25-53ac-4ee1-ab4c-48607aaae34d"
/>

"Add" button moved to top right, no query count
This commit is contained in:
Scott Gress
2025-02-06 11:25:43 -08:00
committed by GitHub
parent f8de2d9e50
commit 3e3d19c6d4
7 changed files with 194 additions and 112 deletions
+1 -1
View File
@@ -1 +1 @@
- Clarified text on the Policies page when no policies exist for the selected team (or All Teams)
- Clarified text on the Policies and Queries pages when no policies/queries exist for the selected team (or All Teams)
@@ -754,9 +754,8 @@ const ManagePolicyPage = ({
? teamPolicies && teamPolicies.length > 0
: globalPolicies && globalPolicies.length > 0;
// Show CTA buttons if there is no errors AND there are policy results or a search filter
const showCtaButtons =
!policiesErrors && (policyResults || searchQuery !== "");
// Show CTA buttons if there are no errors
const showCtaButtons = !policiesErrors;
const automationsConfig = !isAllTeamsSelected ? teamConfig : config;
const hasPoliciesToAutomateOrDelete = policiesAvailableToAutomate.length > 0;
@@ -8,6 +8,35 @@ import createMockPolicy from "__mocks__/policyMock";
import PoliciesTable from "./PoliciesTable";
describe("Policies table", () => {
it("Renders the page-wide empty state when no policies are present (free tier)", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
render(
<PoliciesTable
policiesList={[]}
isLoading={false}
onDeletePolicyClick={noop}
currentTeam={{ id: -1, name: "All teams" }}
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={() => null}
resetPageIndex={false}
/>
);
expect(screen.getByText("You don't have any policies")).toBeInTheDocument();
expect(screen.queryByText("Name")).toBeNull();
expect(screen.queryByPlaceholderText("Search by name")).toBeNull();
});
it("Renders the page-wide empty state when no policies are present (all teams)", async () => {
const render = createCustomRenderer({
context: {
@@ -37,6 +66,7 @@ describe("Policies table", () => {
screen.getByText("You don't have any policies that apply to all teams")
).toBeInTheDocument();
expect(screen.queryByText("Name")).toBeNull();
expect(screen.queryByPlaceholderText("Search by name")).toBeNull();
});
it("Renders the page-wide empty state when no policies are present (specific team)", async () => {
@@ -96,6 +126,7 @@ describe("Policies table", () => {
);
expect(screen.getByText("No matching policies")).toBeInTheDocument();
expect(screen.queryByPlaceholderText("Search by name")).toBeInTheDocument();
expect(screen.queryByText("Name")).toBeNull();
});
@@ -62,26 +62,18 @@ const PoliciesTable = ({
"Add policies to detect device health issues and trigger automations.",
};
if (
currentTeam?.id === null ||
currentTeam?.id === APP_CONTEXT_ALL_TEAMS_ID
) {
emptyState.header += " that apply to all teams";
} else {
emptyState.header += " that apply to this team";
if (isPremiumTier) {
if (
currentTeam?.id === null ||
currentTeam?.id === APP_CONTEXT_ALL_TEAMS_ID
) {
emptyState.header += " that apply to all teams";
} else {
emptyState.header += " that apply to this team";
}
}
if (canAddOrDeletePolicy) {
emptyState.primaryButton = (
<Button
variant="brand"
className={`${baseClass}__select-policy-button`}
onClick={onAddPolicyClick}
>
Add policy
</Button>
);
} else {
if (!canAddOrDeletePolicy) {
emptyState.info = "";
}
@@ -291,6 +291,7 @@ const ManageQueriesPage = ({
router={router}
queryParams={location.query}
currentTeamId={teamIdForApi}
isPremiumTier={isPremiumTier}
/>
);
};
@@ -381,13 +382,6 @@ const ManageQueriesPage = ({
isTeamMaintainer ||
isObserverPlus; // isObserverPlus checks global and selected team
const hideQueryActions =
// there are no filters and no returned queries, indicating there are no global/team queries at all
!(!!location.query.query || !!location.query.platform) &&
!queriesResponse?.count &&
// the user has permission
(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus);
return (
<MainContent className={baseClass}>
<div className={`${baseClass}__wrapper`}>
@@ -398,7 +392,7 @@ const ManageQueriesPage = ({
</div>
</div>
{!hideQueryActions && (
{canCustomQuery && (
<div className={`${baseClass}__action-button-container`}>
{(isGlobalAdmin || isTeamAdmin) && !onlyInheritedQueries && (
<Button
@@ -143,7 +143,16 @@ const renderAsPremiumGlobalAdmin = createCustomRenderer({
},
});
describe("QueriesTable", () => {
it("Renders the page-wide empty state when no queries are present", () => {
it("Renders the page-wide empty state when no queries are present (free tier)", () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testData: IQueriesTableProps[] = [
{
queries: [],
@@ -161,14 +170,81 @@ describe("QueriesTable", () => {
];
testData.forEach((tableProps) => {
renderAsPremiumGlobalAdmin(<QueriesTable {...tableProps} />);
render(<QueriesTable {...tableProps} />);
expect(
screen.getByText("You don't have any queries")
).toBeInTheDocument();
expect(screen.queryByText("Frequency")).toBeNull();
expect(screen.queryByPlaceholderText("Search by name")).toBeNull();
});
});
it("Renders inherited global queries and team queries when viewing a team, then renders the 'no-matching' empty state when a search string is entered that matches no queries", async () => {
it("Renders the page-wide empty state when no queries are present (all teams)", () => {
const testData: IQueriesTableProps[] = [
{
queries: [],
totalQueriesCount: 0,
hasNextResults: false,
onlyInheritedQueries: false,
isLoading: false,
onDeleteQueryClick: jest.fn(),
onCreateQueryClick: jest.fn(),
isOnlyObserver: false,
isObserverPlus: false,
isAnyTeamObserverPlus: false,
currentTeamId: undefined,
isPremiumTier: true,
},
];
testData.forEach((tableProps) => {
renderAsPremiumGlobalAdmin(<QueriesTable {...tableProps} />);
expect(
screen.getByText("You don't have any queries that apply to all teams")
).toBeInTheDocument();
expect(screen.queryByText("Frequency")).toBeNull();
expect(screen.queryByPlaceholderText("Search by name")).toBeNull();
});
});
it("Renders the page-wide empty state when no queries are present (specific team)", () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testData: IQueriesTableProps[] = [
{
queries: [],
totalQueriesCount: 0,
hasNextResults: false,
onlyInheritedQueries: false,
isLoading: false,
onDeleteQueryClick: jest.fn(),
onCreateQueryClick: jest.fn(),
isOnlyObserver: false,
isObserverPlus: false,
isAnyTeamObserverPlus: false,
isPremiumTier: true,
currentTeamId: 1,
},
];
testData.forEach((tableProps) => {
renderAsPremiumGlobalAdmin(<QueriesTable {...tableProps} />);
expect(
screen.getByText("You don't have any queries that apply to this team")
).toBeInTheDocument();
expect(screen.queryByText("Frequency")).toBeNull();
expect(screen.queryByPlaceholderText("Search by name")).toBeNull();
});
});
it("Renders inherited global queries and team queries when viewing a team", async () => {
const testData: IQueriesTableProps[] = [
{
queries: [...testGlobalQueries, ...testTeamQueries],
@@ -193,26 +269,35 @@ describe("QueriesTable", () => {
"Team query 2",
];
testData.forEach(async (tableProps) => {
// will have no context to get current user from
const { user } = renderAsPremiumGlobalAdmin(
<QueriesTable {...tableProps} />
);
dataStrings.forEach((val) => {
expect(screen.getAllByText(val)[0]).toBeInTheDocument();
});
await user.type(
screen.getByPlaceholderText("Search by name"),
"shouldn't match anything"
);
expect(screen.getByText("No matching queries")).toBeInTheDocument();
dataStrings.forEach((val) => {
expect(screen.getAllByText(val)).toHaveLength(0);
});
// will have no context to get current user from
renderAsPremiumGlobalAdmin(<QueriesTable {...testData[0]} />);
dataStrings.forEach((val) => {
expect(screen.getAllByText(val)[0]).toBeInTheDocument();
});
});
it("renders the 'no-matching' empty state when a search string is entered that matches no queries", async () => {
const testData: IQueriesTableProps = {
queries: [],
totalQueriesCount: 0,
hasNextResults: false,
onlyInheritedQueries: false,
isLoading: false,
onDeleteQueryClick: jest.fn(),
onCreateQueryClick: jest.fn(),
isOnlyObserver: false,
isObserverPlus: false,
isAnyTeamObserverPlus: false,
currentTeamId: 1,
queryParams: {
query: "dont match me bro",
},
};
// will have no context to get current user from
renderAsPremiumGlobalAdmin(<QueriesTable {...testData} />);
expect(screen.getByText("No matching queries")).toBeInTheDocument();
});
it("Renders an observer can run badge and tooltip for a observer can run query", async () => {
const render = createCustomRenderer({
context: {
@@ -5,6 +5,7 @@ import { Row } from "react-table";
import { AppContext } from "context/app";
import { IEmptyTableProps } from "interfaces/empty_table";
import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
import { isQueryablePlatform, SelectedPlatform } from "interfaces/platform";
import { IEnhancedQuery } from "interfaces/schedulable_query";
import { ITableQueryData } from "components/TableContainer/TableContainer";
@@ -44,6 +45,7 @@ export interface IQueriesTableProps {
team_id?: string;
};
currentTeamId?: number;
isPremiumTier?: boolean;
}
interface IRowProps extends Row {
@@ -94,6 +96,7 @@ const QueriesTable = ({
router,
queryParams,
currentTeamId,
isPremiumTier,
}: IQueriesTableProps): JSX.Element | null => {
const { currentUser } = useContext(AppContext);
@@ -170,45 +173,39 @@ const QueriesTable = ({
]
);
const getEmptyStateParams = useCallback(() => {
const emptyParams: IEmptyTableProps = {
graphicName: "empty-queries",
header: "You don't have any queries",
};
if (searchQuery || curTargetedPlatformFilter !== "all") {
delete emptyParams.graphicName;
emptyParams.header = "No matching queries";
emptyParams.info = "No queries match the current filters.";
} else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {
emptyParams.additionalInfo = (
<>
Create a new query, or{" "}
<CustomLink
url="https://fleetdm.com/docs/using-fleet/standard-query-library"
text="import Fleet's standard query library"
newTab
/>
</>
);
emptyParams.primaryButton = (
<Button
variant="brand"
className={`${baseClass}__create-button`}
onClick={onCreateQueryClick}
>
Add query
</Button>
);
}
const emptyParams: IEmptyTableProps = {
graphicName: "empty-queries",
header: "You don't have any queries",
};
return emptyParams;
}, [
isAnyTeamObserverPlus,
isObserverPlus,
isOnlyObserver,
onCreateQueryClick,
searchQuery,
]);
if (isPremiumTier) {
if (
typeof currentTeamId === "undefined" ||
currentTeamId === null ||
currentTeamId === APP_CONTEXT_ALL_TEAMS_ID
) {
emptyParams.header += " that apply to all teams";
} else {
emptyParams.header += " that apply to this team";
}
}
if (searchQuery || curTargetedPlatformFilter !== "all") {
delete emptyParams.graphicName;
emptyParams.header = "No matching queries";
emptyParams.info = "No queries match the current filters.";
} else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {
emptyParams.additionalInfo = (
<>
Create a new query, or{" "}
<CustomLink
url="https://fleetdm.com/docs/using-fleet/standard-query-library"
text="import Fleet's standard query library"
newTab
/>
</>
);
}
const handlePlatformFilterDropdownChange = useCallback(
(selectedTargetedPlatform: SingleValue<CustomOptionType>) => {
@@ -264,26 +261,7 @@ const QueriesTable = ({
);
const searchable =
(totalQueriesCount ?? 0) > 0 ||
!!curTargetedPlatformFilter ||
!!searchQuery;
const emptyComponent = useCallback(() => {
const {
graphicName,
header,
info,
additionalInfo,
primaryButton,
} = getEmptyStateParams();
return EmptyTable({
graphicName,
header,
info,
additionalInfo,
primaryButton,
});
}, [getEmptyStateParams]);
(totalQueriesCount ?? 0) > 0 || !!targetedPlatformParam || !!searchQuery;
const trimmedSearchQuery = searchQuery.trim();
@@ -310,10 +288,13 @@ const QueriesTable = ({
variant: "text-icon",
onActionButtonClick: onDeleteQueryClick,
}}
emptyComponent={emptyComponent}
renderCount={() => (
<TableCount name="queries" count={totalQueriesCount} />
)}
emptyComponent={() => EmptyTable(emptyParams)}
renderCount={() =>
((totalQueriesCount || searchQuery) && (
<TableCount name="queries" count={totalQueriesCount} />
)) ||
null
}
inputPlaceHolder="Search by name"
onQueryChange={onQueryChange}
searchable={searchable}