From 78ca68d13dd26319ce86aa8b6ed25227b027ede3 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:12:12 -0400 Subject: [PATCH] Fleet UI: Munki title on homepage, hide entire card when no munki_versions installed on team/global (#7646) --- cypress/integration/free/admin.spec.ts | 1 - cypress/integration/free/maintainer.spec.ts | 1 - cypress/integration/free/observer.spec.ts | 1 - .../integration/premium/maintainer.spec.ts | 1 - cypress/integration/premium/observer.spec.ts | 1 - .../integration/premium/team_admin.spec.ts | 1 - .../premium/team_maintainer_observer.spec.ts | 1 - frontend/interfaces/macadmins.ts | 10 +- frontend/pages/Homepage/Homepage.tsx | 119 ++++++++++++++++-- frontend/pages/Homepage/cards/MDM/MDM.tsx | 104 ++++----------- .../cards/MDM/MDMEnrollmentTableConfig.tsx | 4 +- .../cards/MDM/MDMSolutionsTableConfig.tsx | 10 +- frontend/pages/Homepage/cards/Munki/Munki.tsx | 67 +++------- .../Homepage/components/InfoCard/InfoCard.tsx | 12 +- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 4 +- 15 files changed, 170 insertions(+), 167 deletions(-) diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index ff234abf40..36d3e12fa9 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -100,7 +100,6 @@ describe( cy.findByText(/fleet test/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index 9b5dae075b..d8848b9e2d 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -92,7 +92,6 @@ describe( cy.findByText(/fleet test/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index d060ed64e3..e1e8afa676 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -87,7 +87,6 @@ describe("Free tier - Observer user", () => { cy.findByText(/fleet test/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts index a68354d2b9..c67b7dde1f 100644 --- a/cypress/integration/premium/maintainer.spec.ts +++ b/cypress/integration/premium/maintainer.spec.ts @@ -84,7 +84,6 @@ describe("Premium tier - Maintainer user", () => { cy.findByText(/all teams/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts index 2c24c8870a..b4b1ecae66 100644 --- a/cypress/integration/premium/observer.spec.ts +++ b/cypress/integration/premium/observer.spec.ts @@ -85,7 +85,6 @@ describe("Premium tier - Observer user", () => { cy.findByText(/all teams/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/premium/team_admin.spec.ts b/cypress/integration/premium/team_admin.spec.ts index 0424a191f5..b57df11b9b 100644 --- a/cypress/integration/premium/team_admin.spec.ts +++ b/cypress/integration/premium/team_admin.spec.ts @@ -90,7 +90,6 @@ describe("Premium tier - Team Admin user", () => { cy.findByText(/apples/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/cypress/integration/premium/team_maintainer_observer.spec.ts b/cypress/integration/premium/team_maintainer_observer.spec.ts index 7e5c351c6e..5b13fe4abc 100644 --- a/cypress/integration/premium/team_maintainer_observer.spec.ts +++ b/cypress/integration/premium/team_maintainer_observer.spec.ts @@ -85,7 +85,6 @@ describe("Premium tier - Team observer/maintainer user", () => { cy.findByText(/apples/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); cy.getAttached(".hosts-status").should("exist"); - cy.getAttached(".home-munki").should("exist"); cy.getAttached(".home-mdm").should("exist"); // "get" because we expect it not to exist cy.get(".home-software").should("not.exist"); diff --git a/frontend/interfaces/macadmins.ts b/frontend/interfaces/macadmins.ts index 7532b78c24..9c9f2f3f38 100644 --- a/frontend/interfaces/macadmins.ts +++ b/frontend/interfaces/macadmins.ts @@ -1,4 +1,4 @@ -export interface IDataTableMDMFormat { +export interface IDataTableMdmFormat { status: string; hosts: number; } @@ -14,13 +14,13 @@ export interface IMunkiIssuesAggregate { type: "error" | "warning"; hosts_count: number; } -export interface IMDMAggregateStatus { +export interface IMdmAggregateStatus { enrolled_manual_hosts_count: number; enrolled_automated_hosts_count: number; unenrolled_hosts_count: number; } -export interface IMDMSolution { +export interface IMdmSolution { id: number; name: string | null; server_url: string; @@ -32,7 +32,7 @@ export interface IMacadminAggregate { counts_updated_at: string; munki_versions: IMunkiVersionsAggregate[]; munki_issues: IMunkiIssuesAggregate[]; - mobile_device_management_enrollment_status: IMDMAggregateStatus; - mobile_device_management_solution: IMDMSolution[] | null; + 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 74978033c4..ad229f52a4 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -9,10 +9,18 @@ import { } from "interfaces/enroll_secret"; import { IHostSummary, IHostSummaryPlatforms } from "interfaces/host_summary"; import { ILabelSummary } from "interfaces/label"; +import { + IDataTableMdmFormat, + IMdmSolution, + IMacadminAggregate, + IMunkiIssuesAggregate, + IMunkiVersionsAggregate, +} from "interfaces/macadmins"; import { IOsqueryPlatform } from "interfaces/platform"; import { ITeam } from "interfaces/team"; import enrollSecretsAPI from "services/entities/enroll_secret"; import hostSummaryAPI from "services/entities/host_summary"; +import macadminsAPI from "services/entities/macadmins"; import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import sortUtils from "utilities/sort"; import { PLATFORM_DROPDOWN_OPTIONS } from "utilities/constants"; @@ -22,6 +30,7 @@ import Spinner from "components/Spinner"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; import MainContent from "components/MainContent"; +import LastUpdatedText from "components/LastUpdatedText"; import useInfoCard from "./components/InfoCard"; import HostsStatus from "./cards/HostsStatus"; import HostsSummary from "./cards/HostsSummary"; @@ -29,7 +38,7 @@ import ActivityFeed from "./cards/ActivityFeed"; import Software from "./cards/Software"; import LearnFleet from "./cards/LearnFleet"; import WelcomeHost from "./cards/WelcomeHost"; -import MDM from "./cards/MDM"; +import Mdm from "./cards/MDM"; import Munki from "./cards/Munki"; import OperatingSystems from "./cards/OperatingSystems"; import AddHostsModal from "../../components/AddHostsModal"; @@ -62,10 +71,28 @@ const Homepage = (): JSX.Element => { const [showActivityFeedTitle, setShowActivityFeedTitle] = useState(false); const [showSoftwareUI, setShowSoftwareUI] = useState(false); const [showMunkiUI, setShowMunkiUI] = useState(false); + const [showMunkiCard, setShowMunkiCard] = useState(true); const [showMDMUI, setShowMDMUI] = useState(false); const [showAddHostsModal, setShowAddHostsModal] = useState(false); const [showOperatingSystemsUI, setShowOperatingSystemsUI] = useState(false); const [showHostsUI, setShowHostsUI] = useState(false); // Hides UI on first load only + const [formattedMdmData, setFormattedMdmData] = useState< + IDataTableMdmFormat[] + >([]); + const [mdmSolutions, setMdmSolutions] = useState([]); + + const [munkiIssuesData, setMunkiIssuesData] = useState< + IMunkiIssuesAggregate[] + >([]); + const [munkiVersionsData, setMunkiVersionsData] = useState< + IMunkiVersionsAggregate[] + >([]); + const [mdmTitleDetail, setMdmTitleDetail] = useState< + JSX.Element | string | null + >(); + const [munkiTitleDetail, setMunkiTitleDetail] = useState< + JSX.Element | string | null + >(); const canEnrollHosts = isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer; @@ -147,6 +174,68 @@ const Homepage = (): JSX.Element => { } ); + const { isFetching: isMacAdminsFetching, error: errorMacAdmins } = useQuery< + IMacadminAggregate, + Error + >( + ["macAdmins", currentTeam?.id], + () => macadminsAPI.loadAll(currentTeam?.id), + { + keepPreviousData: true, + enabled: selectedPlatform === "darwin", + onSuccess: (data) => { + const { + counts_updated_at: macadmins_counts_updated_at, + mobile_device_management_enrollment_status, + mobile_device_management_solution, + } = data.macadmins; + const { + enrolled_manual_hosts_count, + enrolled_automated_hosts_count, + unenrolled_hosts_count, + } = mobile_device_management_enrollment_status; + + const { + counts_updated_at: munki_counts_updated_at, + munki_versions, + munki_issues, + } = data.macadmins; + + setMdmTitleDetail( + + ); + setFormattedMdmData([ + { + status: "Enrolled (manual)", + hosts: enrolled_manual_hosts_count, + }, + { + status: "Enrolled (automatic)", + hosts: enrolled_automated_hosts_count, + }, + { status: "Unenrolled", hosts: unenrolled_hosts_count }, + ]); + setMdmSolutions(mobile_device_management_solution); + setMunkiVersionsData(munki_versions); + setMunkiIssuesData(munki_issues); + setShowMunkiCard(!!munki_versions); + setMunkiTitleDetail( + + ); + }, + onError: () => { + setShowMDMUI(true); + setShowMunkiUI(true); + }, + } + ); + const handleTeamSelect = (teamId: number) => { const selectedTeam = find(teams, ["id", teamId]); setCurrentTeam(selectedTeam); @@ -242,8 +331,9 @@ const Homepage = (): JSX.Element => { }); const MunkiCard = useInfoCard({ - title: "Munki versions", - showTitle: showMunkiUI, + title: "Munki", + titleDetail: munkiTitleDetail, + showTitle: !isMacAdminsFetching, description: (

Munki is a tool for managing software on macOS devices.{" "} @@ -259,16 +349,18 @@ const Homepage = (): JSX.Element => { ), children: ( ), }); const MDMCard = useInfoCard({ title: "Mobile device management (MDM)", - showTitle: showMDMUI, + titleDetail: mdmTitleDetail, + showTitle: !isMacAdminsFetching, description: (

MDM is used to manage configuration on macOS devices.{" "} @@ -283,10 +375,11 @@ const Homepage = (): JSX.Element => {

), children: ( - ), }); @@ -326,7 +419,9 @@ const Homepage = (): JSX.Element => { <>
{OperatingSystemsCard}
{MDMCard}
-
{MunkiCard}
+ {showMunkiCard && ( +
{MunkiCard}
+ )} ); diff --git a/frontend/pages/Homepage/cards/MDM/MDM.tsx b/frontend/pages/Homepage/cards/MDM/MDM.tsx index 65091ee8c0..04317a12a7 100644 --- a/frontend/pages/Homepage/cards/MDM/MDM.tsx +++ b/frontend/pages/Homepage/cards/MDM/MDM.tsx @@ -1,30 +1,23 @@ 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, - IDataTableMDMFormat, - IMDMSolution, -} from "interfaces/macadmins"; +import { IDataTableMdmFormat, IMdmSolution } 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 { generateSolutionsTableHeaders, generateSolutionsDataSet, } from "./MDMSolutionsTableConfig"; import generateEnrollmentTableHeaders from "./MDMEnrollmentTableConfig"; -interface IMDMCardProps { - showMDMUI: boolean; - currentTeamId: number | undefined; - setShowMDMUI: (showMDMTitle: boolean) => void; - setTitleDetail?: (content: JSX.Element | string | null) => void; +interface IMdmCardProps { + errorMacAdmins: Error | null; + isMacAdminsFetching: boolean; + formattedMdmData: IDataTableMdmFormat[]; + mdmSolutions: IMdmSolution[] | null; } const DEFAULT_SORT_DIRECTION = "desc"; @@ -34,7 +27,7 @@ const ENROLLMENT_DEFAULT_SORT_HEADER = "status"; const PAGE_SIZE = 8; const baseClass = "home-mdm"; -const EmptyMDMEnrollment = (): JSX.Element => ( +const EmptyMdmEnrollment = (): JSX.Element => (

Unable to detect MDM enrollment

@@ -51,7 +44,7 @@ const EmptyMDMEnrollment = (): JSX.Element => (

); -const EmptyMDMSolutions = (): JSX.Element => ( +const EmptyMdmSolutions = (): JSX.Element => (

No MDM solutions detected

@@ -61,60 +54,13 @@ const EmptyMDMSolutions = (): JSX.Element => (

); -const MDM = ({ - showMDMUI, - currentTeamId, - setShowMDMUI, - setTitleDetail, -}: IMDMCardProps): JSX.Element => { +const Mdm = ({ + isMacAdminsFetching, + errorMacAdmins, + formattedMdmData, + mdmSolutions, +}: IMdmCardProps): JSX.Element => { const [navTabIndex, setNavTabIndex] = useState(0); - const [formattedMDMData, setFormattedMDMData] = useState< - IDataTableMDMFormat[] - >([]); - const [solutions, setSolutions] = useState([]); - - const { isFetching: isMDMFetching, error: errorMDM } = useQuery< - IMacadminAggregate, - Error - >(["MDM", currentTeamId], () => macadminsAPI.loadAll(currentTeamId), { - keepPreviousData: true, - onSuccess: (data) => { - const { - counts_updated_at, - mobile_device_management_enrollment_status, - mobile_device_management_solution, - } = data.macadmins; - const { - enrolled_manual_hosts_count, - enrolled_automated_hosts_count, - unenrolled_hosts_count, - } = mobile_device_management_enrollment_status; - - setShowMDMUI(true); - setTitleDetail && - setTitleDetail( - - ); - setFormattedMDMData([ - { - status: "Enrolled (manual)", - hosts: enrolled_manual_hosts_count, - }, - { - status: "Enrolled (automatic)", - hosts: enrolled_automated_hosts_count, - }, - { status: "Unenrolled", hosts: unenrolled_hosts_count }, - ]); - setSolutions(mobile_device_management_solution); - }, - onError: () => { - setShowMDMUI(true); - }, - }); const onTabChange = (index: number) => { setNavTabIndex(index); @@ -122,14 +68,14 @@ const MDM = ({ const solutionsTableHeaders = generateSolutionsTableHeaders(); const enrollmentTableHeaders = generateEnrollmentTableHeaders(); - const solutionsDataSet = generateSolutionsDataSet(solutions); + const solutionsDataSet = generateSolutionsDataSet(mdmSolutions); // Renders opaque information as host information is loading - const opacity = showMDMUI ? { opacity: 1 } : { opacity: 0 }; + const opacity = isMacAdminsFetching ? { opacity: 0 } : { opacity: 1 }; return (
- {!showMDMUI && ( + {isMacAdminsFetching && (
@@ -142,18 +88,18 @@ const MDM = ({ Enrollment - {errorMDM ? ( + {errorMacAdmins ? ( ) : ( - {errorMDM ? ( + {errorMacAdmins ? ( ) : ( { return solutionsTableHeaders; }; -const enhanceSolutionsData = (solutions: IMDMSolution[]): IMDMSolution[] => { +const enhanceSolutionsData = (solutions: IMdmSolution[]): IMdmSolution[] => { return Object.values(solutions).map((solution) => { return { id: solution.id, @@ -110,8 +110,8 @@ const enhanceSolutionsData = (solutions: IMDMSolution[]): IMDMSolution[] => { }; export const generateSolutionsDataSet = ( - solutions: IMDMSolution[] | null -): IMDMSolution[] => { + solutions: IMdmSolution[] | null +): IMdmSolution[] => { if (!solutions) { return []; } diff --git a/frontend/pages/Homepage/cards/Munki/Munki.tsx b/frontend/pages/Homepage/cards/Munki/Munki.tsx index b8ad40244e..b03daa3333 100644 --- a/frontend/pages/Homepage/cards/Munki/Munki.tsx +++ b/frontend/pages/Homepage/cards/Munki/Munki.tsx @@ -1,10 +1,7 @@ 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, IMunkiIssuesAggregate, IMunkiVersionsAggregate, } from "interfaces/macadmins"; @@ -13,15 +10,14 @@ 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 munkiVersionsTableHeaders from "./MunkiVersionsTableConfig"; import munkiIssuesTableHeaders from "./MunkiIssuesTableConfig"; interface IMunkiCardProps { - showMunkiUI: boolean; - currentTeamId: number | undefined; - setShowMunkiUI: (showMunkiTitle: boolean) => void; - setTitleDetail?: (content: JSX.Element | string | null) => void; + errorMacAdmins: Error | null; + isMacAdminsFetching: boolean; + munkiIssuesData: IMunkiIssuesAggregate[]; + munkiVersionsData: IMunkiVersionsAggregate[]; } const DEFAULT_SORT_DIRECTION = "desc"; @@ -57,58 +53,23 @@ const EmptyMunkiVersions = (): JSX.Element => ( ); const Munki = ({ - showMunkiUI, - currentTeamId, - setShowMunkiUI, - setTitleDetail, + errorMacAdmins, + isMacAdminsFetching, + munkiIssuesData, + munkiVersionsData, }: IMunkiCardProps): JSX.Element => { 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, - Error - >(["munki", currentTeamId], () => macadminsAPI.loadAll(currentTeamId), { - keepPreviousData: true, - onSuccess: (data) => { - const { - counts_updated_at, - munki_versions, - munki_issues, - } = data.macadmins; - - setMunkiVersionsData(munki_versions); - setMunkiIssuesData(munki_issues); - setShowMunkiUI(true); - setTitleDetail && - setTitleDetail( - - ); - }, - onError: () => { - setShowMunkiUI(true); - }, - }); const onTabChange = (index: number) => { setNavTabIndex(index); }; // Renders opaque information as host information is loading - const opacity = showMunkiUI ? { opacity: 1 } : { opacity: 0 }; + const opacity = isMacAdminsFetching ? { opacity: 0 } : { opacity: 1 }; return (
- {!showMunkiUI && ( + {isMacAdminsFetching && (
@@ -121,13 +82,13 @@ const Munki = ({ Versions - {errorMunki ? ( + {errorMacAdmins ? ( ) : ( - {errorMunki ? ( + {errorMacAdmins ? ( ) : ( { const [actionLink, setActionURL] = useState(null); const [titleDetail, setTitleDetail] = useState( - null + defaultTitleDetail || null ); const [description, setDescription] = useState( defaultDescription || null ); + useEffect(() => { + if (defaultTitleDetail) { + setTitleDetail(defaultTitleDetail); + } + }, [defaultTitleDetail]); + const renderAction = () => { if (action) { if (action.type === "button") { diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index ed9498e0d5..3ed0a208ff 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, IMunkiIssuesAggregate } from "interfaces/macadmins"; +import { IMdmSolution, IMunkiIssuesAggregate } from "interfaces/macadmins"; import { formatOperatingSystemDisplayName, IOperatingSystemVersion, @@ -225,7 +225,7 @@ const ManageHostsPage = ({ const [ mdmSolutionDetails, setMDMSolutionDetails, - ] = useState(null); + ] = useState(null); const [ munkiIssueDetails, setMunkiIssueDetails,