diff --git a/changes/feature-6931-display-munki-issues-in-ui b/changes/feature-6931-display-munki-issues-in-ui new file mode 100644 index 0000000000..803d101ce9 --- /dev/null +++ b/changes/feature-6931-display-munki-issues-in-ui @@ -0,0 +1 @@ +* UI displays munki issues on macOS homepage, filter by issue on manage host page, and munki issue table on host details page \ No newline at end of file diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx index 4c01f94b97..7ef21d221d 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { uniqueId } from "lodash"; +import { v4 as uuidv4 } from "uuid"; import ReactTooltip from "react-tooltip"; @@ -90,9 +90,7 @@ const PillCell = ({ <> {pillText} @@ -102,7 +100,7 @@ const PillCell = ({ // offset={getTooltipOffset(pillText)} effect="solid" backgroundColor="#3e4771" - id={`${customIdPrefix || "pill"}__${id?.toString() || uniqueId()}`} + id={`${customIdPrefix || "pill"}__${id?.toString() || uuidv4()}`} data-html > diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx b/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx new file mode 100644 index 0000000000..83771a6365 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx @@ -0,0 +1,59 @@ +import React, { useState, useRef, useLayoutEffect } from "react"; +import { v4 as uuidv4 } from "uuid"; + +import ReactTooltip from "react-tooltip"; + +interface ITruncatedTextCellProps { + value: string | number | boolean; + classes?: string; +} + +const TruncatedTextCell = ({ + value, + classes = "w250", +}: ITruncatedTextCellProps): JSX.Element => { + const ref = useRef(null); + + const [offsetWidth, setOffsetWidth] = useState(0); + const [scrollWidth, setScrollWidth] = useState(0); + + useLayoutEffect(() => { + if (ref?.current !== null) { + setOffsetWidth(ref.current.offsetWidth); + setScrollWidth(ref.current.scrollWidth); + } + }, []); + + const id = uuidv4(); + const tooltipDisabled = offsetWidth === scrollWidth; + + return ( +
+
+ + {value} + +
+ + {value} + +
+ ); +}; + +export default TruncatedTextCell; diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss new file mode 100644 index 0000000000..c2b834b275 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss @@ -0,0 +1,13 @@ +.data-table__truncated-text { + &--cell { + display: inline-block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .truncated { + display: inline-block; + max-width: 100%; + } +} diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/index.ts b/frontend/components/TableContainer/DataTable/TruncatedTextCell/index.ts new file mode 100644 index 0000000000..ab9461c700 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/index.ts @@ -0,0 +1 @@ +export { default } from "./TruncatedTextCell"; diff --git a/frontend/components/TooltipWrapper/_styles.scss b/frontend/components/TooltipWrapper/_styles.scss index 324d67f5dd..6e758795b2 100644 --- a/frontend/components/TooltipWrapper/_styles.scss +++ b/frontend/components/TooltipWrapper/_styles.scss @@ -36,7 +36,7 @@ } &__tip-text { width: max-content; - max-width: 296px; + max-width: 341px; padding: 12px; color: $core-white; background-color: $core-fleet-blue; diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index f52df5dd0e..25038cf876 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -80,10 +80,18 @@ export interface IMDMData { server_url: string; } +export interface IMunkiIssue { + id: number; + name: string; + type: "error" | "warning"; + created_at: string; +} + export interface IMacadminsResponse { macadmins: null | { munki: null | IMunkiData; mobile_device_management: null | IMDMData; + munki_issues: IMunkiIssue[]; }; } diff --git a/frontend/interfaces/macadmins.ts b/frontend/interfaces/macadmins.ts index 5d66432049..7532b78c24 100644 --- a/frontend/interfaces/macadmins.ts +++ b/frontend/interfaces/macadmins.ts @@ -3,11 +3,17 @@ export interface IDataTableMDMFormat { hosts: number; } -export interface IMunkiAggregate { +export interface IMunkiVersionsAggregate { version: string; hosts_count: number; } +export interface IMunkiIssuesAggregate { + id: number; + name: string; + type: "error" | "warning"; + hosts_count: number; +} export interface IMDMAggregateStatus { enrolled_manual_hosts_count: number; enrolled_automated_hosts_count: number; @@ -24,7 +30,8 @@ export interface IMDMSolution { export interface IMacadminAggregate { macadmins: { counts_updated_at: string; - munki_versions: IMunkiAggregate[]; + munki_versions: IMunkiVersionsAggregate[]; + munki_issues: IMunkiIssuesAggregate[]; mobile_device_management_enrollment_status: IMDMAggregateStatus; mobile_device_management_solution: IMDMSolution[] | null; }; diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx index 89d8fd85e4..e9907b8724 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -323,11 +323,11 @@ const Homepage = (): JSX.Element => { }; const macOSLayout = () => ( -
- {OperatingSystemsCard} - {MunkiCard} - {MDMCard} -
+ <> +
{OperatingSystemsCard}
+
{MunkiCard}
+
{MDMCard}
+ ); const windowsLayout = () => ( diff --git a/frontend/pages/Homepage/cards/Munki/Munki.tsx b/frontend/pages/Homepage/cards/Munki/Munki.tsx index 9cebcfa85f..b8ad40244e 100644 --- a/frontend/pages/Homepage/cards/Munki/Munki.tsx +++ b/frontend/pages/Homepage/cards/Munki/Munki.tsx @@ -1,14 +1,21 @@ import React, { useState } from "react"; import { useQuery } from "react-query"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import macadminsAPI from "services/entities/macadmins"; -import { IMacadminAggregate, IMunkiAggregate } from "interfaces/macadmins"; +import { + IMacadminAggregate, + IMunkiIssuesAggregate, + IMunkiVersionsAggregate, +} from "interfaces/macadmins"; +import TabsWrapper from "components/TabsWrapper"; import TableContainer from "components/TableContainer"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; import LastUpdatedText from "components/LastUpdatedText"; -import generateTableHeaders from "./MunkiTableConfig"; +import munkiVersionsTableHeaders from "./MunkiVersionsTableConfig"; +import munkiIssuesTableHeaders from "./MunkiIssuesTableConfig"; interface IMunkiCardProps { showMunkiUI: boolean; @@ -22,9 +29,19 @@ const DEFAULT_SORT_HEADER = "hosts_count"; const PAGE_SIZE = 8; const baseClass = "home-munki"; -const EmptyMunki = (): JSX.Element => ( +const EmptyMunkiIssues = (): JSX.Element => (
-

Unable to detect Munki versions.

+

No Munki issues detected

+

+ This report is updated every hour to protect the performance of your + devices. +

+
+); + +const EmptyMunkiVersions = (): JSX.Element => ( +
+

Unable to detect Munki versions

To see Munki versions, deploy  { - const [munkiData, setMunkiData] = useState([]); + const [navTabIndex, setNavTabIndex] = useState(0); + const [pageIndex, setPageIndex] = useState(0); + const [munkiIssuesData, setMunkiIssuesData] = useState< + IMunkiIssuesAggregate[] + >([]); + const [munkiVersionsData, setMunkiVersionsData] = useState< + IMunkiVersionsAggregate[] + >([]); const { isFetching: isMunkiFetching, error: errorMunki } = useQuery< IMacadminAggregate, @@ -53,9 +77,14 @@ const Munki = ({ >(["munki", currentTeamId], () => macadminsAPI.loadAll(currentTeamId), { keepPreviousData: true, onSuccess: (data) => { - const { counts_updated_at, munki_versions } = data.macadmins; + const { + counts_updated_at, + munki_versions, + munki_issues, + } = data.macadmins; - setMunkiData(munki_versions); + setMunkiVersionsData(munki_versions); + setMunkiIssuesData(munki_issues); setShowMunkiUI(true); setTitleDetail && setTitleDetail( @@ -70,7 +99,9 @@ const Munki = ({ }, }); - const tableHeaders = generateTableHeaders(); + const onTabChange = (index: number) => { + setNavTabIndex(index); + }; // Renders opaque information as host information is loading const opacity = showMunkiUI ? { opacity: 1 } : { opacity: 0 }; @@ -83,26 +114,60 @@ const Munki = ({

)}
- {errorMunki ? ( - - ) : ( - - )} + + + + Issues + Versions + + + {errorMunki ? ( + + ) : ( + + )} + + + {errorMunki ? ( + + ) : ( + + )} + + +
); diff --git a/frontend/pages/Homepage/cards/Munki/MunkiIssuesTableConfig.tsx b/frontend/pages/Homepage/cards/Munki/MunkiIssuesTableConfig.tsx new file mode 100644 index 0000000000..e817ac4cfe --- /dev/null +++ b/frontend/pages/Homepage/cards/Munki/MunkiIssuesTableConfig.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { capitalize } from "lodash"; +import { Link } from "react-router"; +import PATHS from "router/paths"; + +import { IMunkiIssuesAggregate } from "interfaces/macadmins"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; +import TextCell from "components/TableContainer/DataTable/TextCell"; +import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell"; +import TooltipWrapper from "components/TooltipWrapper"; + +import Chevron from "../../../../../assets/images/icon-chevron-right-9x6@2x.png"; + +const TAGGED_TEMPLATES = { + hostsByMunkiIssue: (munkiIssueId: number) => { + return `?munki_issue_id=${munkiIssueId}`; + }, +}; + +// NOTE: cellProps come from react-table +// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties +interface ICellProps { + cell: { + value: string; + }; + row: { + original: IMunkiIssuesAggregate; + }; +} + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface IDataColumn { + title: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: (props: ICellProps) => JSX.Element; + disableHidden?: boolean; + disableSortBy?: boolean; +} + +const munkiIssuesTableHeaders = [ + { + title: "Issue", + Header: (): JSX.Element => { + const titleWithToolTip = ( + + Issue + + ); + return ; + }, + disableSortBy: true, + accessor: "name", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + title: "Type", + Header: "Type", + disableSortBy: true, + accessor: "type", + Cell: (cellProps: ICellProps) => ( + + ), + }, + { + title: "Hosts", + Header: (headerProps: IHeaderProps): JSX.Element => { + return ( + + ); + }, + disableSortBy: false, + accessor: "hosts_count", + Cell: (cellProps: ICellProps) => , + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + View all hosts{" "} + link to hosts filtered by policy ID + + )} + + ); + }, + }, +]; + +export default munkiIssuesTableHeaders; diff --git a/frontend/pages/Homepage/cards/Munki/MunkiTableConfig.tsx b/frontend/pages/Homepage/cards/Munki/MunkiVersionsTableConfig.tsx similarity index 87% rename from frontend/pages/Homepage/cards/Munki/MunkiTableConfig.tsx rename to frontend/pages/Homepage/cards/Munki/MunkiVersionsTableConfig.tsx index 9b444d800f..6a83d57c16 100644 --- a/frontend/pages/Homepage/cards/Munki/MunkiTableConfig.tsx +++ b/frontend/pages/Homepage/cards/Munki/MunkiVersionsTableConfig.tsx @@ -31,7 +31,7 @@ interface IDataColumn { disableSortBy?: boolean; } -const munkiTableHeaders = [ +const munkiVersionsTableHeaders = [ { title: "Version", Header: "Version", @@ -48,8 +48,4 @@ const munkiTableHeaders = [ }, ]; -const generateTableHeaders = (): IDataColumn[] => { - return munkiTableHeaders; -}; - -export default generateTableHeaders; +export default munkiVersionsTableHeaders; diff --git a/frontend/pages/Homepage/cards/Munki/_styles.scss b/frontend/pages/Homepage/cards/Munki/_styles.scss index f700146373..18ecd65364 100644 --- a/frontend/pages/Homepage/cards/Munki/_styles.scss +++ b/frontend/pages/Homepage/cards/Munki/_styles.scss @@ -11,7 +11,7 @@ &__empty-munki { margin: $pad-medium auto 0; - h1 { + h2 { font-size: $small; font-weight: $bold; margin-bottom: $pad-medium; @@ -29,8 +29,8 @@ table-layout: fixed; thead { - .name__header { - width: 60%; + .type__header { + width: $col-sm; } .version__header { width: 30%; @@ -39,29 +39,51 @@ .hosts_count__header { border-right: 0; padding-right: 0; - width: 60px; + width: 9%; } .id__header { padding: 0; border-left: 0; width: 40px; } + .linkToFilteredHosts__header { + width: 140px; + } } tbody { - .name__cell, - .version__cell { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .id__cell { - padding: 0; - width: 40px; - } - .vulnerabilities__cell { - img { - transform: scale(0.5); + tr { + .version__cell { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .id__cell { + padding: 0; + width: 40px; + } + + .vulnerabilities__cell { + img { + transform: scale(0.5); + } + } + + .issue-link { + visibility: hidden; + text-overflow: none; + img { + vertical-align: text-top; + width: 16px; + } + } + + &:hover { + .issue-link { + visibility: visible; + text-overflow: none; + } } } } diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index 88a5f23b58..9424d6df06 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -34,7 +34,7 @@ import { } from "interfaces/enroll_secret"; import { IHost } from "interfaces/host"; import { ILabel } from "interfaces/label"; -import { IMDMSolution } from "interfaces/macadmins"; +import { IMDMSolution, IMunkiIssuesAggregate } from "interfaces/macadmins"; import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IPolicy } from "interfaces/policy"; import { ISoftware } from "interfaces/software"; @@ -223,6 +223,10 @@ const ManageHostsPage = ({ mdmSolutionDetails, setMDMSolutionDetails, ] = useState(null); + const [ + munkiIssueDetails, + setMunkiIssueDetails, + ] = useState(null); const [tableQueryData, setTableQueryData] = useState(); const [ currentQueryOptions, @@ -248,6 +252,10 @@ const ManageHostsPage = ({ : undefined; const mdmEnrollmentStatus = queryParams?.mdm_enrollment_status; const { os_id: osId, os_name: osName, os_version: osVersion } = queryParams; + const munkiIssueId = + queryParams?.munki_issue_id !== undefined + ? parseInt(queryParams?.munki_issue_id, 10) + : undefined; const { active_label: activeLabel, label_id: labelID } = routeParams; // ===== filter matching @@ -427,11 +435,13 @@ const ManageHostsPage = ({ hosts: returnedHosts, software, mobile_device_management_solution, + munki_issue, } = await hostsAPI.loadHosts(options); setHosts(returnedHosts); software && setSoftwareDetails(software); mobile_device_management_solution && setMDMSolutionDetails(mobile_device_management_solution); + munki_issue && setMunkiIssueDetails(munki_issue); } catch (error) { console.error(error); setHasHostErrors(true); @@ -517,6 +527,7 @@ const ManageHostsPage = ({ osId, osName, osVersion, + munkiIssueId, page: tableQueryData ? tableQueryData.pageIndex : 0, perPage: tableQueryData ? tableQueryData.pageSize : 100, device_mapping: true, @@ -684,6 +695,20 @@ const ManageHostsPage = ({ ); }; + const handleClearMunkiIssueFilter = () => { + handleResetPageIndex(); + + router.replace( + getNextLocationPath({ + pathPrefix: PATHS.MANAGE_HOSTS, + routeTemplate, + routeParams, + queryParams: omit(queryParams, ["munki_issue_id"]), + }) + ); + setMunkiIssueDetails(null); + }; + const handleTeamSelect = (teamId: number) => { const { MANAGE_HOSTS } = PATHS; const teamIdParam = getValidatedTeamId( @@ -818,6 +843,16 @@ const ManageHostsPage = ({ newQueryParams.mdm_enrollment_status = mdmEnrollmentStatus; } + if ( + munkiIssueId && + !mdmEnrollmentStatus && + !policyId && + !softwareId && + !mdmId + ) { + newQueryParams.munki_issue_id = munkiIssueId; + } + if ( (osId || (osName && osVersion)) && !softwareId && @@ -852,6 +887,7 @@ const ManageHostsPage = ({ osId, osName, osVersion, + munkiIssueId, sortBy, ] ); @@ -1052,6 +1088,7 @@ const ManageHostsPage = ({ osId, osName, osVersion, + munkiIssueId, }); toggleTransferHostModal(); @@ -1106,6 +1143,7 @@ const ManageHostsPage = ({ osId, osName, osVersion, + munkiIssueId, }); refetchLabels(); @@ -1321,6 +1359,24 @@ const ManageHostsPage = ({ ); }; + const renderMunkiIssueFilterBlock = () => { + if (munkiIssueDetails) { + return ( + + Hosts that reported this Munki issue
+ the last time Munki ran on each host. +
+ } + onClear={handleClearMunkiIssueFilter} + /> + ); + } + return null; + }; + const renderEditColumnsModal = () => { if (!config || !currentUser) { return null; @@ -1484,6 +1540,7 @@ const ManageHostsPage = ({ os_id: osId, os_name: osName, os_version: osVersion, + munkiIssueId, visibleColumns, }; @@ -1558,7 +1615,8 @@ const ManageHostsPage = ({ mdmId || mdmEnrollmentStatus || osId || - (osName && osVersion) + (osName && osVersion) || + munkiIssueId ) { return (
@@ -1567,24 +1625,28 @@ const ManageHostsPage = ({ !softwareId && !mdmId && !mdmEnrollmentStatus && + !munkiIssueId && !showSelectedLabel && renderPoliciesFilterBlock()} {!!softwareId && !policyId && !mdmId && !mdmEnrollmentStatus && + !munkiIssueId && !showSelectedLabel && renderSoftwareFilterBlock()} {!!mdmId && !policyId && !softwareId && !mdmEnrollmentStatus && + !munkiIssueId && !showSelectedLabel && renderMDMSolutionFilterBlock()} {!!mdmEnrollmentStatus && !policyId && !softwareId && !mdmId && + !munkiIssueId && !showSelectedLabel && renderMDMEnrollmentFilterBlock()} {(!!osId || (!!osName && !!osVersion)) && @@ -1593,7 +1655,15 @@ const ManageHostsPage = ({ !showSelectedLabel && !mdmId && !mdmEnrollmentStatus && + !munkiIssueId && renderOSFilterBlock()} + {!!munkiIssueId && + !policyId && + !softwareId && + !showSelectedLabel && + !mdmId && + !mdmEnrollmentStatus && + renderMunkiIssueFilterBlock()}
); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index b72952ea8e..cc58029c2c 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -44,6 +44,7 @@ import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; import AgentOptionsCard from "../cards/AgentOptions"; import LabelsCard from "../cards/Labels"; +import MunkiIssuesCard from "../cards/MunkiIssues"; import SoftwareCard from "../cards/Software"; import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; @@ -611,6 +612,13 @@ const HostDetailsPage = ({ softwareInventoryEnabled={features?.enable_software_inventory} deviceType={host?.platform === "darwin" ? "macos" : ""} /> + {macadmins && ( + + )} { @@ -16,62 +16,70 @@ const EmptyState = ({ title, reason }: IEmptyStateProps): JSX.Element => { return "Software inventory"; case "users": return "User collection"; + case "munki-issues": + return "Munki issues"; default: return "Data collection"; } }; - switch (reason) { - case "empty-search": - return ( -
-
-
-

No {title} matched your search criteria.

-

Try a different search.

-
+ const renderEmptyState = () => { + switch (reason) { + case "empty-search": + return ( +
+

No {title} matched your search criteria.

+

Try a different search.

-
- ); - case "disabled": - return ( -
-
-
-

{formalTitle()} has been disabled.

-

- Check out the Fleet documentation for{" "} - - steps to enable this feature - Open external link - -

-
+ ); + case "disabled": + return ( +
+

{formalTitle()} has been disabled.

+

+ Check out the Fleet documentation for{" "} + + steps to enable this feature + External link + +

-
- ); - default: - return ( -
-
-
-

- No {title === "software" ? "installed software" : title}{" "} - detected on this host. -

-

- Expecting to see {title}? Try again in a few seconds as the - system catches up. -

-
+ ); + case "none-detected": + return ( +
+

No {formalTitle()} detected

+

+ {title === "munki-issues" && + "The last time Munki ran on this host, no issues were reported."} +

-
- ); - } + ); + default: + return ( +
+

+ No {title === "software" ? "installed software" : title} detected + on this host. +

+

+ Expecting to see {title}? Try again in a few seconds as the system + catches up. +

+
+ ); + } + }; + + return ( +
+
{renderEmptyState()}
+
+ ); }; export default EmptyState; diff --git a/frontend/pages/hosts/details/cards/EmptyState/_styles.scss b/frontend/pages/hosts/details/cards/EmptyState/_styles.scss index 6f3687abf3..4be3295f3f 100644 --- a/frontend/pages/hosts/details/cards/EmptyState/_styles.scss +++ b/frontend/pages/hosts/details/cards/EmptyState/_styles.scss @@ -1,15 +1,16 @@ .empty-state { display: flex; flex-direction: column; - align-items: flex-start; + align-items: center; margin: 0; &__inner { display: flex; flex-direction: row; + width: 350px; - h1 { - font-size: $x-small; + h2 { + font-size: $small; font-weight: $bold; margin-bottom: $pad-medium; } @@ -36,21 +37,4 @@ margin-left: 4px; } } - - &__empty-list { - h1 { - font-size: $small; - } - } - - &__empty-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } -} - -.empty-search { - margin-top: $pad-small; - align-items: center; } diff --git a/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx new file mode 100644 index 0000000000..a8139020d1 --- /dev/null +++ b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssues.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +import { IMunkiIssue } from "interfaces/host"; +import TableContainer from "components/TableContainer"; + +import EmptyState from "../EmptyState"; + +import { munkiIssuesTableHeaders } from "./MunkiIssuesTableConfig"; + +const baseClass = "munki-issues"; + +interface IMunkiIssuesTableProps { + isLoading: boolean; + munkiIssues?: IMunkiIssue[]; + deviceType?: string; +} + +const MunkiIssuesTable = ({ + isLoading, + munkiIssues, + deviceType, +}: IMunkiIssuesTableProps): JSX.Element => { + const tableMunkiIssues = munkiIssues; + const tableHeaders = munkiIssuesTableHeaders; + + const EmptyMunkiIssues = () => { + return ( +
+

Munki issues

+ +
+ ); + }; + + return ( +
+

Munki issues

+ + {munkiIssues?.length ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; +export default MunkiIssuesTable; diff --git a/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx new file mode 100644 index 0000000000..45fd7685e3 --- /dev/null +++ b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { capitalize } from "lodash"; +import ReactTooltip from "react-tooltip"; + +import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict"; +import { abbreviateTimeUnits } from "utilities/helpers"; + +import PATHS from "router/paths"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; +import TextCell from "components/TableContainer/DataTable/TextCell"; +import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell"; +import TooltipWrapper from "components/TooltipWrapper"; +import { IMunkiIssue } from "interfaces/host"; + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} +interface ICellProps { + cell: { + value: number | string | string[]; + }; + row: { + original: IMunkiIssue; + index: number; + }; +} + +interface IStringCellProps extends ICellProps { + cell: { + value: string; + }; +} + +interface IDataColumn { + title: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: (props: IStringCellProps) => JSX.Element; + disableHidden?: boolean; + disableSortBy?: boolean; + disableGlobalFilter?: boolean; + sortType?: string; + // Filter can be used by react-table to render a filter input inside the column header + Filter?: () => null | JSX.Element; + filter?: string; // one of the enumerated `filterTypes` for react-table + // (see https://github.com/tannerlinsley/react-table/blob/master/src/filterTypes.js) + // or one of the custom `filterTypes` defined for the `useTable` instance (see `DataTable`) +} + +interface IMunkiIssueTableData extends IMunkiIssue { + time: string; +} + +// NOTE: cellProps come from react-table +// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties +export const munkiIssuesTableHeaders: IDataColumn[] = [ + { + title: "Issue", + Header: (headerProps: IHeaderProps): JSX.Element => { + const titleWithToolTip = ( + + Issue + + ); + return ( + + ); + }, + disableSortBy: false, + accessor: "name", + Cell: (cellProps: IStringCellProps) => ( + + ), + sortType: "caseInsensitive", + }, + { + title: "Type", + Header: "Type", + disableSortBy: true, + accessor: "type", + Cell: (cellProps: IStringCellProps) => ( + + ), + }, + { + title: "Time", + Header: (headerProps: IHeaderProps): JSX.Element => { + const titleWithToolTip = ( + + Time + + ); + return ( + + ); + }, + disableSortBy: false, + accessor: "created_at", + Cell: (cellProps: IStringCellProps) => { + const time = abbreviateTimeUnits( + formatDistanceToNowStrict(new Date(cellProps.cell.value), { + addSuffix: true, + }) + ); + return ; + }, + }, +]; + +export default munkiIssuesTableHeaders; diff --git a/frontend/pages/hosts/details/cards/MunkiIssues/_styles.scss b/frontend/pages/hosts/details/cards/MunkiIssues/_styles.scss new file mode 100644 index 0000000000..af77de0d1e --- /dev/null +++ b/frontend/pages/hosts/details/cards/MunkiIssues/_styles.scss @@ -0,0 +1,27 @@ +.section--munki-issues { + .data-table-block { + .issue_tooltip, + .time_tooltip { + text-align: center; + } + + .data-table__table { + thead { + .type__header { + width: $col-sm; + } + + .time__header { + width: $col-md; + } + } + + tbody { + .name__cell { + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } +} diff --git a/frontend/pages/hosts/details/cards/MunkiIssues/index.ts b/frontend/pages/hosts/details/cards/MunkiIssues/index.ts new file mode 100644 index 0000000000..50f3eab998 --- /dev/null +++ b/frontend/pages/hosts/details/cards/MunkiIssues/index.ts @@ -0,0 +1 @@ +export { default } from "./MunkiIssues"; diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts index 3d3ad5a46d..d40d3283be 100644 --- a/frontend/services/entities/host_count.ts +++ b/frontend/services/entities/host_count.ts @@ -25,6 +25,7 @@ export interface IHostCountLoadOptions { softwareId?: number; mdmId?: number; mdmEnrollmentStatus?: string; + munkiIssueId?: number; os_id?: number; os_name?: string; os_version?: string; @@ -40,6 +41,7 @@ export default { const softwareId = options?.softwareId; const mdmId = options?.mdmId; const mdmEnrollmentStatus = options?.mdmEnrollmentStatus; + const munkiIssueId = options?.munkiIssueId; const label = getLabelParam(selectedLabels); const queryParams = { @@ -51,6 +53,7 @@ export default { policyResponse, mdmId, mdmEnrollmentStatus, + munkiIssueId, softwareId ), status: getStatusParam(selectedLabels), diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 3b7571adba..4601e2b9fa 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -29,6 +29,7 @@ export interface ILoadHostsOptions { osId?: number; osName?: string; osVersion?: string; + munkiIssueId?: number; device_mapping?: boolean; columns?: string; visibleColumns?: string; @@ -45,6 +46,7 @@ export interface IExportHostsOptions { policyResponse?: string; softwareId?: number; mdmId?: number; + munkiIssueId?: number; mdmEnrollmentStatus?: string; osId?: number; osName?: string; @@ -128,6 +130,7 @@ export default { const mdmEnrollmentStatus = options?.mdmEnrollmentStatus; const visibleColumns = options?.visibleColumns; const label = getLabelParam(selectedLabels); + const munkiIssueId = options?.munkiIssueId; if (!sortBy.length) { throw Error("sortBy is a required field."); @@ -144,6 +147,7 @@ export default { policyResponse, mdmId, mdmEnrollmentStatus, + munkiIssueId, softwareId ), status: getStatusParam(selectedLabels), @@ -168,6 +172,7 @@ export default { softwareId, mdmId, mdmEnrollmentStatus, + munkiIssueId, osId, osName, osVersion, @@ -192,6 +197,7 @@ export default { policyResponse, mdmId, mdmEnrollmentStatus, + munkiIssueId, softwareId, osId, osName, diff --git a/frontend/utilities/url/index.ts b/frontend/utilities/url/index.ts index cbcfdd28b6..51d5f84207 100644 --- a/frontend/utilities/url/index.ts +++ b/frontend/utilities/url/index.ts @@ -46,6 +46,7 @@ export const reconcileMutuallyExclusiveHostParams = ( policyResponse?: string, mdmId?: number, mdmEnrollmentStatus?: string, + munkiIssueId?: number, softwareId?: number, osId?: number, osName?: string, @@ -59,6 +60,8 @@ export const reconcileMutuallyExclusiveHostParams = ( return { policy_id: policyId, policy_response: policyResponse }; case !!mdmId: return { mdm_id: mdmId, mdm_status: mdmEnrollmentStatus }; + case !!munkiIssueId: + return { munki_issue_id: munkiIssueId }; case !!softwareId: return { software_id: softwareId }; case !!osId: