mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Fleet Desktop device user page (#4589)
This commit is contained in:
parent
d661d23956
commit
84de0b7db0
24 changed files with 1316 additions and 12 deletions
1
changes/issue-4092-fleet-desktop-ui
Normal file
1
changes/issue-4092-fleet-desktop-ui
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fleet desktop UI for device user
|
||||
|
|
@ -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 (
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.schedule-error {
|
||||
.page-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
1
frontend/components/PageError/index.ts
Normal file
1
frontend/components/PageError/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PageError";
|
||||
|
|
@ -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`;
|
||||
},
|
||||
|
|
|
|||
513
frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx
Normal file
513
frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx
Normal 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 can’t 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
|
||||
)}`}
|
||||
|
||||
</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;
|
||||
54
frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx
Normal file
54
frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx
Normal 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?
|
||||
<a
|
||||
href="https://fleetdm.com/transparency"
|
||||
className={`${baseClass}__learn-more ${baseClass}__learn-more--inline`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read about transparency
|
||||
<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;
|
||||
31
frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss
Normal file
31
frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
1
frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts
Normal file
1
frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./InfoModal";
|
||||
668
frontend/pages/hosts/DeviceUserPage/_styles.scss
Normal file
668
frontend/pages/hosts/DeviceUserPage/_styles.scss
Normal 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;
|
||||
}
|
||||
1
frontend/pages/hosts/DeviceUserPage/index.ts
Normal file
1
frontend/pages/hosts/DeviceUserPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DeviceUserPage";
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./ScheduleError";
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
29
frontend/services/entities/device_user.ts
Normal file
29
frontend/services/entities/device_user.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
Loading…
Reference in a new issue