Fleet UI: Create consistent padding and headers styling for HDP (#27302)

This commit is contained in:
RachelElysia
2025-03-20 09:20:30 -04:00
committed by GitHub
parent 59d9cafbdd
commit 3a04df2746
26 changed files with 184 additions and 132 deletions
@@ -0,0 +1,18 @@
import { Meta, StoryObj } from "@storybook/react";
import CardHeader from ".";
const meta: Meta<typeof CardHeader> = {
component: CardHeader,
title: "Components/CardHeader",
args: {
header: "Card header",
subheader: "This is a card subtitle",
},
};
export default meta;
type Story = StoryObj<typeof CardHeader>;
export const Default: Story = {};
@@ -0,0 +1,33 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import CardHeader from "./CardHeader";
describe("CardHeader", () => {
it("renders header text and subheader text when provided", () => {
const headerText = "Test Header";
const subheaderText = "Test Subheader";
render(<CardHeader header={headerText} subheader={subheaderText} />);
const header = screen.getByText(headerText);
expect(header).toBeInTheDocument();
expect(header.tagName).toBe("H2");
const subheader = screen.getByText(subheaderText);
expect(subheader).toBeInTheDocument();
expect(subheader.tagName).toBe("P");
});
it("does not render subheader when not provided", () => {
const headerText = "Test Header";
render(<CardHeader header={headerText} />);
const subheader = screen.queryByText(/subheader/i);
expect(subheader).not.toBeInTheDocument();
});
it("renders JSX elements for header and subheader", () => {
const headerJSX = <span data-testid="header-jsx">Header JSX</span>;
const subheaderJSX = <span data-testid="subheader-jsx">Subheader JSX</span>;
render(<CardHeader header={headerJSX} subheader={subheaderJSX} />);
expect(screen.getByTestId("header-jsx")).toBeInTheDocument();
expect(screen.getByTestId("subheader-jsx")).toBeInTheDocument();
});
});
@@ -0,0 +1,28 @@
// To be used within <Card/>
import React from "react";
import classnames from "classnames";
const baseClass = "card-header";
interface ICardHeaderProps {
header: JSX.Element | string;
subheader?: JSX.Element | string;
className?: string;
}
/**
* A generic CardHeader component that will be used to render content within a CardHeader with a border and
* and selected background color.
*/
const CardHeader = ({ header, subheader, className }: ICardHeaderProps) => {
const classNames = classnames(baseClass, className);
return (
<div className={classNames}>
<h2 className={`${baseClass}__header`}>{header}</h2>
{subheader && <p className={`${baseClass}__subheader`}>{subheader}</p>}
</div>
);
};
export default CardHeader;
@@ -0,0 +1,19 @@
.card-header {
margin: 0 0 $pad-large;
display: flex;
flex-direction: column;
gap: $pad-small;
&__header {
font-size: $medium;
font-weight: $bold;
margin: 0;
}
&__subheader {
color: $ui-fleet-black-75;
font-size: $x-small;
font-weight: $regular;
margin: 0;
}
}
+1
View File
@@ -0,0 +1 @@
export { default } from "./CardHeader";
@@ -15,12 +15,6 @@
flex-direction: column;
}
.card {
&__header {
font-size: $medium;
font-weight: $bold;
margin: 0 0 $pad-medium 0;
}
.list {
list-style: none;
padding: 0;
@@ -18,6 +18,7 @@ import {
BATTERY_TOOLTIP,
} from "utilities/constants";
import DataSet from "components/DataSet";
import CardHeader from "components/CardHeader";
const getDeviceUserTipContent = (deviceMapping: IDeviceUser[]) => {
if (deviceMapping.length === 0) {
@@ -225,12 +226,12 @@ const About = ({
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
paddingSize="xxlarge"
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<h2>About</h2>
<CardHeader header="About" />
<div className="info-flex">
<DataSet
title="Added to Fleet"
@@ -1,13 +1,8 @@
.about-card {
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
}
.truncated-tooltip {
.about-card__device-mapping__source {
color: inherit;
}
color: inherit;
}
}
&__used-by {
@@ -8,6 +8,7 @@ import {
} from "services/entities/activities";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import TabNav from "components/TabNav";
import TabText from "components/TabText";
import Spinner from "components/Spinner";
@@ -58,8 +59,8 @@ const Activity = ({
return (
<Card
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
largePadding
className={baseClass}
>
{isLoading && (
@@ -67,7 +68,7 @@ const Activity = ({
<Spinner />
</div>
)}
<h2>Activity</h2>
<CardHeader header="Activity" />
<TabNav>
<Tabs
selectedIndex={activeTab === "past" ? 0 : 1}
@@ -1,11 +1,6 @@
.activity-card {
position: relative;
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
}
&__loading-overlay {
height: 100%;
width: 100%;
@@ -6,6 +6,7 @@ import { secondsToHms } from "utilities/helpers";
import DataSet from "components/DataSet";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
const baseClass = "agent-options-card";
interface IAgentOptionsProps {
@@ -54,20 +55,21 @@ const AgentOptions = ({
return (
<Card
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
largePadding
className={classNames}
>
{isChromeOS ? (
<TooltipWrapper
tipContent={CHROMEOS_AGENT_OPTIONS_TOOLTIP_MESSAGE}
className="card__header"
>
Agent options
</TooltipWrapper>
) : (
<p className="card__header">Agent options</p>
)}
<CardHeader
header={
isChromeOS ? (
<TooltipWrapper tipContent={CHROMEOS_AGENT_OPTIONS_TOOLTIP_MESSAGE}>
Agent options
</TooltipWrapper>
) : (
"Agent options"
)
}
/>
<div className={`${baseClass}__data`}>
<DataSet title="Config TLS refresh" value={configTLSRefresh} />
<DataSet title="Logger TLS period" value={loggerTLSPeriod} />
@@ -7,6 +7,7 @@ import { IListSort } from "interfaces/list_options";
import { HostPlatform } from "interfaces/platform";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import DataError from "components/DataError";
import CertificatesTable from "./CertificatesTable";
@@ -65,10 +66,10 @@ const CertificatesCard = ({
<Card
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
paddingSize="xxlarge"
>
<h2>Certificates</h2>
<CardHeader header="Certificates" />
{renderContent()}
</Card>
);
@@ -1,7 +0,0 @@
.certificates-card {
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
}
}
@@ -501,8 +501,8 @@ const HostSummary = ({
return (
<Card
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
largePadding
className={`${baseClass}-card`}
>
{!isIosOrIpadosHost && !isAndroidHost && (
@@ -5,6 +5,7 @@ import { ILabel } from "interfaces/label";
import classnames from "classnames";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import { LABEL_DISPLAY_MAP } from "utilities/constants";
const baseClass = "labels-card";
@@ -36,11 +37,11 @@ const Labels = ({ onLabelClick, labels }: ILabelsProps): JSX.Element => {
return (
<Card
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
paddingSize="xxlarge"
className={classNames}
>
<h2>Labels</h2>
<CardHeader header="Labels" />
{labels.length === 0 ? (
<p className="info-flex__item">
No labels are associated with this host.
@@ -1,7 +0,0 @@
.labels-card {
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
}
}
@@ -5,6 +5,7 @@ import { IMunkiIssue } from "interfaces/host";
import TableContainer from "components/TableContainer";
import EmptyTable from "components/EmptyTable";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import { munkiIssuesTableHeaders } from "./MunkiIssuesTableConfig";
@@ -26,13 +27,12 @@ const MunkiIssuesTable = ({
return (
<Card
className={`${baseClass} card`}
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
largePadding
>
<p className="card__header">Munki issues</p>
<CardHeader header="Munki issues" />
{munkiIssues?.length ? (
<div className={deviceType || ""}>
<TableContainer
@@ -3,6 +3,7 @@ import React from "react";
import { IPackStats } from "interfaces/host";
import TableContainer from "components/TableContainer";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import {
Accordion,
@@ -72,12 +73,12 @@ const Packs = ({ packsState, isLoading }: IPacksProps): JSX.Element => {
<></>
) : (
<Card
borderRadiusSize="xxlarge"
includeShadow
largePadding
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<p className="card__header">Packs</p>
<CardHeader header="Packs" />
<Accordion allowMultipleExpanded allowZeroExpanded>
{packsAccordion}
</Accordion>
@@ -11,6 +11,7 @@ import { getPathWithQueryParams } from "utilities/url";
import TableContainer from "components/TableContainer";
import EmptyTable from "components/EmptyTable";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import CustomLink from "components/CustomLink";
import {
@@ -152,12 +153,12 @@ const Policies = ({
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
largePadding
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<p className="card__header">Policies</p>
<CardHeader header="Policies" />
{renderHostPolicies()}
</Card>
);
@@ -7,6 +7,7 @@ import TableContainer from "components/TableContainer";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import PATHS from "router/paths";
import { InjectedRouter } from "react-router";
import { Row } from "react-table";
@@ -156,12 +157,12 @@ const HostQueries = ({
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
largePadding
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<p className="card__header">Queries</p>
<CardHeader header="Queries" />
{renderHostQueries()}
</Card>
);
@@ -22,6 +22,7 @@ import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import Card from "components/Card/Card";
import CardHeader from "components/CardHeader";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import SoftwareFiltersModal from "pages/SoftwarePage/components/SoftwareFiltersModal";
@@ -415,17 +416,17 @@ const HostSoftware = ({
return (
<Card
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xxlarge"
paddingSize="xlarge"
includeShadow
className={`${baseClass} ${isMyDevicePage ? "device-software" : ""}`}
>
<div className={`card-header`}>Software</div>
{isMyDevicePage && (
<div className={`card-subheader`}>
Software installed on your device.
</div>
)}
<CardHeader
header="Software"
subheader={
isMyDevicePage ? "Software installed on your device." : undefined
}
/>
{renderHostSoftware()}
</Card>
);
@@ -12,6 +12,7 @@ import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import { pluralize } from "utilities/strings/stringUtils";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import CustomLink from "components/CustomLink";
import DataError from "components/DataError";
import EmptyTable from "components/EmptyTable";
@@ -88,21 +89,25 @@ const SoftwareSelfService = ({
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
paddingSize="xxlarge"
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<div className={`${baseClass}__card-header`}>Self-service</div>
<div className={`${baseClass}__card-subheader`}>
Install organization-approved apps provided by your IT department.{" "}
{contactUrl && (
<span>
If you need help,{" "}
<CustomLink url={contactUrl} text="reach out to IT" newTab />
</span>
)}
</div>
<CardHeader
header="Self-service"
subheader={
<>
Install organization-approved apps provided by your IT department.{" "}
{contactUrl && (
<span>
If you need help,{" "}
<CustomLink url={contactUrl} text="reach out to IT" newTab />
</span>
)}
</>
}
/>
{isLoading ? (
<Spinner />
) : (
@@ -1,14 +1,4 @@
.software-self-service {
&__card-header {
margin: 0 0 8px 0;
}
&__card-subheader {
margin: 0 0 24px 0;
color: $ui-fleet-black-75;
font-size: $x-small;
}
// TODO: empty table styling differs slightly from figma (font size, color, spacing), why?g
.empty-table__container {
margin: 64px 0;
@@ -1,9 +1,4 @@
.software-card {
.card-header {
font-weight: $bold;
margin: 0 0 $pad-large 0;
}
.table-container {
.stackable-header {
min-width: initial; // Fix card overflow on low widths
@@ -111,16 +106,3 @@
}
}
}
.device-software {
.card-header {
font-weight: $regular;
margin: 0 0 $pad-small 0;
}
.card-subheader {
margin: 0 0 $pad-large 0;
color: $ui-fleet-black-75;
font-size: $x-small;
}
}
@@ -7,6 +7,7 @@ import TableCount from "components/TableContainer/TableCount";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
import generateUsersTableHeaders from "./UsersTable/UsersTableConfig";
@@ -36,12 +37,12 @@ const Users = ({
if (!hostUsersEnabled) {
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
largePadding
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<p className="card__header">Users</p>
<CardHeader header="Users" />
<EmptyTable
header="User collection has been disabled"
info={
@@ -61,13 +62,13 @@ const Users = ({
return (
<Card
borderRadiusSize="xxlarge"
includeShadow
paddingSize="xxlarge"
className={baseClass}
borderRadiusSize="xxlarge"
paddingSize="xlarge"
includeShadow
>
<>
<h2 className="card__header">Users</h2>
<CardHeader header="Users" />
{users?.length ? (
<TableContainer
columnConfigs={tableHeaders}
@@ -1,9 +1,4 @@
.users-card {
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
}
.data-table-block {
.data-table__table {
thead {