From 73a368a2005b71af826f8be705a73a23f0a73f0a Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:48:08 -0500 Subject: [PATCH] Fleet UI: Clean up TabNav and TargetChipSelector components (#26256) --- .../PlatformWrapper/PlatformWrapper.tsx | 9 +- .../PlatformWrapper/_styles.scss | 2 +- .../components/LiveQuery/SelectTargets.tsx | 47 +----- .../TargetChipSelector.stories.tsx | 110 ++++++++++++++ .../TargetChipSelector.tests.tsx | 102 +++++++++++++ .../TargetChipSelector/TargetChipSelector.tsx | 55 +++++++ .../{ => TargetChipSelector}/_styles.scss | 4 +- .../LiveQuery/TargetChipSelector/index.ts | 1 + frontend/components/TabNav/TabNav.stories.tsx | 93 ++++++++++++ frontend/components/TabNav/TabNav.tests.tsx | 68 +++++++++ .../TabsWrapper.tsx => TabNav/TabNav.tsx} | 11 +- frontend/components/TabNav/_styles.scss | 134 ++++++++++++++++++ frontend/components/TabNav/index.ts | 1 + frontend/components/TabText/TabText.tsx | 47 ++++++ frontend/components/TabText/_styles.scss | 23 +++ frontend/components/TabText/index.ts | 1 + frontend/components/TabsWrapper/_styles.scss | 90 ------------ frontend/components/TabsWrapper/index.ts | 1 - frontend/components/icons/Check.tsx | 1 + frontend/components/icons/Plus.tsx | 1 + .../pages/DashboardPage/cards/MDM/MDM.tsx | 15 +- .../DashboardPage/cards/MDM/_styles.scss | 2 +- .../pages/DashboardPage/cards/Munki/Munki.tsx | 15 +- .../DashboardPage/cards/Munki/_styles.scss | 2 +- .../DashboardPage/cards/Software/Software.tsx | 15 +- .../DashboardPage/cards/Software/_styles.scss | 2 +- .../ManageControlsPage/ManageControlsPage.tsx | 9 +- .../components/PlatformTabs/PlatformTabs.tsx | 17 ++- .../SoftwareAddPage/SoftwareAddPage.tsx | 9 +- frontend/pages/SoftwarePage/SoftwarePage.tsx | 9 +- frontend/pages/SoftwarePage/_styles.scss | 2 +- .../components/AddSoftwareModal/_styles.scss | 3 +- frontend/pages/admin/AdminWrapper.tsx | 9 +- .../TeamDetailsWrapper/TeamDetailsWrapper.tsx | 128 ++++++++--------- frontend/pages/admin/_styles.scss | 2 +- .../details/DeviceUserPage/DeviceUserPage.tsx | 28 ++-- .../HostDetailsPage/HostDetailsPage.tsx | 26 ++-- .../details/HostDetailsPage/_styles.scss | 7 - frontend/pages/hosts/details/_styles.scss | 13 +- .../hosts/details/cards/Activity/Activity.tsx | 18 ++- .../hosts/details/cards/Activity/_styles.scss | 15 -- .../SoftwareDetailsModal.tsx | 15 +- .../labels/NewLabelPage/NewLabelPage.tsx | 9 +- .../pages/labels/NewLabelPage/_styles.scss | 2 +- .../PolicyResults/PolicyResults.tsx | 18 +-- .../components/QueryResults/QueryResults.tsx | 16 +-- 46 files changed, 849 insertions(+), 358 deletions(-) create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx rename frontend/components/LiveQuery/{ => TargetChipSelector}/_styles.scss (92%) create mode 100644 frontend/components/LiveQuery/TargetChipSelector/index.ts create mode 100644 frontend/components/TabNav/TabNav.stories.tsx create mode 100644 frontend/components/TabNav/TabNav.tests.tsx rename frontend/components/{TabsWrapper/TabsWrapper.tsx => TabNav/TabNav.tsx} (65%) create mode 100644 frontend/components/TabNav/_styles.scss create mode 100644 frontend/components/TabNav/index.ts create mode 100644 frontend/components/TabText/TabText.tsx create mode 100644 frontend/components/TabText/_styles.scss create mode 100644 frontend/components/TabText/index.ts delete mode 100644 frontend/components/TabsWrapper/_styles.scss delete mode 100644 frontend/components/TabsWrapper/index.ts diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index dbacef6bc8..e53ce0e068 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -13,10 +13,11 @@ import RevealButton from "components/buttons/RevealButton"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import TooltipWrapper from "components/TooltipWrapper"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; import InfoBanner from "components/InfoBanner/InfoBanner"; import CustomLink from "components/CustomLink/CustomLink"; import Radio from "components/forms/fields/Radio"; +import TabText from "components/TabText"; import { isValidPemCertificate } from "../../../pages/hosts/ManageHostsPage/helpers"; import IosIpadosPanel from "./IosIpadosPanel"; @@ -573,7 +574,7 @@ const PlatformWrapper = ({ return (
- + setSelectedTabIndex(index)} selectedIndex={selectedTabIndex} @@ -584,7 +585,7 @@ const PlatformWrapper = ({ // so we add a hidden pseudo element with the same text string return ( - {navItem.name} + {navItem.name} ); })} @@ -601,7 +602,7 @@ const PlatformWrapper = ({ ); })} - +
- ); -}; - const SelectTargets = ({ baseClass, queryId, diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx new file mode 100644 index 0000000000..f5ca496d71 --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { ISelectLabel, ISelectTeam } from "interfaces/target"; +import TargetChipSelector from "./TargetChipSelector"; // Adjust the path if necessary + +const meta: Meta = { + component: TargetChipSelector, + title: "Components/TargetChipSelector", + argTypes: { + entity: { + description: "The label or team entity to display.", + control: { type: "object" }, + }, + isSelected: { + description: + "Whether the chip is currently selected, updated by parent onClick handler.", + control: { type: "boolean" }, + }, + onClick: { + description: "The handler to call when the chip is clicked.", + action: "clicked", // Use Storybook's action to track clicks + }, + }, + parameters: { + backgrounds: { + default: "light", + values: [ + { name: "light", value: "#ffffff" }, + { name: "dark", value: "#333333" }, + ], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +// Example data for labels and teams +const mockLabel: ISelectLabel = { + id: 1, + name: "Example Label", + label_type: "regular", + description: "A test label", +}; + +const mockTeam: ISelectTeam = { + id: 2, + name: "Example Team", + description: "A test team", +}; + +export const LabelExample: Story = { + args: { + entity: mockLabel, + isSelected: false, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked label:", value); + }, + }, + render: (args) => ( + + ), +}; + +export const TeamExample: Story = { + args: { + entity: mockTeam, + isSelected: true, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked team:", value); + }, + }, + render: (args) => ( + + ), +}; + +export const BuiltInLabelExample: Story = { + args: { + entity: { + id: 3, + name: "MS Windows", + label_type: "builtin", + description: "Microsoft Windows hosts", + }, + isSelected: false, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked label:", value); + }, + }, + render: (args) => ( + + ), +}; diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx new file mode 100644 index 0000000000..6259fa6490 --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; + +import { ISelectLabel, ISelectTeam } from "interfaces/target"; +import TargetChipSelector from "./TargetChipSelector"; + +describe("TargetChipSelector", () => { + const mockOnClick = jest.fn(); + + const mockLabel: ISelectLabel = { + id: 1, + name: "Example Label", + label_type: "regular", + description: "A test label", + }; + + const mockTeam: ISelectTeam = { + id: 2, + name: "Example Team", + description: "A test team", + }; + + it("renders the correct display text for a label", () => { + render( + + ); + + expect(screen.getByText("Example Label")).toBeInTheDocument(); + }); + + it("renders the correct display text for a team", () => { + render( + + ); + + expect(screen.getByText("Example Team")).toBeInTheDocument(); + }); + + it("renders the correct icon when selected", () => { + render( + + ); + + expect(screen.getByLabelText("check")).toBeInTheDocument(); + }); + + it("renders the correct icon when not selected", () => { + render( + + ); + + expect(screen.getByLabelText("plus")).toBeInTheDocument(); + }); + + it("calls the onClick handler with the correct entity when clicked", () => { + render( + (event) => mockOnClick(value, event)} + /> + ); + + fireEvent.click(screen.getByRole("button")); + + expect(mockOnClick).toHaveBeenCalledWith(mockLabel, expect.any(Object)); + }); + + it("applies the correct data-selected attribute when selected", () => { + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("data-selected", "true"); + }); + + it("applies the correct data-selected attribute when not selected", () => { + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("data-selected", "false"); + }); +}); diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx new file mode 100644 index 0000000000..98ad2ccc1e --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { + ISelectLabel, + ISelectTeam, + ISelectTargetsEntity, +} from "interfaces/target"; +import Icon from "components/Icon"; +import { + PlatformLabelNameFromAPI, + LABEL_DISPLAY_MAP, +} from "utilities/constants"; + +interface ITargetChipSelectorProps { + entity: ISelectLabel | ISelectTeam; + isSelected: boolean; + onClick: ( + value: ISelectLabel | ISelectTeam + ) => React.MouseEventHandler; +} + +const isBuiltInLabel = ( + entity: ISelectTargetsEntity +): entity is ISelectLabel & { label_type: "builtin" } => { + return "label_type" in entity && entity.label_type === "builtin"; +}; + +const TargetChipSelector = ({ + entity, + isSelected, + onClick, +}: ITargetChipSelectorProps): JSX.Element => { + const displayText = (): string => { + if (isBuiltInLabel(entity)) { + const labelName = entity.name as PlatformLabelNameFromAPI; + if (labelName in LABEL_DISPLAY_MAP) { + return LABEL_DISPLAY_MAP[labelName] || labelName; + } + } + + return entity.name || "Missing display name"; + }; + + return ( + + ); +}; + +export default TargetChipSelector; diff --git a/frontend/components/LiveQuery/_styles.scss b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss similarity index 92% rename from frontend/components/LiveQuery/_styles.scss rename to frontend/components/LiveQuery/TargetChipSelector/_styles.scss index e6e9ce0554..ca844e1354 100644 --- a/frontend/components/LiveQuery/_styles.scss +++ b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss @@ -1,4 +1,4 @@ -.target-pill-selector { +.target-chip-selector { padding: $pad-small; background-color: $core-white; border: none; @@ -34,7 +34,7 @@ } &:hover { - box-shadow: inset 0 0 0 1px $core-vibrant-blue-over; + background-color: $ui-vibrant-blue-10; } &:active { diff --git a/frontend/components/LiveQuery/TargetChipSelector/index.ts b/frontend/components/LiveQuery/TargetChipSelector/index.ts new file mode 100644 index 0000000000..94cc8dca7f --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/index.ts @@ -0,0 +1 @@ +export { default } from "./TargetChipSelector"; diff --git a/frontend/components/TabNav/TabNav.stories.tsx b/frontend/components/TabNav/TabNav.stories.tsx new file mode 100644 index 0000000000..c86fc9ed64 --- /dev/null +++ b/frontend/components/TabNav/TabNav.stories.tsx @@ -0,0 +1,93 @@ +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import TabText from "components/TabText"; +import TabNav from "./TabNav"; + +const meta: Meta = { + component: TabNav, + title: "Components/TabNav", + parameters: { + backgrounds: { + default: "light", + values: [ + { + name: "light", + value: "#ffffff", + }, + { + name: "dark", + value: "#333333", + }, + ], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const platformSubNav = [ + { name: Basic tab, type: "type1" }, + { name: Basic tab 2, type: "type2" }, + { + name: Disabled tab, + type: "type3", + disabled: true, + }, + { name: Tab with count, type: "type4" }, + { + name: ( + + Tab with error count + + ), + type: "type5", + }, + ]; + + const renderPanel = (type: string) => { + switch (type) { + case "type1": + return
Content for Tab 1
; + case "type2": + return
Content for Tab 2
; + case "type3": + return
Content for Tab 3
; + case "type4": + return
Content for Tab 4
; + case "type5": + return
Content for Tab 5
; + default: + return null; + } + }; + + return ( + + setSelectedTabIndex(index)} + selectedIndex={selectedTabIndex} + > + + {platformSubNav.map((navItem) => ( + + {navItem.name} + + ))} + + {platformSubNav.map((navItem) => ( + +
{renderPanel(navItem.type)}
+
+ ))} +
+
+ ); + }, +}; diff --git a/frontend/components/TabNav/TabNav.tests.tsx b/frontend/components/TabNav/TabNav.tests.tsx new file mode 100644 index 0000000000..efff13cd9d --- /dev/null +++ b/frontend/components/TabNav/TabNav.tests.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import TabText from "components/TabText"; +import TabNav from "./TabNav"; + +describe("TabNav", () => { + it("renders tabs and panels correctly", () => { + render( + + + + + Tab 1 + + + Tab 2 + + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+
+
+ ); + + // Check if tabs are rendered + expect(screen.getByText("Tab 1")).toBeInTheDocument(); + expect(screen.getByText("Tab 2")).toBeInTheDocument(); + + // Check if the first panel content is rendered by default + expect(screen.getByText("Content for Tab 1")).toBeInTheDocument(); + expect(screen.queryByText("Content for Tab 2")).not.toBeInTheDocument(); + }); + + it("switches tabs and displays the correct panel content", () => { + render( + + + + + Tab 1 + + + Tab 2 + + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+
+
+ ); + + // Switch to the second tab + fireEvent.click(screen.getByText("Tab 2")); + + // Check if the second panel content is displayed + expect(screen.getByText("Content for Tab 2")).toBeInTheDocument(); + expect(screen.queryByText("Content for Tab 1")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/components/TabsWrapper/TabsWrapper.tsx b/frontend/components/TabNav/TabNav.tsx similarity index 65% rename from frontend/components/TabsWrapper/TabsWrapper.tsx rename to frontend/components/TabNav/TabNav.tsx index d788fb4d4e..6b9f59b049 100644 --- a/frontend/components/TabsWrapper/TabsWrapper.tsx +++ b/frontend/components/TabNav/TabNav.tsx @@ -1,7 +1,7 @@ import React from "react"; import classnames from "classnames"; -interface ITabsWrapperProps { +interface ITabNavProps { children: React.ReactChild | React.ReactChild[]; className?: string; } @@ -10,15 +10,12 @@ interface ITabsWrapperProps { * This component exists so we can unify the styles * and overwrite the loaded React Tabs styles. */ -const baseClass = "component__tabs-wrapper"; +const baseClass = "tab-nav"; -const TabsWrapper = ({ - children, - className, -}: ITabsWrapperProps): JSX.Element => { +const TabNav = ({ children, className }: ITabNavProps): JSX.Element => { const classNames = classnames(baseClass, className); return
{children}
; }; -export default TabsWrapper; +export default TabNav; diff --git a/frontend/components/TabNav/_styles.scss b/frontend/components/TabNav/_styles.scss new file mode 100644 index 0000000000..cb5d2a79b9 --- /dev/null +++ b/frontend/components/TabNav/_styles.scss @@ -0,0 +1,134 @@ +.tab-nav { + position: sticky; + top: 0; + background-color: $core-white; + z-index: 2; + + .react-tabs { + &__tab-list { + display: inline-flex; + align-items: flex-start; + gap: $pad-xxlarge; + border-bottom: 1px solid $ui-fleet-black-10; + width: 100%; + height: 43px; + } + .tab-text { + display: flex; /* Ensure text and count are aligned horizontally */ + align-items: center; /* Vertically align items */ + + .tab-text__text { + display: relative; + + // Reserve space for bold text using a hidden pseudo-element + &::before { + content: attr(data-text); /* Same text as the visible one */ + font-weight: bold; /* Mimic bold styling */ + visibility: hidden; /* Keep it invisible */ + position: absolute; /* Prevent it from affecting layout */ + } + } + } + + &__tab { + padding: 5px 0 $pad-medium; + font-size: $x-small; + border: none; + display: inline-flex; + flex-direction: column; + align-items: center; + line-height: 21px; + + &:focus { + box-shadow: none; + outline: 0; + &:after { + left: 0; + bottom: 0; + } + } + + // focus-visible only highlights when tabbing not clicking + &:focus-visible { + .tab-text { + border-radius: $border-radius; + // Outline used instead of border not to shift component + outline: 1px solid $ui-vibrant-blue-25; + outline-offset: -1px; + } + } + + // // Bolding text when the button is active causes a layout shift + // // so we add a hidden pseudo element with the same text string + &:before { + content: attr(data-text); + height: 0; + visibility: hidden; + overflow: hidden; + user-select: none; + pointer-events: none; + font-weight: $bold; + } + + &--selected { + font-weight: $bold; + + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 2px solid $core-vibrant-blue; + position: absolute; + bottom: 0; + left: 0; + } + } + + &:hover { + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 2px solid $core-vibrant-blue; + position: absolute; + bottom: 0; + left: 0; + } + } + + &--disabled { + cursor: not-allowed; + + &:hover { + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 0; + position: absolute; + bottom: 0; + left: 0; + } + } + } + + &.no-count:not(.errors-empty).react-tabs__tab--selected::after { + bottom: -2px; + } + } + &__tab-panel { + .no-results-message { + margin-top: $pad-xxlarge; + font-size: $small; + font-weight: $bold; + + span { + margin-top: $pad-medium; + font-size: $x-small; + font-weight: $regular; + display: block; + } + } + } + } +} diff --git a/frontend/components/TabNav/index.ts b/frontend/components/TabNav/index.ts new file mode 100644 index 0000000000..bbb0fa5b23 --- /dev/null +++ b/frontend/components/TabNav/index.ts @@ -0,0 +1 @@ +export { default } from "./TabNav"; diff --git a/frontend/components/TabText/TabText.tsx b/frontend/components/TabText/TabText.tsx new file mode 100644 index 0000000000..10ac42b2d5 --- /dev/null +++ b/frontend/components/TabText/TabText.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import classnames from "classnames"; + +interface ITabTextProps { + className?: string; + children: React.ReactNode; + count?: number; + /** Changes count badge from default purple to red */ + isErrorCount?: boolean; +} + +/* + * This component exists so we can unify the styles + * and add styles to react-tab text. + */ +const baseClass = "tab-text"; + +const TabText = ({ + className, + children, + count, + isErrorCount = false, +}: ITabTextProps): JSX.Element => { + const classNames = classnames(baseClass, className); + + const countClassNames = classnames(`${baseClass}__count`, { + [`${baseClass}__count--error`]: isErrorCount, + }); + + const renderCount = () => { + if (count && count > 0) { + return
{count.toLocaleString()}
; + } + return undefined; + }; + + return ( +
+
+ {children} +
+ {renderCount()} +
+ ); +}; + +export default TabText; diff --git a/frontend/components/TabText/_styles.scss b/frontend/components/TabText/_styles.scss new file mode 100644 index 0000000000..d28705f3ce --- /dev/null +++ b/frontend/components/TabText/_styles.scss @@ -0,0 +1,23 @@ +.tab-text { + display: flex; + flex-direction: row; + gap: $pad-small; + align-items: center; + height: 21px; + + &__count { + display: flex; + padding: 1px 12px; + justify-content: center; + align-items: center; + background-color: $core-vibrant-blue; + border-radius: 29px; + color: $core-white; + font-weight: $bold; + font-size: $xx-small; + + &--error { + background-color: $core-vibrant-red; + } + } +} diff --git a/frontend/components/TabText/index.ts b/frontend/components/TabText/index.ts new file mode 100644 index 0000000000..2f260c4d2c --- /dev/null +++ b/frontend/components/TabText/index.ts @@ -0,0 +1 @@ +export { default } from "./TabText"; diff --git a/frontend/components/TabsWrapper/_styles.scss b/frontend/components/TabsWrapper/_styles.scss deleted file mode 100644 index d4e8d08caa..0000000000 --- a/frontend/components/TabsWrapper/_styles.scss +++ /dev/null @@ -1,90 +0,0 @@ -.component__tabs-wrapper { - position: sticky; - top: 0; - background-color: $core-white; - z-index: 2; - - .react-tabs { - &__tab-list { - border-bottom: 1px solid $ui-gray; - } - &__tab { - padding: $pad-small 0; - margin-right: $pad-xxlarge; - font-size: $x-small; - border: none; - display: inline-flex; - flex-direction: column; - align-items: center; - line-height: 19px; // Fix shifty bold text - - &:focus { - box-shadow: none; - outline: 0; - &:after { - left: 0; - bottom: 0; - } - } - - // focus-visible only highlights when tabbing not clicking - &:focus-visible { - background-color: $ui-vibrant-blue-10; - } - - // Bolding text when the button is active causes a layout shift - // so we add a hidden pseudo element with the same text string - &:before { - content: attr(data-text); - height: 0; - visibility: hidden; - overflow: hidden; - user-select: none; - pointer-events: none; - font-weight: $bold; - } - &--selected { - font-weight: $bold; - - &::after { - content: ""; - width: 100%; - height: 0; - border-bottom: 2px solid #6a67fe; - position: absolute; - bottom: 0; - left: 0; - } - } - &--disabled { - cursor: not-allowed; - } - &.no-count:not(.errors-empty).react-tabs__tab--selected::after { - bottom: -2px; - } - .count { - margin-right: $pad-small; - padding: $pad-xxsmall 12px; - background-color: $core-vibrant-red; - display: inline-block; - border-radius: 29px; - color: $core-white; - font-weight: $bold; - } - } - &__tab-panel { - .no-results-message { - margin-top: $pad-xxlarge; - font-size: $small; - font-weight: $bold; - - span { - margin-top: $pad-medium; - font-size: $x-small; - font-weight: $regular; - display: block; - } - } - } - } -} diff --git a/frontend/components/TabsWrapper/index.ts b/frontend/components/TabsWrapper/index.ts deleted file mode 100644 index 980c53aaa0..0000000000 --- a/frontend/components/TabsWrapper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TabsWrapper"; diff --git a/frontend/components/icons/Check.tsx b/frontend/components/icons/Check.tsx index f84a2ffd1f..106f6e00c3 100644 --- a/frontend/components/icons/Check.tsx +++ b/frontend/components/icons/Check.tsx @@ -13,6 +13,7 @@ const Check = ({ color = "core-fleet-blue" }: ICheckProps) => { height="16" fill="none" viewBox="0 0 16 16" + aria-label="check" > { fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" + aria-label="plus" > )}
- + - Solutions - Status + + Solutions + + + Status + {error ? ( @@ -189,7 +194,7 @@ const Mdm = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/MDM/_styles.scss b/frontend/pages/DashboardPage/cards/MDM/_styles.scss index 29ba4c319c..e78a190404 100644 --- a/frontend/pages/DashboardPage/cards/MDM/_styles.scss +++ b/frontend/pages/DashboardPage/cards/MDM/_styles.scss @@ -3,7 +3,7 @@ position: relative; height: 100%; // centers loading spinner - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } diff --git a/frontend/pages/DashboardPage/cards/Munki/Munki.tsx b/frontend/pages/DashboardPage/cards/Munki/Munki.tsx index 8b12f2d2a1..7ac7cf6a8a 100644 --- a/frontend/pages/DashboardPage/cards/Munki/Munki.tsx +++ b/frontend/pages/DashboardPage/cards/Munki/Munki.tsx @@ -6,7 +6,8 @@ import { IMunkiVersionsAggregate, } from "interfaces/macadmins"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import TableContainer from "components/TableContainer"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; @@ -58,11 +59,15 @@ const Munki = ({
)}
- + - Issues - Versions + + Issues + + + Versions + {errorMacAdmins ? ( @@ -128,7 +133,7 @@ const Munki = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/Munki/_styles.scss b/frontend/pages/DashboardPage/cards/Munki/_styles.scss index a447f7aa36..39ba88050b 100644 --- a/frontend/pages/DashboardPage/cards/Munki/_styles.scss +++ b/frontend/pages/DashboardPage/cards/Munki/_styles.scss @@ -5,7 +5,7 @@ .data-table__wrapper { overflow-x: auto; } - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } .data-table-block { diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index a8b8d98741..3aafa34a73 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -8,7 +8,8 @@ import { buildQueryStringFromParams } from "utilities/url"; import { ISoftwareResponse } from "interfaces/software"; import { ITableQueryData } from "components/TableContainer/TableContainer"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; @@ -76,11 +77,15 @@ const Software = ({ )}
- + - All - Vulnerable + + All + + + Vulnerable + {!isSoftwareFetching && errorSoftware ? ( @@ -129,7 +134,7 @@ const Software = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/Software/_styles.scss b/frontend/pages/DashboardPage/cards/Software/_styles.scss index 3535b1f320..71ea46b649 100644 --- a/frontend/pages/DashboardPage/cards/Software/_styles.scss +++ b/frontend/pages/DashboardPage/cards/Software/_styles.scss @@ -19,7 +19,7 @@ .form-field--dropdown { margin: 0; } - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } &__empty-software { diff --git a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx index c278cd5a3e..71570f209e 100644 --- a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx +++ b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx @@ -6,7 +6,8 @@ import PATHS from "router/paths"; import { AppContext } from "context/app"; import useTeamIdParam from "hooks/useTeamIdParam"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import MainContent from "components/MainContent"; import TeamsDropdown from "components/TeamsDropdown"; import { parseOSUpdatesCurrentVersionsQueryParams } from "./OSUpdates/components/CurrentVersionSection/CurrentVersionSection"; @@ -112,7 +113,7 @@ const ManageControlsPage = ({ const renderBody = () => { return (
- + { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { teamIdForApi, currentPage: page, diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx index d69a9ec838..bc02c5800f 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import CustomLink from "components/CustomLink"; import { SUPPORT_LINK } from "utilities/constants"; @@ -62,27 +63,25 @@ const PlatformTabs = ({ return (
- + - {/* Bolding text when the tab is active causes a layout shift so - we add a hidden pseudo element with the same text string */} - macOS + macOS {isWindowsMdmEnabled && ( - Windows + Windows )} - iOS + iOS - iPadOS + iPadOS {isAndroidMdmEnabled && ( @@ -149,7 +148,7 @@ const PlatformTabs = ({ )} - +
); }; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx index ef9f244ab2..fa326a9a5d 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx @@ -11,7 +11,8 @@ import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import MainContent from "components/MainContent"; import BackLink from "components/BackLink"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import SidePanelContent from "components/SidePanelContent"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; @@ -112,7 +113,7 @@ const SoftwareAddPage = ({ className={`${baseClass}__back-to-software`} />

Add software

- + { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { router, currentTeamId: parseInt(location.query.team_id, 10), diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx index 21a8e18d55..0beceae5c3 100644 --- a/frontend/pages/SoftwarePage/SoftwarePage.tsx +++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx @@ -31,7 +31,8 @@ import { getNextLocationPath } from "utilities/helpers"; import Button from "components/buttons/Button"; import MainContent from "components/MainContent"; import TeamsHeader from "components/TeamsHeader"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import ManageAutomationsModal from "./components/ManageSoftwareAutomationsModal"; import AddSoftwareModal from "./components/AddSoftwareModal"; @@ -412,7 +413,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { const renderBody = () => { return (
- + { {softwareSubNav.map((navItem) => { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { router, isSoftwareEnabled: Boolean( diff --git a/frontend/pages/SoftwarePage/_styles.scss b/frontend/pages/SoftwarePage/_styles.scss index fdbe0bea5d..5500c122f1 100644 --- a/frontend/pages/SoftwarePage/_styles.scss +++ b/frontend/pages/SoftwarePage/_styles.scss @@ -57,7 +57,7 @@ } &__wrapper { - .component__tabs-wrapper { + .tab-nav { margin-bottom: $pad-xxlarge; } } diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss b/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss index 706272708b..cd58cf7729 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss @@ -1,7 +1,6 @@ .add-software-modal { - // have to use this selector to override the default styles - .component__tabs-wrapper { + .tab-nav { margin-bottom: 0; } } diff --git a/frontend/pages/admin/AdminWrapper.tsx b/frontend/pages/admin/AdminWrapper.tsx index 9efa69ae67..98666ac002 100644 --- a/frontend/pages/admin/AdminWrapper.tsx +++ b/frontend/pages/admin/AdminWrapper.tsx @@ -4,8 +4,9 @@ import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; import { AppContext } from "context/app"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; import MainContent from "components/MainContent"; +import TabText from "components/TabText"; import classnames from "classnames"; interface ISettingSubNavItem { @@ -77,7 +78,7 @@ const AdminWrapper = ({ return (
- +

Settings

-
+ {children}
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 3a35856f60..eaf07701c0 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -24,7 +24,8 @@ import sortUtils from "utilities/sort"; import ActionButtons from "components/buttons/ActionButtons/ActionButtons"; import Spinner from "components/Spinner"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import BackLink from "components/BackLink"; import TeamsDropdown from "components/TeamsDropdown"; import MainContent from "components/MainContent"; @@ -387,69 +388,66 @@ const TeamDetailsWrapper = ({ return ( <> - - {isGlobalAdmin ? ( -
- -
- ) : ( - <> - )} -
-
- {userTeams?.length === 1 ? ( -

{currentTeamDetails.name}

- ) : ( - - )} - {!!hostsTotalDisplay && ( - - {hostsTotalDisplay} - - )} -
- + {isGlobalAdmin ? ( +
+
+ ) : ( + <> + )} +
+
+ {userTeams?.length === 1 ? ( +

{currentTeamDetails.name}

+ ) : ( + + )} + {!!hostsTotalDisplay && ( + + {hostsTotalDisplay} + + )} +
+ +
+ - + {showAddHostsModal && ( - + router.push(tabPaths[i])} > - Details + + Details + {isPremiumTier && isSoftwareEnabled && hasSelfService && ( - Self-service + + Self-service + + )} + {isSoftwareEnabled && ( + + Software + )} - {isSoftwareEnabled && Software} {isPremiumTier && ( -
- {failingPoliciesCount > 0 && ( - {failingPoliciesCount} - )} + Policies -
+
)}
@@ -461,7 +467,7 @@ const DeviceUserPage = ({ )}
-
+ {showInfoModal && } {showEnrollMdmModal && (host.dep_assigned_to_fleet ? ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 8f72c023ec..dd5d715c00 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -45,7 +45,8 @@ import { import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; import Spinner from "components/Spinner"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import MainContent from "components/MainContent"; import BackLink from "components/BackLink"; import RunScriptDetailsModal from "pages/DashboardPage/cards/ActivityFeed/components/RunScriptDetailsModal"; @@ -124,6 +125,7 @@ interface IHostDetailsSubNavItem { name: string | JSX.Element; title: string; pathname: string; + count?: number; } const DEFAULT_ACTIVITY_PAGE_SIZE = 8; @@ -755,16 +757,10 @@ const HostDetailsPage = ({ pathname: PATHS.HOST_QUERIES(hostIdFromURL), }, { - name: ( - <> - {failingPoliciesCount > 0 && ( - {failingPoliciesCount} - )} - Policies - - ), + name: "Policies", title: "policies", pathname: PATHS.HOST_POLICIES(hostIdFromURL), + count: failingPoliciesCount, }, ]; @@ -844,7 +840,7 @@ const HostDetailsPage = ({ )} hostMdmDeviceStatus={hostMdmDeviceStatus} /> - + navigateToNav(i)} @@ -853,7 +849,13 @@ const HostDetailsPage = ({ {hostDetailsSubNav.map((navItem) => { // Bolding text when the tab is active causes a layout shift // so we add a hidden pseudo element with the same text string - return {navItem.name}; + return ( + + + {navItem.name} + + + ); })} @@ -956,7 +958,7 @@ const HostDetailsPage = ({ /> - + {showDeleteHostModal && ( setShowDeleteHostModal(false)} diff --git a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss index 25f6e705f6..00621284c3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss @@ -1,11 +1,4 @@ .host-details { - &__tabs-wrapper { - .react-tabs__tab { - display: inline-flex; - flex-direction: row; - } - } - // grid layout styles for the host details page &__details-panel { display: grid; diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss index cd225a7577..d9efc254f5 100644 --- a/frontend/pages/hosts/details/_styles.scss +++ b/frontend/pages/hosts/details/_styles.scss @@ -71,24 +71,15 @@ } } - &__tabs-wrapper { + &__tab-nav { background-color: $ui-off-white; width: 100%; // direct descendant of selector allows us to only change the first level of // tab styling and not change the tabs inside the cards. > .react-tabs > .react-tabs__tab-list { - .react-tabs__tab { - padding: 6px 0px 16px 0px; - margin-right: $pad-xxlarge; - } .react-tabs__tab--selected { background-color: $ui-off-white; - - // When tabbing through the app - &:focus-visible { - background-color: $ui-vibrant-blue-10; - } } } } @@ -160,6 +151,6 @@ // we dont need the margin on the host details page as we are not using grid css // for the spacing. -.host-details__tabs-wrapper .card { +.host-details__tab-nav .card { margin-top: 0; } diff --git a/frontend/pages/hosts/details/cards/Activity/Activity.tsx b/frontend/pages/hosts/details/cards/Activity/Activity.tsx index 58646d7661..97739410a1 100644 --- a/frontend/pages/hosts/details/cards/Activity/Activity.tsx +++ b/frontend/pages/hosts/details/cards/Activity/Activity.tsx @@ -8,7 +8,8 @@ import { } from "services/entities/activities"; import Card from "components/Card"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; @@ -67,20 +68,17 @@ const Activity = ({
)}

Activity

- + - Past - Upcoming - {!!upcomingCount && ( - - {upcomingCount} - - )} + Past + + + Upcoming @@ -106,7 +104,7 @@ const Activity = ({ /> - + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/_styles.scss b/frontend/pages/hosts/details/cards/Activity/_styles.scss index 7fc58cd091..3aee164762 100644 --- a/frontend/pages/hosts/details/cards/Activity/_styles.scss +++ b/frontend/pages/hosts/details/cards/Activity/_styles.scss @@ -6,21 +6,6 @@ margin: 0 0 $pad-large; } - .react-tabs__tab-list { - li:first-of-type { - padding-bottom: 10px; // adds 2px to bottom padding of "Past" tab to adjust for the count badge on the "Upcoming" tab - } - } - - &__upcoming-count { - padding: $pad-xxsmall $pad-xsmall; - color: $core-white; - background-color: $core-vibrant-blue; - border-radius: $border-radius; - font-weight: $bold; - margin-left: $pad-small; - } - &__loading-overlay { height: 100%; width: 100%; diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx index 74c30fd026..078d7158b3 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx @@ -11,7 +11,8 @@ import { } from "interfaces/software"; import Modal from "components/Modal"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import Button from "components/buttons/Button"; import DataSet from "components/DataSet"; import { dateAgo } from "utilities/date_format"; @@ -169,11 +170,15 @@ const TabsContent = ({ software: IHostSoftware; }) => { return ( - + - Software details - Install details + + Software details + + + Install details + @@ -185,7 +190,7 @@ const TabsContent = ({ /> - + ); }; diff --git a/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx b/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx index 1f18639c42..10ff28f75f 100644 --- a/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx +++ b/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx @@ -6,7 +6,8 @@ import useToggleSidePanel from "hooks/useToggleSidePanel"; import MainContent from "components/MainContent"; import SidePanelContent from "components/SidePanelContent"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; import PATHS from "router/paths"; @@ -79,7 +80,7 @@ const NewLabelPage = ({ router, location, children }: INewLabelPageProps) => { Dynamic (smart) labels are assigned to hosts if the query returns results. Manual labels are assigned to selected hosts.

- + { {labelSubNav.map((navItem) => { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { showOpenSidebarButton, onOpenSidebar, diff --git a/frontend/pages/labels/NewLabelPage/_styles.scss b/frontend/pages/labels/NewLabelPage/_styles.scss index 637d670a31..bb1592ba83 100644 --- a/frontend/pages/labels/NewLabelPage/_styles.scss +++ b/frontend/pages/labels/NewLabelPage/_styles.scss @@ -9,7 +9,7 @@ font-size: $xx-small; } - &__new-label-tabs-wrapper { + &__new-label-tab-nav { margin-bottom: $pad-xxlarge; } } diff --git a/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx b/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx index b78ffbb171..07f2aa5c3a 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx @@ -15,7 +15,8 @@ import { ITarget } from "interfaces/target"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import InfoBanner from "components/InfoBanner"; import ShowQueryModal from "components/modals/ShowQueryModal"; import TooltipWrapper from "components/TooltipWrapper"; @@ -240,23 +241,22 @@ const PolicyResults = ({ onClickRunAgain={onRunQuery} onClickStop={onStopQuery} /> - + setNavTabIndex(i)}> - {NAV_TITLES.RESULTS} + + {NAV_TITLES.RESULTS} + - - {errors?.length > 0 && ( - {errors.length} - )} + {NAV_TITLES.ERRORS} - + {renderResultsTable()} {renderErrorsTable()} - + {showQueryModal && ( )} - + setNavTabIndex(i)}> {NAV_TITLES.RESULTS} - - {errors?.length > 0 && ( - - {errors.length.toLocaleString()} - - )} + {NAV_TITLES.ERRORS} - + {renderResultsTab()} {renderErrorsTab()} - + {showQueryModal && (