From 84de0b7db04678ae4319e746809ea1ee19b10069 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 21 Mar 2022 09:38:59 -0400 Subject: [PATCH] Fleet Desktop device user page (#4589) --- changes/issue-4092-fleet-desktop-ui | 1 + .../PageError/PageError.tsx} | 8 +- .../PageError}/_styles.scss | 2 +- frontend/components/PageError/index.ts | 1 + frontend/fleet/endpoints.ts | 1 + .../hosts/DeviceUserPage/DeviceUserPage.tsx | 513 ++++++++++++++ .../DeviceUserPage/InfoModal/InfoModal.tsx | 54 ++ .../DeviceUserPage/InfoModal/_styles.scss | 31 + .../hosts/DeviceUserPage/InfoModal/index.ts | 1 + .../pages/hosts/DeviceUserPage/_styles.scss | 668 ++++++++++++++++++ frontend/pages/hosts/DeviceUserPage/index.ts | 1 + .../hosts/HostDetailsPage/HostDetailsPage.tsx | 6 +- .../EmptySoftware/EmptySoftware.tsx | 0 .../SoftwareTab/EmptySoftware/_styles.scss | 0 .../SoftwareTab/EmptySoftware/index.ts | 0 .../SoftwareTab/SoftwareTab.tsx | 0 .../SoftwareTab/SoftwareTableConfig.tsx | 4 +- .../SoftwareVulnCount/SoftwareVulnCount.tsx | 2 +- .../SoftwareVulnCount/_styles.scss | 0 .../SoftwareTab/SoftwareVulnCount/index.ts | 0 .../components/ScheduleError/index.ts | 1 - frontend/router/index.tsx | 2 + frontend/router/paths.ts | 3 + frontend/services/entities/device_user.ts | 29 + 24 files changed, 1316 insertions(+), 12 deletions(-) create mode 100644 changes/issue-4092-fleet-desktop-ui rename frontend/{pages/schedule/ManageSchedulePage/components/ScheduleError/ScheduleError.tsx => components/PageError/PageError.tsx} (76%) rename frontend/{pages/schedule/ManageSchedulePage/components/ScheduleError => components/PageError}/_styles.scss (97%) create mode 100644 frontend/components/PageError/index.ts create mode 100644 frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx create mode 100644 frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx create mode 100644 frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss create mode 100644 frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts create mode 100644 frontend/pages/hosts/DeviceUserPage/_styles.scss create mode 100644 frontend/pages/hosts/DeviceUserPage/index.ts rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/EmptySoftware/EmptySoftware.tsx (100%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/EmptySoftware/_styles.scss (100%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/EmptySoftware/index.ts (100%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/SoftwareTab.tsx (100%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/SoftwareTableConfig.tsx (97%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx (90%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/SoftwareVulnCount/_styles.scss (100%) rename frontend/pages/hosts/{HostDetailsPage => }/SoftwareTab/SoftwareVulnCount/index.ts (100%) delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/index.ts create mode 100644 frontend/services/entities/device_user.ts diff --git a/changes/issue-4092-fleet-desktop-ui b/changes/issue-4092-fleet-desktop-ui new file mode 100644 index 0000000000..fa3c84d576 --- /dev/null +++ b/changes/issue-4092-fleet-desktop-ui @@ -0,0 +1 @@ +* Fleet desktop UI for device user \ No newline at end of file diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/ScheduleError.tsx b/frontend/components/PageError/PageError.tsx similarity index 76% rename from frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/ScheduleError.tsx rename to frontend/components/PageError/PageError.tsx index 9427ff1a1b..1c05d698ab 100644 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/ScheduleError.tsx +++ b/frontend/components/PageError/PageError.tsx @@ -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 ( diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/_styles.scss b/frontend/components/PageError/_styles.scss similarity index 97% rename from frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/_styles.scss rename to frontend/components/PageError/_styles.scss index 95f9ebd404..6779f5453e 100644 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/_styles.scss +++ b/frontend/components/PageError/_styles.scss @@ -1,4 +1,4 @@ -.schedule-error { +.page-error { display: flex; flex-direction: column; align-items: center; diff --git a/frontend/components/PageError/index.ts b/frontend/components/PageError/index.ts new file mode 100644 index 0000000000..79563768ac --- /dev/null +++ b/frontend/components/PageError/index.ts @@ -0,0 +1 @@ +export { default } from "./PageError"; diff --git a/frontend/fleet/endpoints.ts b/frontend/fleet/endpoints.ts index 997a827258..32f25bcd0d 100644 --- a/frontend/fleet/endpoints.ts +++ b/frontend/fleet/endpoints.ts @@ -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`; }, diff --git a/frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx new file mode 100644 index 0000000000..9661c05f9b --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/DeviceUserPage.tsx @@ -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(false); + + const [refetchStartTime, setRefetchStartTime] = useState(null); + const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); + const [hostSoftware, setHostSoftware] = useState([]); + const [host, setHost] = useState(); + const [orgLogoURL, setOrgLogoURL] = useState(""); + + 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( + ["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 ( +
+ +
+ ); + }; + + const renderSoftware = () => { + return ; + }; + + const renderRefetch = () => { + const isOnline = host?.status === "online"; + + return ( + <> +
+ +
+ + + You can’t fetch data from
an offline host. +
+
+ + ); + }; + + const renderDeviceUser = () => { + const numUsers = deviceMapping?.length; + if (numUsers) { + return ( +
+ Used by + + {numUsers === 1 && deviceMapping ? ( + deviceMapping[0].email || "---" + ) : ( + + + {`${numUsers} users`} + + +
+ {deviceMapping?.map((user, i, arr) => ( + {`${user.email}${ + i < arr.length - 1 ? ", " : "" + }`} + ))} +
+
+
+ )} +
+
+ ); + } + return null; + }; + + const renderDiskSpace = () => { + if ( + host && + (host.gigs_disk_space_available > 0 || + host.percent_disk_space_available > 0) + ) { + return ( + +
+
20 + ? "info-flex__disk-space-used" + : "info-flex__disk-space-warning" + } + style={{ + width: `${100 - titleData.percent_disk_space_available}%`, + }} + /> +
+ {titleData.gigs_disk_space_available} GB available + + ); + } + return No data available; + }; + + const renderShowInfoModal = () => ; + + const statusClassName = classnames("status", `status--${host?.status}`); + + const renderDeviceUserPage = () => { + return ( +
+ {isLoadingHost ? ( + + ) : ( +
+
+
+
+

My device

+

+ {`Last reported vitals ${humanHostDetailUpdated( + titleData.detail_updated_at + )}`} +   +

+ {renderRefetch()} +
+
+ {renderActionButtons()} +
+
+
+
+
+ Status + + {titleData.status} + +
+
+ Disk Space + {renderDiskSpace()} +
+
+ Memory + + {wrapFleetHelper(humanHostMemory, titleData.memory)} + +
+
+ Processor type + + {titleData.cpu_type} + +
+
+ Operating system + + {titleData.os_version} + +
+
+
+
+ + + + Details + Software + + +
+

About

+
+
+ + Last restarted + + + {wrapFleetHelper(humanHostUptime, aboutData.uptime)} + +
+
+ + Hardware model + + + {aboutData.hardware_model} + +
+
+ + Added to Fleet + + + {wrapFleetHelper( + humanHostEnrolled, + aboutData.last_enrolled_at + )} + +
+
+ Serial number + + {aboutData.hardware_serial} + +
+
+ IP address + + {aboutData.primary_ip} + +
+ {renderDeviceUser()} +
+
+
+ {renderSoftware()} +
+
+ + {showInfoModal && renderShowInfoModal()} +
+ )} +
+ ); + }; + + return ( +
+ + {loadingDeviceUserError ? : renderDeviceUserPage()} +
+ ); +}; + +export default DeviceUserPage; diff --git a/frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx b/frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx new file mode 100644 index 0000000000..0db03e473b --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/InfoModal/InfoModal.tsx @@ -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 ( + +
+

+ Your organization uses Fleet to check if all devices meet its security + policies. +

+

With Fleet, you and your team can secure your device, together.

+

+ Want to know what your organization can see?  + + Read about transparency  + open new tab + +

+
+ +
+
+
+ ); +}; + +export default InfoModal; diff --git a/frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss b/frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss new file mode 100644 index 0000000000..a5b77cfb6a --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/InfoModal/_styles.scss @@ -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; + } +} diff --git a/frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts b/frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts new file mode 100644 index 0000000000..5dd1cefbdd --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/InfoModal/index.ts @@ -0,0 +1 @@ +export { default } from "./InfoModal"; diff --git a/frontend/pages/hosts/DeviceUserPage/_styles.scss b/frontend/pages/hosts/DeviceUserPage/_styles.scss new file mode 100644 index 0000000000..46910f2be0 --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/_styles.scss @@ -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; +} diff --git a/frontend/pages/hosts/DeviceUserPage/index.ts b/frontend/pages/hosts/DeviceUserPage/index.ts new file mode 100644 index 0000000000..1187d93aed --- /dev/null +++ b/frontend/pages/hosts/DeviceUserPage/index.ts @@ -0,0 +1 @@ +export { default } from "./DeviceUserPage"; diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx index d4a47c50d8..71bd94d395 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tsx @@ -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 = ({
Used by - {numUsers === 1 ? ( + {numUsers === 1 && deviceMapping ? ( deviceMapping[0].email || "---" ) : ( @@ -1210,7 +1210,7 @@ const HostDetailsPage = ({
-

About this host

+

About

First enrolled diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/EmptySoftware.tsx b/frontend/pages/hosts/SoftwareTab/EmptySoftware/EmptySoftware.tsx similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/EmptySoftware.tsx rename to frontend/pages/hosts/SoftwareTab/EmptySoftware/EmptySoftware.tsx diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/_styles.scss b/frontend/pages/hosts/SoftwareTab/EmptySoftware/_styles.scss similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/_styles.scss rename to frontend/pages/hosts/SoftwareTab/EmptySoftware/_styles.scss diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/index.ts b/frontend/pages/hosts/SoftwareTab/EmptySoftware/index.ts similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/EmptySoftware/index.ts rename to frontend/pages/hosts/SoftwareTab/EmptySoftware/index.ts diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTab.tsx b/frontend/pages/hosts/SoftwareTab/SoftwareTab.tsx similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTab.tsx rename to frontend/pages/hosts/SoftwareTab/SoftwareTab.tsx diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTableConfig.tsx b/frontend/pages/hosts/SoftwareTab/SoftwareTableConfig.tsx similarity index 97% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTableConfig.tsx rename to frontend/pages/hosts/SoftwareTab/SoftwareTableConfig.tsx index b3a1c930d4..499d9a41ff 100644 --- a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTableConfig.tsx +++ b/frontend/pages/hosts/SoftwareTab/SoftwareTableConfig.tsx @@ -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: { diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx b/frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx similarity index 90% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx rename to frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx index f2a10276fb..e30e1d381c 100644 --- a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx +++ b/frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/SoftwareVulnCount.tsx @@ -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"; diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/_styles.scss b/frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/_styles.scss similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/_styles.scss rename to frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/_styles.scss diff --git a/frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/index.ts b/frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/index.ts similarity index 100% rename from frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareVulnCount/index.ts rename to frontend/pages/hosts/SoftwareTab/SoftwareVulnCount/index.ts diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/index.ts deleted file mode 100644 index bd3ab91443..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleError/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ScheduleError"; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index d3f6ad55ce..522dab3636 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -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 = ( + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 5e1b88b25c..1c2b4e6c35 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -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`; diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts new file mode 100644 index 0000000000..5d8519db94 --- /dev/null +++ b/frontend/services/entities/device_user.ts @@ -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); + }, +};