Fleet Desktop device user page (#4589)

This commit is contained in:
RachelElysia 2022-03-21 09:38:59 -04:00 committed by GitHub
parent d661d23956
commit 84de0b7db0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1316 additions and 12 deletions

View file

@ -0,0 +1 @@
* Fleet desktop UI for device user

View file

@ -1,12 +1,12 @@
/**
* Component when there is an error retrieving schedule set up in fleet
* Component when there is an error retrieving a page in fleet
*/
import React from "react";
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../../../../assets/images/icon-error-16x16@2x.png";
import OpenNewTabIcon from "../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../assets/images/icon-error-16x16@2x.png";
const baseClass = "schedule-error";
const baseClass = "page-error";
const ScheduleError = (): JSX.Element => {
return (

View file

@ -1,4 +1,4 @@
.schedule-error {
.page-error {
display: flex;
flex-direction: column;
align-items: center;

View file

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

View file

@ -5,6 +5,7 @@ export default {
CONFIRM_EMAIL_CHANGE: (token: string): string => {
return `/v1/fleet/email/change/${token}`;
},
DEVICE_USER_DETAILS: "/v1/fleet/device",
ENABLE_USER: (id: number): string => {
return `/v1/fleet/users/${id}/enable`;
},

View file

@ -0,0 +1,513 @@
import React, { useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { Params } from "react-router/lib/Router";
import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import classnames from "classnames";
import { isEmpty, pick, reduce } from "lodash";
import deviceUserAPI from "services/entities/device_user";
import hostAPI from "services/entities/hosts";
import {
IHost,
IDeviceMappingResponse,
IMacadminsResponse,
} from "interfaces/host";
import { ISoftware } from "interfaces/software";
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import ReactTooltip from "react-tooltip";
import PageError from "components/PageError";
// @ts-ignore
import OrgLogoIcon from "components/icons/OrgLogoIcon";
import Spinner from "components/Spinner";
import Button from "components/buttons/Button";
import TabsWrapper from "components/TabsWrapper";
import {
humanHostUptime,
humanHostEnrolled,
humanHostMemory,
humanHostDetailUpdated,
} from "fleet/helpers";
import InfoModal from "./InfoModal";
import SoftwareTab from "../SoftwareTab/SoftwareTab";
import InfoIcon from "../../../../assets/images/icon-info-purple-14x14@2x.png";
import FleetIcon from "../../../../assets/images/fleet-avatar-24x24@2x.png";
const baseClass = "device-user";
interface IDeviceUserPageProps {
params: Params;
}
interface IHostResponse {
host: IHost;
org_logo_url: string;
}
const DeviceUserPage = ({
params: { device_auth_token },
}: IDeviceUserPageProps): JSX.Element => {
const deviceAuthToken = device_auth_token;
const dispatch = useDispatch();
const handlePageError = useErrorHandler();
const [showInfoModal, setShowInfoModal] = useState<boolean>(false);
const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
const [showRefetchSpinner, setShowRefetchSpinner] = useState<boolean>(false);
const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]);
const [host, setHost] = useState<IHost | null>();
const [orgLogoURL, setOrgLogoURL] = useState<string>("");
const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery(
["deviceMapping", deviceAuthToken],
() =>
deviceUserAPI.loadHostDetailsExtension(deviceAuthToken, "device_mapping"),
{
enabled: !!deviceAuthToken,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IDeviceMappingResponse) => data.device_mapping,
}
);
const { data: macadmins, refetch: refetchMacadmins } = useQuery(
["macadmins", deviceAuthToken],
() => deviceUserAPI.loadHostDetailsExtension(deviceAuthToken, "macadmins"),
{
enabled: !!deviceAuthToken,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IMacadminsResponse) => data.macadmins,
}
);
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
macadmins !== null && refetchMacadmins();
};
const {
isLoading: isLoadingHost,
error: loadingDeviceUserError,
refetch: refetchHostDetails,
} = useQuery<IHostResponse, Error, IHostResponse>(
["host", deviceAuthToken],
() => deviceUserAPI.loadHostDetails(deviceAuthToken),
{
enabled: !!deviceAuthToken,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IHostResponse) => data,
onSuccess: (returnedHost) => {
setShowRefetchSpinner(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 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") {
setRefetchStartTime(Date.now());
setTimeout(() => {
refetchHostDetails();
refetchExtensions();
}, 1000);
} else {
setShowRefetchSpinner(false);
}
} else {
const totalElapsedTime = Date.now() - refetchStartTime;
if (totalElapsedTime < 60000) {
if (returnedHost.host.status === "online") {
setTimeout(() => {
refetchHostDetails();
refetchExtensions();
}, 1000);
} else {
dispatch(
renderFlash(
"error",
`This host is offline. Please try refetching host vitals later.`
)
);
setShowRefetchSpinner(false);
}
} else {
dispatch(
renderFlash(
"error",
`We're having trouble fetching fresh vitals for this host. Please try again later.`
)
);
setShowRefetchSpinner(false);
}
}
return; // exit early because refectch is pending so we can avoid unecessary steps below
}
setHostSoftware(returnedHost.host.software);
setHost(returnedHost.host);
setOrgLogoURL(returnedHost.org_logo_url);
},
onError: (error) => handlePageError(error),
}
);
const wrapFleetHelper = (
helperFn: (value: any) => string,
value: string
): any => {
return value === "---" ? value : helperFn(value);
};
// returns a mixture of props from host
const normalizeEmptyValues = (hostData: any): { [key: string]: any } => {
return reduce(
hostData,
(result, value, key) => {
if ((Number.isFinite(value) && value !== 0) || !isEmpty(value)) {
Object.assign(result, { [key]: value });
} else {
Object.assign(result, { [key]: "---" });
}
return result;
},
{}
);
};
const titleData = normalizeEmptyValues(
pick(host, [
"status",
"issues",
"memory",
"cpu_type",
"os_version",
"osquery_version",
"enroll_secret_name",
"detail_updated_at",
"percent_disk_space_available",
"gigs_disk_space_available",
])
);
const aboutData = normalizeEmptyValues(
pick(host, [
"seen_time",
"uptime",
"last_enrolled_at",
"hardware_model",
"hardware_serial",
"primary_ip",
])
);
const toggleInfoModal = useCallback(() => {
setShowInfoModal(!showInfoModal);
}, [showInfoModal, setShowInfoModal]);
const onRefetchHost = async () => {
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);
try {
await hostAPI.refetch(host).then(() => {
setRefetchStartTime(Date.now());
setTimeout(() => {
refetchHostDetails();
refetchExtensions();
}, 1000);
});
} catch (error) {
console.log(error);
dispatch(renderFlash("error", `Host "${host.hostname}" refetch error`));
setShowRefetchSpinner(false);
}
}
};
const renderActionButtons = () => {
return (
<div className={`${baseClass}__action-button-container`}>
<Button onClick={() => setShowInfoModal(true)} variant="text-icon">
<>
Info <img src={InfoIcon} alt="Host info icon" />
</>
</Button>
</div>
);
};
const renderSoftware = () => {
return <SoftwareTab isLoading={isLoadingHost} software={hostSoftware} />;
};
const renderRefetch = () => {
const isOnline = host?.status === "online";
return (
<>
<div
className="refetch"
data-tip
data-for="refetch-tooltip"
data-tip-disable={isOnline || showRefetchSpinner}
>
<Button
className={`
button
button--unstyled
${!isOnline ? "refetch-offline" : ""}
${showRefetchSpinner ? "refetch-spinner" : "refetch-btn"}
`}
disabled={!isOnline}
onClick={onRefetchHost}
>
{showRefetchSpinner
? "Fetching fresh vitals...this may take a moment"
: "Refetch"}
</Button>
</div>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
id="refetch-tooltip"
backgroundColor="#3e4771"
>
<span className={`${baseClass}__tooltip-text`}>
You cant fetch data from <br /> an offline host.
</span>
</ReactTooltip>
</>
);
};
const renderDeviceUser = () => {
const numUsers = deviceMapping?.length;
if (numUsers) {
return (
<div className="info-grid__block">
<span className="info-grid__header">Used by</span>
<span className="info-grid__data">
{numUsers === 1 && deviceMapping ? (
deviceMapping[0].email || "---"
) : (
<span className={`${baseClass}__device-mapping`}>
<span
className="device-user"
data-tip
data-for="device-user-tooltip"
>
{`${numUsers} users`}
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
id="device-user-tooltip"
backgroundColor="#3e4771"
>
<div
className={`${baseClass}__tooltip-text device-user-tooltip`}
>
{deviceMapping?.map((user, i, arr) => (
<span key={user.email}>{`${user.email}${
i < arr.length - 1 ? ", " : ""
}`}</span>
))}
</div>
</ReactTooltip>
</span>
)}
</span>
</div>
);
}
return null;
};
const renderDiskSpace = () => {
if (
host &&
(host.gigs_disk_space_available > 0 ||
host.percent_disk_space_available > 0)
) {
return (
<span className="info-flex__data">
<div className="info-flex__disk-space">
<div
className={
titleData.percent_disk_space_available > 20
? "info-flex__disk-space-used"
: "info-flex__disk-space-warning"
}
style={{
width: `${100 - titleData.percent_disk_space_available}%`,
}}
/>
</div>
{titleData.gigs_disk_space_available} GB available
</span>
);
}
return <span className="info-flex__data">No data available</span>;
};
const renderShowInfoModal = () => <InfoModal onCancel={toggleInfoModal} />;
const statusClassName = classnames("status", `status--${host?.status}`);
const renderDeviceUserPage = () => {
return (
<div className="fleet-desktop-wrapper">
{isLoadingHost ? (
<Spinner />
) : (
<div className={`${baseClass} body-wrap`}>
<div className="header title">
<div className="title__inner">
<div className="hostname-container">
<h1 className="hostname">My device</h1>
<p className="last-fetched">
{`Last reported vitals ${humanHostDetailUpdated(
titleData.detail_updated_at
)}`}
&nbsp;
</p>
{renderRefetch()}
</div>
</div>
{renderActionButtons()}
</div>
<div className="section title">
<div className="title__inner">
<div className="info-flex">
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Status</span>
<span className={`${statusClassName} info-flex__data`}>
{titleData.status}
</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Disk Space</span>
{renderDiskSpace()}
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Memory</span>
<span className="info-flex__data">
{wrapFleetHelper(humanHostMemory, titleData.memory)}
</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Processor type</span>
<span className="info-flex__data">
{titleData.cpu_type}
</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Operating system</span>
<span className="info-flex__data">
{titleData.os_version}
</span>
</div>
</div>
</div>
</div>
<TabsWrapper>
<Tabs>
<TabList>
<Tab>Details</Tab>
<Tab>Software</Tab>
</TabList>
<TabPanel>
<div className="section about">
<p className="section__header">About</p>
<div className="info-grid">
<div className="info-grid__block">
<span className="info-grid__header">
Last restarted
</span>
<span className="info-grid__data">
{wrapFleetHelper(humanHostUptime, aboutData.uptime)}
</span>
</div>
<div className="info-grid__block">
<span className="info-grid__header">
Hardware model
</span>
<span className="info-grid__data">
{aboutData.hardware_model}
</span>
</div>
<div className="info-grid__block">
<span className="info-grid__header">
Added to Fleet
</span>
<span className="info-grid__data">
{wrapFleetHelper(
humanHostEnrolled,
aboutData.last_enrolled_at
)}
</span>
</div>
<div className="info-grid__block">
<span className="info-grid__header">Serial number</span>
<span className="info-grid__data">
{aboutData.hardware_serial}
</span>
</div>
<div className="info-grid__block">
<span className="info-grid__header">IP address</span>
<span className="info-grid__data">
{aboutData.primary_ip}
</span>
</div>
{renderDeviceUser()}
</div>
</div>
</TabPanel>
<TabPanel>{renderSoftware()}</TabPanel>
</Tabs>
</TabsWrapper>
{showInfoModal && renderShowInfoModal()}
</div>
)}
</div>
);
};
return (
<div className="app-wrap">
<nav className="site-nav">
<div className="site-nav-container">
<ul className="site-nav-list">
<li className={`site-nav-item--logo`} key={`nav-item`}>
<OrgLogoIcon className="logo" src={orgLogoURL || FleetIcon} />
</li>
</ul>
</div>
</nav>
{loadingDeviceUserError ? <PageError /> : renderDeviceUserPage()}
</div>
);
};
export default DeviceUserPage;

