Add policies to my device page (#5787)

This commit is contained in:
Luke Heath 2022-05-18 10:27:03 -05:00 committed by GitHub
parent 894fa22c71
commit c52604cfb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 94 additions and 28 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

View File

@ -0,0 +1 @@
- Added policies to device user page for premium users

View File

@ -38,7 +38,7 @@
} }
&--no:before { &--no:before {
padding-right: 8px; padding-right: 8px;
content: url(../assets/images/icon-exclamation-circle-red-16x16@2x.png); content: url(../assets/images/icon-action-fail-16x16@2x.png);
transform: translate(-4px, -6px) scale(0.5); transform: translate(-4px, -6px) scale(0.5);
} }
&--indeterminate { &--indeterminate {

View File

@ -7,10 +7,12 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import classnames from "classnames"; import classnames from "classnames";
import { pick } from "lodash"; import { pick } from "lodash";
import { AppContext, IAppContext } from "context/app";
import { NotificationContext } from "context/notification"; import { NotificationContext } from "context/notification";
import deviceUserAPI from "services/entities/device_user"; import deviceUserAPI from "services/entities/device_user";
import { IHost, IDeviceMappingResponse } from "interfaces/host"; import { IHost, IDeviceMappingResponse } from "interfaces/host";
import { ISoftware } from "interfaces/software"; import { ISoftware } from "interfaces/software";
import { IHostPolicy } from "interfaces/policy";
import PageError from "components/DataError"; import PageError from "components/DataError";
// @ts-ignore // @ts-ignore
import OrgLogoIcon from "components/icons/OrgLogoIcon"; import OrgLogoIcon from "components/icons/OrgLogoIcon";
@ -22,10 +24,12 @@ import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers";
import HostSummaryCard from "../cards/HostSummary"; import HostSummaryCard from "../cards/HostSummary";
import AboutCard from "../cards/About"; import AboutCard from "../cards/About";
import SoftwareCard from "../cards/Software"; import SoftwareCard from "../cards/Software";
import PoliciesCard from "../cards/Policies";
import InfoModal from "./InfoModal"; import InfoModal from "./InfoModal";
import InfoIcon from "../../../../../assets/images/icon-info-purple-14x14@2x.png"; import InfoIcon from "../../../../../assets/images/icon-info-purple-14x14@2x.png";
import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png"; import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png";
import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal";
const baseClass = "device-user"; const baseClass = "device-user";
@ -43,6 +47,7 @@ const DeviceUserPage = ({
}: IDeviceUserPageProps): JSX.Element => { }: IDeviceUserPageProps): JSX.Element => {
const deviceAuthToken = device_auth_token; const deviceAuthToken = device_auth_token;
const { renderFlash } = useContext(NotificationContext); const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);
const handlePageError = useErrorHandler(); const handlePageError = useErrorHandler();
const [showInfoModal, setShowInfoModal] = useState<boolean>(false); const [showInfoModal, setShowInfoModal] = useState<boolean>(false);
@ -52,6 +57,12 @@ const DeviceUserPage = ({
const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]); const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]);
const [host, setHost] = useState<IHost | null>(); const [host, setHost] = useState<IHost | null>();
const [orgLogoURL, setOrgLogoURL] = useState<string>(""); const [orgLogoURL, setOrgLogoURL] = useState<string>("");
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
null
);
const [showPolicyDetailsModal, setShowPolicyDetailsModal] = useState<boolean>(
false
);
const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery( const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery(
["deviceMapping", deviceAuthToken], ["deviceMapping", deviceAuthToken],
@ -88,15 +99,7 @@ const DeviceUserPage = ({
onSuccess: (returnedHost) => { onSuccess: (returnedHost) => {
setShowRefetchSpinner(returnedHost.host.refetch_requested); setShowRefetchSpinner(returnedHost.host.refetch_requested);
if (returnedHost.host.refetch_requested) { if (returnedHost.host.refetch_requested) {
// If the API reports that a Fleet refetch request is pending, we want to check back for fresh
// host details. Here we set a one second timeout and poll the API again using
// fullyReloadHost. We will repeat this process with each onSuccess cycle for a total of
// 60 seconds or until the API reports that the Fleet refetch request has been resolved
// or that the host has gone offline.
if (!refetchStartTime) { if (!refetchStartTime) {
// If our 60 second timer wasn't already started (e.g., if a refetch was pending when
// the first page loads), we start it now if the host is online. If the host is offline,
// we skip the refetch on page load.
if (returnedHost.host.status === "online") { if (returnedHost.host.status === "online") {
setRefetchStartTime(Date.now()); setRefetchStartTime(Date.now());
setTimeout(() => { setTimeout(() => {
@ -129,7 +132,7 @@ const DeviceUserPage = ({
setShowRefetchSpinner(false); setShowRefetchSpinner(false);
} }
} }
return; // exit early because refectch is pending so we can avoid unecessary steps below return;
} }
setHostSoftware(returnedHost.host.software); setHostSoftware(returnedHost.host.software);
setHost(returnedHost.host); setHost(returnedHost.host);
@ -170,11 +173,20 @@ const DeviceUserPage = ({
setShowInfoModal(!showInfoModal); setShowInfoModal(!showInfoModal);
}, [showInfoModal, setShowInfoModal]); }, [showInfoModal, setShowInfoModal]);
const togglePolicyDetailsModal = useCallback(
(policy: IHostPolicy) => {
setShowPolicyDetailsModal(!showPolicyDetailsModal);
setSelectedPolicy(policy);
},
[showPolicyDetailsModal, setShowPolicyDetailsModal, setSelectedPolicy]
);
const onCancelPolicyDetailsModal = useCallback(() => {
setShowPolicyDetailsModal(!showPolicyDetailsModal);
setSelectedPolicy(null);
}, [showPolicyDetailsModal, setShowPolicyDetailsModal, setSelectedPolicy]);
const onRefetchHost = async () => { const onRefetchHost = async () => {
if (host) { if (host) {
// Once the user clicks to refetch, the refetch loading spinner should continue spinning
// unless there is an error. The spinner state is also controlled in the fullyReloadHost
// method.
setShowRefetchSpinner(true); setShowRefetchSpinner(true);
try { try {
await deviceUserAPI.refetch(deviceAuthToken).then(() => { await deviceUserAPI.refetch(deviceAuthToken).then(() => {
@ -219,6 +231,7 @@ const DeviceUserPage = ({
showRefetchSpinner={showRefetchSpinner} showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost} onRefetchHost={onRefetchHost}
renderActionButtons={renderActionButtons} renderActionButtons={renderActionButtons}
isPremiumTier
deviceUser deviceUser
/> />
<TabsWrapper> <TabsWrapper>
@ -226,6 +239,18 @@ const DeviceUserPage = ({
<TabList> <TabList>
<Tab>Details</Tab> <Tab>Details</Tab>
<Tab>Software</Tab> <Tab>Software</Tab>
{isPremiumTier && (
<Tab>
<div>
{titleData.issues.failing_policies_count > 0 && (
<span className="fail-count">
{titleData.issues.failing_policies_count}
</span>
)}
Policies
</div>
</Tab>
)}
</TabList> </TabList>
<TabPanel> <TabPanel>
<AboutCard <AboutCard
@ -242,11 +267,27 @@ const DeviceUserPage = ({
deviceUser deviceUser
/> />
</TabPanel> </TabPanel>
{isPremiumTier && (
<TabPanel>
<PoliciesCard
policies={host?.policies || []}
isLoading={isLoadingHost}
deviceUser
togglePolicyDetailsModal={togglePolicyDetailsModal}
/>
</TabPanel>
)}
</Tabs> </Tabs>
</TabsWrapper> </TabsWrapper>
{showInfoModal && <InfoModal onCancel={toggleInfoModal} />} {showInfoModal && <InfoModal onCancel={toggleInfoModal} />}
</div> </div>
)} )}
{!!host && showPolicyDetailsModal && (
<PolicyDetailsModal
onCancel={onCancelPolicyDetailsModal}
policy={selectedPolicy}
/>
)}
</div> </div>
); );
}; };

View File

@ -275,6 +275,20 @@
.section { .section {
margin-top: $pad-medium; margin-top: $pad-medium;
} }
.fail-count {
background-color: $ui-error;
border-radius: 45%;
color: $core-white;
display: inline-block;
padding: 0 10px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.75;
margin: 0 8px -8px 0;
position: relative;
top: -1px;
}
} }
.col-50 { .col-50 {

View File

@ -158,9 +158,9 @@ const HostSummary = ({
{titleData.status} {titleData.status}
</span> </span>
</div> </div>
{!deviceUser && {titleData.issues?.total_issues_count > 0 && deviceUser
titleData.issues?.total_issues_count > 0 && ? isPremiumTier && renderIssues()
renderIssues()} : renderIssues()}
{!deviceUser && isPremiumTier && renderHostTeam()} {!deviceUser && isPremiumTier && renderHostTeam()}
<div className="info-flex__item info-flex__item--title"> <div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Disk space</span> <span className="info-flex__header">Disk space</span>

View File

@ -7,7 +7,6 @@ import { IHostPolicy } from "interfaces/policy";
import { PolicyResponse } from "utilities/constants"; import { PolicyResponse } from "utilities/constants";
import Chevron from "../../../../../../../assets/images/icon-chevron-right-9x6@2x.png"; import Chevron from "../../../../../../../assets/images/icon-chevron-right-9x6@2x.png";
import ArrowIcon from "../../../../../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
const TAGGED_TEMPLATES = { const TAGGED_TEMPLATES = {
hostsByPolicyRoute: (policyId: number, policyResponse: PolicyResponse) => { hostsByPolicyRoute: (policyId: number, policyResponse: PolicyResponse) => {

View File

@ -26,8 +26,7 @@ const PolicyDetailsModal = ({
{policy?.resolution && ( {policy?.resolution && (
<div className={`${baseClass}__resolution`}> <div className={`${baseClass}__resolution`}>
<span className={`${baseClass}__resolve-header`}> Resolve:</span> <span className={`${baseClass}__resolve-header`}> Resolve:</span>
<br /> <p>{policy?.resolution}</p>
{policy?.resolution}
</div> </div>
)} )}
<div className={`${baseClass}__btn-wrap`}> <div className={`${baseClass}__btn-wrap`}>

View File

@ -18,4 +18,8 @@
&__btn { &__btn {
margin-left: 12px; margin-left: 12px;
} }
.policy-details-modal__resolution {
overflow: hidden;
}
} }

View File

@ -22,7 +22,8 @@ const PolicyFailingCount = (policyProps: {
{failCount === 1 ? " 1 policy" : ` ${failCount} policies`} {failCount === 1 ? " 1 policy" : ` ${failCount} policies`}
</div> </div>
<p> <p>
Click a policy below to see steps for resolving the failure Click a policy below to see if there are steps you can take to resolve
the issue
{failCount > 1 ? "s" : ""}. {failCount > 1 ? "s" : ""}.
</p> </p>
</div> </div>

View File

@ -25,7 +25,6 @@
display: flex; display: flex;
align-content: center; align-content: center;
align-items: center; align-items: center;
font-size: $small;
font-weight: $bold; font-weight: $bold;
} }
} }

View File

@ -13,12 +13,14 @@ import { isValidPolicyResponse } from "../../../ManageHostsPage/helpers";
interface IPoliciesProps { interface IPoliciesProps {
policies: IHostPolicy[]; policies: IHostPolicy[];
isLoading: boolean; isLoading: boolean;
deviceUser?: boolean;
togglePolicyDetailsModal: (policy: IHostPolicy) => void; togglePolicyDetailsModal: (policy: IHostPolicy) => void;
} }
const Policies = ({ const Policies = ({
policies, policies,
isLoading, isLoading,
deviceUser,
togglePolicyDetailsModal, togglePolicyDetailsModal,
}: IPoliciesProps): JSX.Element => { }: IPoliciesProps): JSX.Element => {
if (policies.length === 0) { if (policies.length === 0) {
@ -26,9 +28,13 @@ const Policies = ({
<div className="section section--policies"> <div className="section section--policies">
<p className="section__header">Policies</p> <p className="section__header">Policies</p>
<div className="results__data"> <div className="results__data">
<b>No policies are checked for this host.</b> <b>
No policies are checked{" "}
{deviceUser ? `on your device` : `for this host`}.
</b>
<p> <p>
Expecting to see policies? Try selecting Refetch to ask this host Expecting to see policies? Try selecting Refetch to ask{" "}
{deviceUser ? `your device ` : `this host `}
to report new vitals. to report new vitals.
</p> </p>
</div> </div>
@ -36,6 +42,8 @@ const Policies = ({
); );
} }
console.log("policies: ", policies);
const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal); const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal);
const noResponses: IHostPolicy[] = const noResponses: IHostPolicy[] =
policies.filter( policies.filter(

View File

@ -13,7 +13,7 @@ import PATHS from "router/paths";
import sortUtils from "utilities/sort"; import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants"; import { PolicyResponse } from "utilities/constants";
import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png"; import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png";
import FailIcon from "../../../../../../assets/images/icon-exclamation-circle-red-16x16@2x.png"; import FailIcon from "../../../../../../assets/images/icon-action-fail-16x16@2x.png";
// TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)} // TODO functions for paths math e.g., path={PATHS.MANAGE_HOSTS + getParams(cellProps.row.original)}

View File

@ -12,7 +12,7 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCel
import { IHostPolicyQuery } from "interfaces/host"; import { IHostPolicyQuery } from "interfaces/host";
import sortUtils from "utilities/sort"; import sortUtils from "utilities/sort";
import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png"; import PassIcon from "../../../../../../assets/images/icon-check-circle-green-16x16@2x.png";
import FailIcon from "../../../../../../assets/images/icon-exclamation-circle-red-16x16@2x.png"; import FailIcon from "../../../../../../assets/images/icon-action-fail-16x16@2x.png";
interface IHeaderProps { interface IHeaderProps {
column: ColumnInstance & IDataColumn; column: ColumnInstance & IDataColumn;

View File

@ -1,3 +1,3 @@
$col-sm: 62px; // column size will "hug" its contents $col-sm: 102px; // column size will "hug" its contents
$col-md: 202px; // column size not including 24px padding on left and right (total width is 250px) $col-md: 252px; // column size not including 24px padding on left and right (total width is 300px)
$col-lg: 352px; // column size not including 24px padding on left and right (total width is 250px) $col-lg: 352px; // column size not including 24px padding on left and right (total width is 400px)