From 7db762f8e2852c72647e60627ba3d8e958a42688 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 12 Nov 2021 09:27:05 -0500 Subject: [PATCH] Homepage UI: Add host total count and host status count (#2894) --- .../issue-1995-homepage-various-host-counts | 1 + frontend/fleet/endpoints.ts | 12 +++-- frontend/interfaces/host_summary.ts | 7 +++ frontend/pages/Homepage/Homepage.tsx | 51 +++++++++++++++++- .../cards/HostsStatus/HostsStatus.tsx | 52 +++++++++++++++++++ .../Homepage/cards/HostsStatus/_styles.scss | 42 +++++++++++++++ .../pages/Homepage/cards/HostsStatus/index.ts | 1 + .../cards/HostsSummary/HostsSummary.tsx | 49 +++-------------- .../Homepage/components/InfoCard/InfoCard.tsx | 15 ++++-- .../Homepage/components/InfoCard/_styles.scss | 18 ++++++- frontend/services/entities/host_summary.ts | 11 ++++ 11 files changed, 208 insertions(+), 51 deletions(-) create mode 100644 changes/issue-1995-homepage-various-host-counts create mode 100644 frontend/pages/Homepage/cards/HostsStatus/HostsStatus.tsx create mode 100644 frontend/pages/Homepage/cards/HostsStatus/_styles.scss create mode 100644 frontend/pages/Homepage/cards/HostsStatus/index.ts create mode 100644 frontend/services/entities/host_summary.ts diff --git a/changes/issue-1995-homepage-various-host-counts b/changes/issue-1995-homepage-various-host-counts new file mode 100644 index 0000000000..745d823341 --- /dev/null +++ b/changes/issue-1995-homepage-various-host-counts @@ -0,0 +1 @@ +* Homepage UI now renders total count, online count, offline count, and new host count for all dashboards \ No newline at end of file diff --git a/frontend/fleet/endpoints.ts b/frontend/fleet/endpoints.ts index bc507db01c..e7a6167299 100644 --- a/frontend/fleet/endpoints.ts +++ b/frontend/fleet/endpoints.ts @@ -15,6 +15,10 @@ export default { FORGOT_PASSWORD: "/v1/fleet/forgot_password", GLOBAL_POLICIES: "/v1/fleet/global/policies", GLOBAL_SCHEDULE: "/v1/fleet/global/schedule", + HOST_SUMMARY: (teamId: number | undefined): string => { + const teamString = teamId ? `?team_id=${teamId}` : ""; + return `/v1/fleet/host_summary${teamString}`; + }, HOSTS: "/v1/fleet/hosts", HOSTS_COUNT: "/v1/fleet/hosts/count", HOSTS_DELETE: "/v1/fleet/hosts/delete", @@ -45,11 +49,11 @@ export default { STATUS_LIVE_QUERY: "/v1/fleet/status/live_query", STATUS_RESULT_STORE: "/v1/fleet/status/result_store", TARGETS: "/v1/fleet/targets", - TEAM_POLICIES: (id: number): string => { - return `/v1/fleet/teams/${id}/policies`; + TEAM_POLICIES: (teamId: number): string => { + return `/v1/fleet/teams/${teamId}/policies`; }, - TEAM_SCHEDULE: (id: number): string => { - return `/v1/fleet/teams/${id}/schedule`; + TEAM_SCHEDULE: (teamId: number): string => { + return `/v1/fleet/teams/${teamId}/schedule`; }, TEAMS: "/v1/fleet/teams", TEAMS_MEMBERS: (teamId: number): string => { diff --git a/frontend/interfaces/host_summary.ts b/frontend/interfaces/host_summary.ts index 47476df0d9..a41a52cd29 100644 --- a/frontend/interfaces/host_summary.ts +++ b/frontend/interfaces/host_summary.ts @@ -7,7 +7,14 @@ export default PropTypes.shape({ new_count: PropTypes.number, }); +export interface IHostSummaryPlatforms { + platform: string; + hosts_count: number; +} + export interface IHostSummary { + totals_hosts_count: number; + platforms: IHostSummaryPlatforms[] | null; online_count: number; offline_count: number; mia_count: number; diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx index 5609b9a548..dac4e78434 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -5,13 +5,16 @@ import { Link } from "react-router"; import { AppContext } from "context/app"; import { find } from "lodash"; +import hostSummaryAPI from "services/entities/host_summary"; import teamsAPI from "services/entities/teams"; -import { ITeam } from "interfaces/team"; +import { IHostSummary, IHostSummaryPlatforms } from "interfaces/host_summary"; import { ISoftware } from "interfaces/software"; +import { ITeam } from "interfaces/team"; import TeamsDropdown from "components/TeamsDropdown"; import Button from "components/buttons/Button"; import InfoCard from "./components/InfoCard"; +import HostsStatus from "./cards/HostsStatus"; import HostsSummary from "./cards/HostsSummary"; import ActivityFeed from "./cards/ActivityFeed"; import Software from "./cards/Software"; @@ -44,6 +47,12 @@ const Homepage = (): JSX.Element => { const [isSoftwareModalOpen, setIsSoftwareModalOpen] = useState( false ); + const [totalCount, setTotalCount] = useState(); + const [macCount, setMacCount] = useState("0"); + const [windowsCount, setWindowsCount] = useState("0"); + const [onlineCount, setOnlineCount] = useState(); + const [offlineCount, setOfflineCount] = useState(); + const [newCount, setNewCount] = useState(); const { data: teams, isLoading: isLoadingTeams } = useQuery< ITeamsResponse, @@ -59,6 +68,30 @@ const Homepage = (): JSX.Element => { setCurrentTeam(selectedTeam); }; + useQuery( + ["host summary", currentTeam], + () => { + return hostSummaryAPI.getSummary(currentTeam?.id); + }, + { + select: (data: IHostSummary) => data, + onSuccess: (data: any) => { + setTotalCount(data.totals_hosts_count.toLocaleString("en-US")); + setOnlineCount(data.online_count.toLocaleString("en-US")); + setOfflineCount(data.offline_count.toLocaleString("en-US")); + setNewCount(data.new_count.toLocaleString("en-US")); + const macHosts = data.platforms?.find( + (platform: IHostSummaryPlatforms) => platform.platform === "darwin" + ) || { platform: "darwin", hosts_count: 0 }; + setMacCount(macHosts.hosts_count.toLocaleString("en-US")); + const windowsHosts = data.platforms?.find( + (platform: IHostSummaryPlatforms) => platform.platform === "windows" + ) || { platform: "windows", hosts_count: 0 }; + setWindowsCount(windowsHosts.hosts_count.toLocaleString("en-US")); + }, + } + ); + return (
@@ -88,8 +121,22 @@ const Homepage = (): JSX.Element => { MANAGE_HOSTS + TAGGED_TEMPLATES.hostsByTeamRoute(currentTeam?.id), text: "View all hosts", }} + total_host_count={totalCount} > - + + +
+
+ +
{isPreviewMode && ( diff --git a/frontend/pages/Homepage/cards/HostsStatus/HostsStatus.tsx b/frontend/pages/Homepage/cards/HostsStatus/HostsStatus.tsx new file mode 100644 index 0000000000..3a1d0e57b7 --- /dev/null +++ b/frontend/pages/Homepage/cards/HostsStatus/HostsStatus.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +const baseClass = "hosts-status"; + +interface IHostSummaryProps { + onlineCount: string | undefined; + offlineCount: string | undefined; + newCount: string | undefined; +} + +const HostsStatus = ({ + onlineCount, + offlineCount, + newCount, +}: IHostSummaryProps): JSX.Element => { + return ( +
+
+
+
+ {onlineCount} +
+
Online hosts
+
+
+
+
+
+ {offlineCount} +
+
Offline hosts
+
+
+
+
+
+ {newCount} +
+
New hosts
+
+
+
+ ); +}; + +export default HostsStatus; diff --git a/frontend/pages/Homepage/cards/HostsStatus/_styles.scss b/frontend/pages/Homepage/cards/HostsStatus/_styles.scss new file mode 100644 index 0000000000..77a64de98d --- /dev/null +++ b/frontend/pages/Homepage/cards/HostsStatus/_styles.scss @@ -0,0 +1,42 @@ +.hosts-status { + width: 100%; + display: flex; + justify-content: space-around; + font-size: $x-small; + + &__tile { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + + &:first-of-type { + justify-content: flex-end; + } + &:last-of-type { + justify-content: flex-start; + } + } + + &__tile-count { + font-size: $large; + + &:before { + border-radius: 100%; + content: " "; + display: inline-block; + margin-right: $pad-small; + height: 8px; + width: 8px; + margin-bottom: 5px; + } + + &--online:before { + background-color: $ui-success; + } + + &--offline:before { + background-color: $ui-offline; + } + } +} diff --git a/frontend/pages/Homepage/cards/HostsStatus/index.ts b/frontend/pages/Homepage/cards/HostsStatus/index.ts new file mode 100644 index 0000000000..78946c8f54 --- /dev/null +++ b/frontend/pages/Homepage/cards/HostsStatus/index.ts @@ -0,0 +1 @@ +export { default } from "./HostsStatus"; diff --git a/frontend/pages/Homepage/cards/HostsSummary/HostsSummary.tsx b/frontend/pages/Homepage/cards/HostsSummary/HostsSummary.tsx index 405cc7ee6d..45c45ee85b 100644 --- a/frontend/pages/Homepage/cards/HostsSummary/HostsSummary.tsx +++ b/frontend/pages/Homepage/cards/HostsSummary/HostsSummary.tsx @@ -11,8 +11,10 @@ import MacIcon from "../../../../../assets/images/icon-mac-48x48@2x.png"; const baseClass = "hosts-summary"; -interface IHostsSummaryProps { +interface IHostSummaryProps { currentTeamId: number | undefined; + macCount: string | undefined; + windowsCount: string | undefined; } interface ILabelsResponse { @@ -23,9 +25,11 @@ interface IHostCountResponse { count: number; } -const HostsSummary = ({ currentTeamId }: IHostsSummaryProps): JSX.Element => { - const [macCount, setMacCount] = useState(); - const [windowsCount, setWindowsCount] = useState(); +const HostsSummary = ({ + currentTeamId, + macCount, + windowsCount, +}: IHostSummaryProps): JSX.Element => { const [linuxCount, setLinuxCount] = useState(); const getLabel = (labelString: string, labels: ILabel[]) => { @@ -41,43 +45,6 @@ const HostsSummary = ({ currentTeamId }: IHostsSummaryProps): JSX.Element => { } ); - useQuery( - ["mac host count", currentTeamId], - () => { - const macOsLabel = getLabel("macOS", labels || []); - return ( - hostCountAPI.load({ - selectedLabels: [`labels/${macOsLabel[0].id}`], - teamId: currentTeamId, - }) || { count: 0 } - ); - }, - { - select: (data: IHostCountResponse) => data.count, - enabled: !!labels, - onSuccess: (data: number) => setMacCount(data.toLocaleString("en-US")), - } - ); - - useQuery( - ["windows host count", currentTeamId], - () => { - const windowsLabel = getLabel("MS Windows", labels || []); - return ( - hostCountAPI.load({ - selectedLabels: [`labels/${windowsLabel[0].id}`], - teamId: currentTeamId, - }) || { count: 0 } - ); - }, - { - select: (data: IHostCountResponse) => data.count, - enabled: !!labels, - onSuccess: (data: number) => - setWindowsCount(data.toLocaleString("en-US")), - } - ); - useQuery( ["linux host count", currentTeamId], () => { diff --git a/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx b/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx index c387598020..68091f9303 100644 --- a/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx +++ b/frontend/pages/Homepage/components/InfoCard/InfoCard.tsx @@ -18,11 +18,17 @@ interface IInfoCardProps { text: string; onClick?: () => void; }; + total_host_count?: string; } const baseClass = "homepage-info-card"; -const InfoCard = ({ title, children, action }: IInfoCardProps) => { +const InfoCard = ({ + title, + children, + action, + total_host_count, +}: IInfoCardProps) => { const renderAction = () => { if (action) { if (action.type === "button") { @@ -53,8 +59,11 @@ const InfoCard = ({ title, children, action }: IInfoCardProps) => { return (
-
-

{title}

+
+
+

{title}

+ {total_host_count && {total_host_count}} +
{renderAction()}
{children} diff --git a/frontend/pages/Homepage/components/InfoCard/_styles.scss b/frontend/pages/Homepage/components/InfoCard/_styles.scss index 22f364d52c..2095e63251 100644 --- a/frontend/pages/Homepage/components/InfoCard/_styles.scss +++ b/frontend/pages/Homepage/components/InfoCard/_styles.scss @@ -7,7 +7,7 @@ box-shadow: 0 2px 0 0 $ui-fleet-blue-15; box-sizing: border-box; - &__section-title { + &__section-title-cta { display: flex; justify-content: space-between; @@ -16,6 +16,22 @@ } } + &__section-title { + display: flex; + flex-direction: row; + align-items: center; + + span { + background-color: $core-vibrant-blue; + color: $core-white; + font-size: $xx-small; + font-weight: $bold; + padding: 2px 4px; + border-radius: 4px; + margin-left: $pad-small; + } + } + &__action-button { display: flex; align-items: center; diff --git a/frontend/services/entities/host_summary.ts b/frontend/services/entities/host_summary.ts new file mode 100644 index 0000000000..8a978b86bc --- /dev/null +++ b/frontend/services/entities/host_summary.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import sendRequest from "services"; +import endpoints from "fleet/endpoints"; + +export default { + getSummary: (teamId: number | undefined) => { + const { HOST_SUMMARY } = endpoints; + + return sendRequest("GET", HOST_SUMMARY(teamId)); + }, +};