View file

@ -0,0 +1,54 @@
import React from "react";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
import OpenNewTabIcon from "../../../../../assets/images/open-new-tab-12x12@2x.png";
export interface IInfoModalProps {
onCancel: () => void;
}
const baseClass = "device-user-info";
const InfoModal = ({ onCancel }: IInfoModalProps): JSX.Element => {
return (
<Modal
title="Welcome to Fleet"
onExit={onCancel}
className={`${baseClass}__modal`}
>
<div>
<p>
Your organization uses Fleet to check if all devices meet its security
policies.
</p>
<p>With Fleet, you and your team can secure your device, together.</p>
<p>
Want to know what your organization can see?&nbsp;
<a
href="https://fleetdm.com/transparency"
className={`${baseClass}__learn-more ${baseClass}__learn-more--inline`}
target="_blank"
rel="noopener noreferrer"
>
Read about transparency&nbsp;
<img className="icon" src={OpenNewTabIcon} alt="open new tab" />
</a>
</p>
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
type="button"
onClick={onCancel}
variant="brand"
>
Ok
</Button>
</div>
</div>
</Modal>
);
};
export default InfoModal;

View file

@ -0,0 +1,31 @@
.device-user-info {
&__modal {
@include position(absolute, 22px null null null);
background-color: $core-white;
width: 658px;
padding: $pad-xxlarge;
border-radius: $pad-small;
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
img {
height: 12px;
width: 12px;
margin: 0;
}
}
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
}
&__btn {
width: 120px;
}
}

