Fleet UI: Dashboard summary tiles clickable with new component (#7826)

This commit is contained in:
RachelElysia
2022-09-19 16:47:43 -04:00
committed by GitHub
parent f98e9885ef
commit 299a4e54fc
14 changed files with 335 additions and 85 deletions
+2 -1
View File
@@ -9,6 +9,7 @@
button {
color: $core-vibrant-blue;
font-weight: $bold;
padding: 6px;
.fleeticon-chevronleft {
@@ -35,7 +36,7 @@
}
button:hover,
button.focus-visible {
button:focus {
background-color: $ui-vibrant-blue-10;
}
@@ -294,15 +294,12 @@ $base-class: "button";
padding: 0;
height: auto;
line-height: normal;
font-weight: normal;
&:active {
box-shadow: none;
top: 0;
}
&:focus {
outline: none;
}
}
&--unstyled-modal-query {
@@ -67,6 +67,10 @@
position: absolute;
bottom: 0px;
right: 0px;
.button {
font-weight: $bold;
}
}
&__load-activities-button {
@@ -1,5 +1,8 @@
import React from "react";
import paths from "router/paths";
import SummaryTile from "../HostsSummary/SummaryTile";
const baseClass = "hosts-status";
interface IHostSummaryProps {
@@ -23,26 +26,20 @@ const HostsStatus = ({
return (
<div className={baseClass} style={opacity}>
<div className={`${baseClass}__tile online-tile`}>
<div>
<div
className={`${baseClass}__tile-count ${baseClass}__tile-count--online`}
>
{onlineCount}
</div>
<div className={`${baseClass}__tile-description`}>Online hosts</div>
</div>
</div>
<div className={`${baseClass}__tile offline-tile`}>
<div>
<div
className={`${baseClass}__tile-count ${baseClass}__tile-count--offline`}
>
{offlineCount}
</div>
<div className={`${baseClass}__tile-description`}>Offline hosts</div>
</div>
</div>
<SummaryTile
count={onlineCount}
isLoading={isLoadingHosts}
showUI={showHostsUI}
title="Online hosts"
path={paths.MANAGE_HOSTS_ONLINE}
/>
<SummaryTile
count={offlineCount}
isLoading={isLoadingHosts}
showUI={showHostsUI}
title="Offline hosts"
path={paths.MANAGE_HOSTS_OFFLINE}
/>
</div>
);
};
@@ -3,33 +3,4 @@
display: flex;
justify-content: space-around;
font-size: $x-small;
&__tile {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
}
&__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;
}
}
}
@@ -5,6 +5,7 @@ import { ILabelSummary } from "interfaces/label";
import { PLATFORM_NAME_TO_LABEL_NAME } from "utilities/constants";
import DataError from "components/DataError";
import SummaryTile from "./SummaryTile";
import WindowsIcon from "../../../../../assets/images/icon-windows-48x48@2x.png";
import LinuxIcon from "../../../../../assets/images/icon-linux-48x48@2x.png";
@@ -76,43 +77,36 @@ const HostsSummary = ({
}
const renderMacCount = () => (
<div className={`${baseClass}__tile mac-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={MacIcon} alt="mac icon" id="mac-icon" />
</div>
<div>
<div className={`${baseClass}__tile-count mac-count`}>{macCount}</div>
<div className={`${baseClass}__tile-description`}>macOS hosts</div>
</div>
</div>
<SummaryTile
icon={MacIcon}
count={macCount}
isLoading={isLoadingHostsSummary}
showUI={showHostsUI}
title="macOS hosts"
path={paths.MANAGE_HOSTS_LABEL(7)}
/>
);
const renderWindowsCount = () => (
<div className={`${baseClass}__tile windows-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={WindowsIcon} alt="windows icon" id="windows-icon" />
</div>
<div>
<div className={`${baseClass}__tile-count windows-count`}>
{windowsCount}
</div>
<div className={`${baseClass}__tile-description`}>Windows hosts</div>
</div>
</div>
<SummaryTile
icon={WindowsIcon}
count={windowsCount}
isLoading={isLoadingHostsSummary}
showUI={showHostsUI}
title="Windows hosts"
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
const renderLinuxCount = () => (
<div className={`${baseClass}__tile linux-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={LinuxIcon} alt="linux icon" id="linux-icon" />
</div>
<div>
<div className={`${baseClass}__tile-count linux-count`}>
{linuxCount}
</div>
<div className={`${baseClass}__tile-description`}>Linux hosts</div>
</div>
</div>
<SummaryTile
icon={LinuxIcon}
count={linuxCount}
isLoading={isLoadingHostsSummary}
showUI={showHostsUI}
title="Linux hosts"
path={paths.MANAGE_HOSTS_LABEL(12)}
/>
);
const renderCounts = () => {
@@ -0,0 +1,115 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import paths from "router/paths";
import SummaryTile from "./SummaryTile";
import TestIcon from "../../../../../../assets/images/icon-windows-black-24x24@2x.png";
const INITIAL_OPACITY = 0;
const LOADING_OPACITY = 0.4;
describe("SummaryTile - component", () => {
it("summary tile is hidden when showUI is false on first load", () => {
render(
<SummaryTile
count={200}
isLoading={false}
showUI={false} // being tested
title={"Windows hosts"}
icon={TestIcon}
tooltip={"Hosts on any Windows device"}
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
const tile = screen.getByTestId("tile");
expect(tile).toHaveStyle(`opacity: ${INITIAL_OPACITY}`);
});
it("renders loading state", () => {
render(
<SummaryTile
count={200}
isLoading // tested
showUI
title={"Windows hosts"}
icon={TestIcon}
tooltip={"Hosts on any Windows device"}
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
const tile = screen.getByTestId("tile");
expect(tile).toHaveStyle(`opacity: ${LOADING_OPACITY}`);
});
it("renders title, count, and image based on the information and data passed in", () => {
render(
<SummaryTile
count={200} // tested
isLoading={false}
showUI
title={"Windows hosts"} // tested
icon={TestIcon} // tested
tooltip={"Hosts on any Windows device"}
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
const title = screen.getByText("Windows hosts");
const count = screen.getByText("200");
const icon = screen.getByRole("img");
expect(title).toBeInTheDocument();
expect(count).toBeInTheDocument();
expect(icon).toHaveAttribute("src", "test-file-stub");
});
it("renders tooltip on title hover", async () => {
render(
<SummaryTile
count={200}
isLoading={false}
showUI
title={"Windows hosts"}
icon={TestIcon}
tooltip={"Hosts on any Windows device"} // tested
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
fireEvent.mouseOver(screen.getByText("Windows hosts"));
expect(
await screen.findByText("Hosts on any Windows device")
).toBeInTheDocument();
});
it("renders manage host page on click", async () => {
render(
<SummaryTile
count={200}
isLoading={false}
showUI
title={"Windows hosts"}
icon={TestIcon}
tooltip={"Hosts on any Windows device"} // tested
path={paths.MANAGE_HOSTS_LABEL(10)}
/>
);
fireEvent.click(screen.getByText("Windows hosts"));
expect(window.location.pathname).toBe("/hosts/manage/labels/10");
});
});
@@ -0,0 +1,82 @@
import React from "react";
import { browserHistory } from "react-router";
import Button from "components/buttons/Button";
import { kebabCase } from "lodash";
import TooltipWrapper from "components/TooltipWrapper";
interface ISummaryTileProps {
count: number;
isLoading: boolean;
showUI: boolean;
title: string;
icon?: any;
tooltip?: string;
path: string;
}
const baseClass = "summary-tile";
const SummaryTile = ({
count,
isLoading,
showUI, // false on first load only
title,
icon,
tooltip,
path,
}: ISummaryTileProps): JSX.Element => {
const numberWithCommas = (x: number): string => {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
// Renders opaque information as host information is loading
let opacity = { opacity: 0 };
if (showUI) {
opacity = isLoading ? { opacity: 0.4 } : { opacity: 1 };
}
const handleClick = () => {
browserHistory.push(path);
};
return (
<div className={baseClass} style={opacity} data-testid="tile">
<Button
className={`${baseClass}__tile ${kebabCase(title)}-tile`}
variant="unstyled"
onClick={() => handleClick()}
>
<>
{icon && (
<div className={`${baseClass}__icon-wrapper`}>
<img
src={icon}
alt={title}
className={`${baseClass}__icon`}
id={`${kebabCase(title)}-icon`}
/>
</div>
)}
<div>
<div
className={`${baseClass}__count ${baseClass}__count--${kebabCase(
title
)}`}
>
{numberWithCommas(count)}
</div>
<div className={`${baseClass}__description`}>
{tooltip ? (
<TooltipWrapper tipContent={tooltip}>{title}</TooltipWrapper>
) : (
title
)}
</div>
</div>
</>
</Button>
</div>
);
};
export default SummaryTile;
@@ -0,0 +1,78 @@
.summary-tile {
width: 100%;
display: flex;
justify-content: space-around;
&__tile {
display: flex;
align-items: center;
text-align: left;
padding: $pad-medium;
height: fit-content;
&:focus-visible {
outline: 1px solid $core-vibrant-blue;
}
}
&__icon-wrapper {
margin-right: px-to-rem(12);
white-space: nowrap;
img {
vertical-align: middle;
float: right;
}
// Scale without dead space around icons
#mac-os-hosts-icon,
#windows-hosts-icon,
#linux-hosts-icon {
width: 48px;
}
#missing-hosts-icon {
width: 28px;
}
#low-disk-space-hosts-icon {
width: 32px;
}
}
&__count {
font-size: $large;
white-space: nowrap;
&--online-hosts:before {
border-radius: 100%;
content: " ";
display: inline-block;
margin-right: $pad-small;
height: 8px;
width: 8px;
margin-bottom: 5px;
background-color: $ui-success;
}
&--offline-hosts:before {
border-radius: 100%;
content: " ";
display: inline-block;
margin-right: $pad-small;
height: 8px;
width: 8px;
margin-bottom: 5px;
background-color: $ui-offline;
}
}
&__description {
white-space: nowrap;
.component__tooltip-wrapper__tip-text {
width: 200px;
white-space: initial;
}
}
}
@@ -0,0 +1 @@
export { default } from "./SummaryTile";
@@ -9,6 +9,9 @@
width: auto;
vertical-align: middle;
}
&:focus-visible {
outline: 1px solid $core-vibrant-blue;
}
}
.data-table__wrapper {
overflow-x: auto;
@@ -63,6 +63,10 @@
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none !important;
&:focus-visible {
outline: 1px solid $core-vibrant-blue;
}
}
&__action-button-text {
text-align: right;
+2
View File
@@ -45,6 +45,8 @@ export default {
MANAGE_HOSTS_LABEL: (labelId: number | string): string => {
return `${URL_PREFIX}/hosts/manage/labels/${labelId}`;
},
MANAGE_HOSTS_ONLINE: `${URL_PREFIX}/hosts/manage/online`,
MANAGE_HOSTS_OFFLINE: `${URL_PREFIX}/hosts/manage/offline`,
HOST_DETAILS: (host: IHost): string => {
return `${URL_PREFIX}/hosts/${host.id}`;
},