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:
@@ -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;
|
||||
|
||||
+31
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
+10
-18
@@ -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
|
||||
|
||||
+105
-20
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user