View file

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

View file

@ -0,0 +1,668 @@
.app-wrap {
background-color: $ui-off-white;
}
.device-user {
display: flex;
flex-wrap: wrap;
flex-grow: 1;
align-content: flex-start;
padding-bottom: 50px;
min-width: 0;
background-color: $ui-off-white;
gap: $pad-medium;
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
.header {
flex: 100%;
display: flex;
flex-direction: column;
}
.section {
flex: 100%;
display: flex;
flex-direction: column;
background-color: $core-white;
border-radius: 16px;
border: 1px solid $ui-fleet-blue-15;
padding: $pad-xxlarge;
box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4);
&__header {
font-size: $medium;
font-weight: $bold;
margin: 0 0 $pad-large 0;
}
.info-flex {
display: flex;
flex-wrap: wrap;
.info-flex__item--title {
margin-bottom: 2.5rem;
}
&__item {
font-size: $x-small;
display: flex;
flex-direction: column;
white-space: nowrap;
&--title {
margin-right: $pad-xxlarge;
.info-flex__data {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
img {
width: 16px;
height: 16px;
vertical-align: sub;
}
.total-issues-count {
margin-left: $pad-small;
}
}
}
}
&__disk-space {
display: inline-block;
height: 4px;
width: 50px;
background-color: $ui-fleet-blue-15;
border-radius: 2px;
margin-right: $pad-small;
overflow: hidden;
}
&__disk-space-used {
background-color: $ui-success;
height: 100%;
}
&__disk-space-warning {
background-color: $ui-warning;
height: 100%;
}
&__header {
color: $core-fleet-black;
font-weight: $bold;
}
}
.info-grid {
display: grid;
grid-auto-flow: column;
grid-template-columns: repeat(4, max-content);
grid-template-rows: repeat(3, 1fr);
column-gap: $pad-xxlarge;
row-gap: $pad-medium;
&__block {
font-size: $x-small;
display: flex;
flex-direction: column;
white-space: nowrap;
&--title {
margin-right: $pad-xxlarge;
.info__data {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
&__header {
color: $core-fleet-black;
font-weight: $bold;
}
}
.list {
list-style: none;
padding: 0;
margin: 0;
&__item {
margin-bottom: 12px;
display: flex;
&:last-child {
margin-bottom: 0;
}
}
}
.results {
margin: 0;
width: 350px;
&__header {
margin: 0 0 $pad-medium 0;
font-size: $small;
color: $core-fleet-black;
font-weight: $bold;
}
&__data {
margin: 0;
font-size: $x-small;
}
}
}
.title {
flex-direction: row;
justify-content: space-between;
margin: 0;
padding-bottom: 0;
.hostname-container {
display: flex;
align-items: center;
}
.hostname {
font-size: $large;
font-weight: $bold;
}
.last-fetched {
font-size: $xx-small;
color: $core-fleet-black;
margin: 0;
padding-left: $pad-small;
}
.refetch {
display: flex;
margin-right: $pad-small;
.refetch-btn {
color: $core-vibrant-blue;
cursor: default;
font-size: $x-small;
height: 38px;
margin-right: $pad-small;
&::before {
display: inline-block;
position: relative;
padding: 5px 0 0 0; // centers spin
content: url(../assets/images/icon-refetch-12x12@2x.png);
transform: scale(0.5);
height: 28px;
}
&:hover {
cursor: pointer;
}
}
.refetch-offline {
opacity: 25%;
&:hover {
cursor: default;
}
}
.refetch-spinner {
color: $core-vibrant-blue;
cursor: default;
font-size: $x-small;
height: 38px;
opacity: 50%;
filter: saturate(100%);
&::before {
display: inline-block;
position: relative;
padding: 5px 0 0 0; // centers spin
display: inline-block;
content: url(../assets/images/icon-refetch-12x12@2x.png);
transform: scale(0.5);
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: scale(0.5) rotate(0deg);
transform-origin: center center;
}
100% {
transform: scale(0.5) rotate(360deg);
transform-origin: center center;
}
}
}
}
}
.button img {
transform: scale(0.5);
}
.component__tabs-wrapper {
background-color: $ui-off-white;
width: 100%;
.react-tabs__tab {
padding: 6px 0px 16px 0px;
margin-right: $pad-xxlarge;
}
.react-tabs__tab--selected {
background-color: $ui-off-white;
}
.section {
margin-top: $pad-medium;
}
}
.col-50 {
flex: 2;
}
.col-25 {
flex: 1;
}
.status {
color: $core-fleet-black;
text-transform: capitalize;
&--online {
&:before {
background-color: $ui-success;
border-radius: 100%;
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
&--offline,
&--mia {
&:before {
background-color: $ui-fleet-black-25;
border-radius: 100%;
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
&--mia {
text-transform: uppercase;
}
}
&__error {
display: flex;
flex-direction: column;
align-items: center;
margin: $pad-xlarge 0;
#error-icon {
height: 12px;
width: 12px;
margin-right: 8px;
}
#new-tab-icon {
height: 12px;
width: 12px;
margin-left: 6px;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
&__inner {
display: flex;
flex-direction: row;
}
.info {
&__header {
display: block;
color: $core-fleet-black;
font-weight: $bold;
font-size: $x-small;
text-align: left;
}
&__data {
display: block;
color: $core-fleet-black;
font-weight: normal;
font-size: $x-small;
text-align: left;
margin-top: 10px;
}
}
}
&__action-button-container {
display: flex;
align-items: center;
}
&__tooltip-text {
font-size: $x-small;
display: flex;
justify-content: center;
text-align: center;
}
&__device-mapping {
.device-user-tooltip {
flex-direction: column;
justify-content: start;
text-align: left;
}
}
&__wrapper {
border: solid 1px $ui-fleet-blue-15;
border-radius: 6px;
margin-top: $pad-small;
overflow: scroll;
box-shadow: inset -8px 0 17px -10px #e8edf4;
}
&__table {
width: 100%;
border-collapse: collapse;
color: $core-fleet-black;
font-size: $x-small;
}
tr {
border-bottom: 1px solid $ui-fleet-blue-15;
&:last-child {
border-bottom: 0;
}
}
thead {
background-color: $ui-off-white;
color: $core-fleet-black;
text-align: left;
border-bottom: 1px solid $ui-fleet-blue-15;
th {
padding: $pad-medium 27px;
white-space: nowrap;
border-right: 1px solid $ui-fleet-blue-15;
&:last-child {
border-right: none;
}
}
}
.section--software {
// Modified table container nav positioning
.table-container__results-count {
width: 150px;
}
.table-container__header {
justify-content: initial;
}
.table-container__table-controls {
margin-left: $pad-medium;
width: 100%;
justify-content: initial;
}
.table-container__search-input {
width: 100%;
}
th {
&:first-child {
border-right: none;
width: 16px;
padding-right: 0px;
}
&:nth-child(2) {
width: 33%;
padding-right: 0px;
}
&:last-child {
width: 150px;
overflow: none;
}
&.source__header {
width: 33%;
}
}
tr {
td {
position: relative;
&:first-child {
padding-right: 0px;
}
}
}
.data-table__table {
table-layout: fixed;
td {
overflow: hidden;
text-overflow: ellipsis;
&:last-child {
text-overflow: unset;
}
}
.name__cell,
.version__cell {
overflow: initial;
text-overflow: initial;
}
}
}
tbody {
td {
padding: 12px 27px;
white-space: nowrap;
}
}
.tooltip__tooltip-icon {
img {
vertical-align: middle;
height: 16px;
width: auto;
}
}
.software-name {
margin-left: 8px;
}
.software-last-used-muted {
color: $ui-fleet-black-50;
}
.buttons {
display: flex;
align-items: center;
position: absolute;
right: 25px;
span {
font-weight: $regular;
}
a {
display: flex;
align-items: center;
}
}
.form-field--dropdown {
margin: 0;
}
&__vuln_dropdown {
width: 219px;
.Select-menu-outer {
width: 364px;
max-height: 310px;
.Select-menu {
max-height: none;
}
}
.Select-value {
padding-left: $pad-medium;
padding-right: $pad-medium;
&::before {
display: inline-block;
position: absolute;
padding: 5px 0 0 0; // centers spin
content: url(../assets/images/icon-filter-black-16x16@2x.png);
transform: scale(0.5);
height: 26px;
left: 2px;
}
}
.Select-value-label {
padding-left: $pad-large;
font-size: $small !important;
}
}
}
.site-nav-item {
position: relative;
transition: color 200ms ease-in-out;
cursor: pointer;
max-height: 50px;
&:hover {
background-color: $core-fleet-black;
}
&--multiple.site-nav-item--active {
background-color: transparent;
border-right: 0;
&:hover {
background-color: transparent;
}
}
&__icon {
position: relative;
font-size: $large;
margin-right: $pad-small;
width: 16px;
height: 16px;
vertical-align: sub;
}
&__name {
display: inline-flex;
flex-direction: column;
align-items: center;
text-decoration: none;
vertical-align: middle;
font-weight: $regular;
font-size: $x-small;
// Bolding text when the button is active causes a layout shift
// so we add a hidden pseudo element with the same text string
&:before {
content: attr(data-text);
height: 0;
visibility: hidden;
overflow: hidden;
user-select: none;
pointer-events: none;
font-weight: $bold;
}
}
&__link {
color: $core-white;
text-align: center;
display: flex;
align-items: center;
padding: 14px 20px 17px;
text-decoration: none;
}
&__logo {
text-align: center;
display: table-cell;
vertical-align: middle;
width: 64px;
}
&--active {
border-bottom: 3px solid $core-vibrant-blue;
background-color: $core-fleet-black;
height: 47px;
&:hover {
background-color: $core-fleet-black;
}
.site-nav-item__name {
font-weight: $bold;
}
}
}
.logo {
height: 48px;
transform: scale(0.5);
position: relative;
top: 1px;
}
.site-nav-container {
flex-grow: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.site-nav-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}

View file

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

View file

@ -59,7 +59,7 @@ import {
secondsToHms,
} from "fleet/helpers";
import SoftwareTab from "./SoftwareTab/SoftwareTab";
import SoftwareTab from "../SoftwareTab/SoftwareTab";
// @ts-ignore
import SelectQueryModal from "./SelectQueryModal";
import TransferHostModal from "./TransferHostModal";
@ -1021,7 +1021,7 @@ const HostDetailsPage = ({
<div className="info-grid__block">
<span className="info-grid__header">Used by</span>
<span className="info-grid__data">
{numUsers === 1 ? (
{numUsers === 1 && deviceMapping ? (
deviceMapping[0].email || "---"
) : (
<span className={`${baseClass}__device-mapping`}>
@ -1210,7 +1210,7 @@ const HostDetailsPage = ({
</TabList>
<TabPanel>
<div className="section about">
<p className="section__header">About this host</p>
<p className="section__header">About</p>
<div className="info-grid">
<div className="info-grid__block">
<span className="info-grid__header">First enrolled</span>

View file

@ -12,8 +12,8 @@ import PATHS from "router/paths";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import Chevron from "../../../../../assets/images/icon-chevron-right-9x6@2x.png";
import IssueIcon from "../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import Chevron from "../../../../assets/images/icon-chevron-right-9x6@2x.png";
interface IHeaderProps {
column: {

View file

@ -1,7 +1,7 @@
import React from "react";
import { ISoftware } from "interfaces/software";
import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
const baseClass = "software-vuln-count";

View file

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

View file

@ -26,6 +26,7 @@ import PremiumTierRoutes from "components/PremiumTierRoutes";
import ConfirmInvitePage from "pages/ConfirmInvitePage";
import ConfirmSSOInvitePage from "pages/ConfirmSSOInvitePage";
import CoreLayout from "layouts/CoreLayout";
import DeviceUserPage from "pages/hosts/DeviceUserPage";
import EditPackPage from "pages/packs/EditPackPage";
import EmailTokenRedirect from "components/EmailTokenRedirect";
import HostDetailsPage from "pages/hosts/HostDetailsPage";
@ -178,6 +179,7 @@ const routes = (
</Route>
</Route>
</Route>
<Route path="/device/:device_auth_token" component={DeviceUserPage} />
</Route>
<Route path="/apionlyuser" component={ApiOnlyUser} />
<Route path="/404" component={Fleet404} />

View file

@ -34,6 +34,9 @@ export default {
HOST_DETAILS: (host: IHost): string => {
return `${URL_PREFIX}/hosts/${host.id}`;
},
DEVICE_USER_DETAILS: (deviceAuthToken: any): string => {
return `${URL_PREFIX}/device/${deviceAuthToken}`;
},
MANAGE_SOFTWARE: `${URL_PREFIX}/software/manage`,
TEAM_DETAILS_MEMBERS: (teamId: number): string => {
return `${URL_PREFIX}/settings/teams/${teamId}/members`;

View file

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import endpoints from "fleet/endpoints";
export type ILoadHostDetailsExtension = "device_mapping" | "macadmins";
export default {
loadHostDetails: (deviceAuthToken: string) => {
const { DEVICE_USER_DETAILS } = endpoints;
const path = `${DEVICE_USER_DETAILS}/${deviceAuthToken}`;
return sendRequest("GET", path);
},
loadHostDetailsExtension: (
deviceAuthToken: string,
extension: ILoadHostDetailsExtension
) => {
const { DEVICE_USER_DETAILS } = endpoints;
const path = `${DEVICE_USER_DETAILS}/${deviceAuthToken}/${extension}`;
return sendRequest("GET", path);
},
refetch: (deviceAuthToken: string) => {
const { DEVICE_USER_DETAILS } = endpoints;
const path = `${DEVICE_USER_DETAILS}/${deviceAuthToken}/refetch`;
return sendRequest("POST", path);
},
};