Host Details Page: Render better messaging for various empty states (#5294)

This commit is contained in:
RachelElysia 2022-04-26 14:00:47 -04:00 committed by GitHub
parent f806cbc638
commit 6917331a1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 162 additions and 136 deletions

View file

@ -0,0 +1 @@
* Improve empty state messages on host details page

View file

@ -9,12 +9,14 @@ import classnames from "classnames";
import { pick } from "lodash";
import PATHS from "router/paths";
import configAPI from "services/entities/config";
import hostAPI from "services/entities/hosts";
import queryAPI from "services/entities/queries";
import teamAPI from "services/entities/teams";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import {
IHost,
IDeviceMappingResponse,
@ -204,6 +206,14 @@ const HostDetailsPage = ({
}
);
const { data: hostSettings } = useQuery<
IConfig,
Error,
{ enable_host_users: boolean; enable_software_inventory: boolean }
>(["config"], () => configAPI.loadAll(), {
select: (data: IConfig) => data.host_settings,
});
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
macadmins !== null && refetchMacadmins();
@ -582,10 +592,15 @@ const HostDetailsPage = ({
usersState={usersState}
isLoading={isLoadingHost}
onUsersTableSearchChange={onUsersTableSearchChange}
hostUsersEnabled={hostSettings?.enable_host_users}
/>
</TabPanel>
<TabPanel>
<SoftwareCard isLoading={isLoadingHost} software={hostSoftware} />
<SoftwareCard
isLoading={isLoadingHost}
software={hostSoftware}
softwareInventoryEnabled={hostSettings?.enable_software_inventory}
/>
</TabPanel>
<TabPanel>
<ScheduleCard

View file

@ -0,0 +1,85 @@
import React from "react";
import ExternalLinkIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
import DisableIcon from "../../../../../../assets/images/icon-action-disable-red-16x16@2x.png";
const baseClass = "empty-state";
interface IEmptyStateProps {
title: "software" | "users";
reason?: "empty-search" | "disabled";
}
const EmptyState = ({ title, reason }: IEmptyStateProps): JSX.Element => {
const formalTitle = () => {
switch (title) {
case "software":
return "Software inventory";
case "users":
return "User collection";
default:
return "Data collection";
}
};
switch (reason) {
case "empty-search":
return (
<div className={`${baseClass} empty-${title} empty-search`}>
<div className={`${baseClass}__inner`}>
<div className={`${baseClass}__empty-filter-results`}>
<h1>No {title} matched your search criteria.</h1>
<p>Try a different search.</p>
</div>
</div>
</div>
);
case "disabled":
return (
<div className={`${baseClass} empty-${title}`}>
<div className={`${baseClass}__inner`}>
<div className={`${baseClass}__disabled`}>
<h1>{formalTitle()} has been disabled.</h1>
<p>
Check out the Fleet documentation for{" "}
<a
href="https://fleetdm.com/docs/using-fleet/configuration-files#host-settings"
target="_blank"
rel="noopener noreferrer"
>
steps to enable this feature
<img alt="External link" src={ExternalLinkIcon} />
</a>
</p>
</div>
</div>
</div>
);
default:
return (
<div className={`${baseClass} empty-${title}`}>
<div className={`${baseClass}__inner`}>
<div className={`${baseClass}__empty-list`}>
<h1>
<img alt="Disable icon" src={DisableIcon} />
No {title} collected from this host.
</h1>
<p>
Check out the Fleet documentation on{" "}
<a
href="https://fleetdm.com/docs/using-fleet/faq#why-is-my-host-not-returning-vitals"
target="_blank"
rel="noopener noreferrer"
>
how to troubleshoot{" "}
<img alt="External link" src={ExternalLinkIcon} />
</a>
</p>
</div>
</div>
</div>
);
}
};
export default EmptyState;

View file

@ -1,23 +1,22 @@
.empty-users {
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 80px;
margin-bottom: 80px;
align-items: flex-start;
margin: 0;
&__inner {
display: flex;
flex-direction: row;
h1 {
font-size: $small;
font-size: $x-small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
img {
width: 176px;
margin-right: 34px;
margin-right: $pad-small;
}
p {
@ -36,13 +35,12 @@
font-size: $x-small;
font-weight: $bold;
text-decoration: none;
margin-left: $pad-xsmall;
}
img {
width: 12px;
height: 12px;
margin-left: 6px;
margin-left: 4px;
}
}
@ -52,3 +50,8 @@
width: 350px;
}
}
.empty-search {
margin-top: $pad-small;
align-items: center;
}

View file

@ -0,0 +1 @@
export { default } from "./EmptyState";

View file

@ -1,18 +0,0 @@
import React from "react";
const baseClass = "empty-software";
const EmptySoftware = (): JSX.Element => {
return (
<div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}>
<div className={`${baseClass}__empty-filter-results`}>
<h1>No software matched your search criteria.</h1>
<p>Try a different search.</p>
</div>
</div>
</div>
);
};
export default EmptySoftware;

View file

@ -1,54 +0,0 @@
.empty-software {
display: flex;
flex-direction: column;
align-items: center;
margin-top: $pad-xxxlarge;
margin-bottom: $pad-xxxlarge;
&__inner {
display: flex;
flex-direction: row;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
img {
width: 176px;
margin-right: 34px;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
}
p.learn-more {
margin-top: $pad-medium;
}
a {
color: $core-vibrant-blue;
font-size: $x-small;
font-weight: $bold;
text-decoration: none;
margin-left: $pad-xsmall;
}
img {
width: 12px;
height: 12px;
margin-left: 6px;
}
}
&__empty-filter-results {
display: flex;
flex-direction: column;
width: 350px;
}
}

View file

@ -1 +0,0 @@
export { default } from "./EmptySoftware";

View file

@ -8,7 +8,7 @@ import { VULNERABLE_DROPDOWN_OPTIONS } from "utilities/constants";
import Dropdown from "components/forms/fields/Dropdown";
import TableContainer from "components/TableContainer";
import EmptySoftware from "./EmptySoftware";
import EmptyState from "../EmptyState";
import SoftwareVulnCount from "./SoftwareVulnCount";
import generateSoftwareTableHeaders from "./SoftwareTableConfig";
@ -23,12 +23,14 @@ interface ISoftwareTableProps {
isLoading: boolean;
software: ISoftware[];
deviceUser?: boolean;
softwareInventoryEnabled?: boolean;
}
const SoftwareTable = ({
isLoading,
software,
deviceUser,
softwareInventoryEnabled,
}: ISoftwareTableProps): JSX.Element => {
const tableSoftware: ITableSoftware[] = software.map((s) => {
return {
@ -76,6 +78,19 @@ const SoftwareTable = ({
const tableHeaders = generateSoftwareTableHeaders(deviceUser);
const EmptySoftwareSearch = () => (
<EmptyState title="software" reason="empty-search" />
);
if (!softwareInventoryEnabled) {
return (
<div className="section section--software">
<p className="section__header">Software</p>
<EmptyState title="software" reason="disabled" />
</div>
);
}
return (
<div className="section section--software">
<p className="section__header">Software</p>
@ -101,7 +116,7 @@ const SoftwareTable = ({
}
onQueryChange={onQueryChange}
resultsTitle={"software items"}
emptyComponent={EmptySoftware}
emptyComponent={EmptySoftwareSearch}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable
@ -113,15 +128,7 @@ const SoftwareTable = ({
)}
</>
) : (
<div className="results">
<p className="results__header">
No installed software detected on this host.
</p>
<p className="results__data">
Expecting to see software? Try again in a few seconds as the system
catches up.
</p>
</div>
<EmptyState title="software" />
)}
</div>
);

View file

@ -1,18 +0,0 @@
import React from "react";
const baseClass = "empty-users";
const EmptyUsers = (): JSX.Element => {
return (
<div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}>
<div className={`${baseClass}__empty-filter-results`}>
<h1>No users matched your search criteria.</h1>
<p>Try a different search.</p>
</div>
</div>
</div>
);
};
export default EmptyUsers;

View file

@ -1 +0,0 @@
export { default } from "./EmptyUsers";

View file

@ -4,7 +4,7 @@ import { IHostUser } from "interfaces/host_users";
import TableContainer from "components/TableContainer";
import generateUsersTableHeaders from "./UsersTable/UsersTableConfig";
import EmptyUsers from "./EmptyUsers";
import EmptyState from "../EmptyState";
interface ISearchQueryData {
searchQuery: string;
@ -19,6 +19,7 @@ interface IUsersProps {
usersState: { username: string }[];
isLoading: boolean;
onUsersTableSearchChange: (queryData: ISearchQueryData) => void;
hostUsersEnabled?: boolean;
}
const Users = ({
@ -26,34 +27,19 @@ const Users = ({
usersState,
isLoading,
onUsersTableSearchChange,
hostUsersEnabled,
}: IUsersProps): JSX.Element => {
const tableHeaders = generateUsersTableHeaders();
if (users) {
const EmptyUserSearch = () => (
<EmptyState title="users" reason="empty-search" />
);
if (!hostUsersEnabled) {
return (
<div className="section section--users">
<p className="section__header">Users</p>
{users.length === 0 ? (
<p className="results__data">No users were detected on this host.</p>
) : (
<TableContainer
columns={tableHeaders}
data={usersState}
isLoading={isLoading}
defaultSortHeader={"username"}
defaultSortDirection={"asc"}
inputPlaceHolder={"Search users by username"}
onQueryChange={onUsersTableSearchChange}
resultsTitle={"users"}
emptyComponent={EmptyUsers}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable
wideSearch
filteredCount={usersState.length}
isClientSidePagination
/>
)}
<EmptyState title="users" reason="disabled" />
</div>
);
}
@ -61,7 +47,27 @@ const Users = ({
return (
<div className="section section--users">
<p className="section__header">Users</p>
<p className="results__data">No users were detected on this host.</p>
{users?.length ? (
<TableContainer
columns={tableHeaders}
data={usersState}
isLoading={isLoading}
defaultSortHeader={"username"}
defaultSortDirection={"asc"}
inputPlaceHolder={"Search users by username"}
onQueryChange={onUsersTableSearchChange}
resultsTitle={"users"}
emptyComponent={EmptyUserSearch}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable
wideSearch
filteredCount={usersState.length}
isClientSidePagination
/>
) : (
<EmptyState title="users" />
)}
</div>
);
};