Fleet UI: Create consistent padding and headers styling for HDP (#27302)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user