Fleet UI [Feature]: UI reskin (#33558)

This commit is contained in:
RachelElysia 2025-09-29 10:10:41 -07:00 committed by GitHub
parent c6474eca82
commit efc64389b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
469 changed files with 4765 additions and 4108 deletions

View file

@ -50,9 +50,7 @@ const config: StorybookConfig = {
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-mdx-gfm",
"@storybook/addon-a11y",
"@storybook/test-runner",
"@storybook/addon-designs",
"@storybook/addon-webpack5-compiler-babel",
],

View file

@ -1,9 +1,10 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import Select, {
StylesConfig,
components,
DropdownIndicatorProps,
OptionProps,
components,
SelectInstance,
StylesConfig,
} from "react-select-5";
import { PADDING } from "styles/var/padding";
@ -12,6 +13,7 @@ import classnames from "classnames";
import { IDropdownOption } from "interfaces/dropdownOption";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
@ -26,11 +28,11 @@ interface IActionsDropdownProps {
className?: string;
menuAlign?: "right" | "left" | "default";
menuPlacement?: "top" | "bottom" | "auto";
variant?: "button";
variant?: "button" | "brand-button" | "small-button";
}
const getOptionBackgroundColor = (state: any) => {
return state.isFocused ? COLORS["ui-vibrant-blue-10"] : "transparent";
return state.isFocused ? COLORS["ui-fleet-black-5"] : "transparent";
};
const getLeftMenuAlign = (menuAlign: "right" | "left" | "default") => {
@ -40,7 +42,7 @@ const getLeftMenuAlign = (menuAlign: "right" | "left" | "default") => {
case "left":
return "0";
default:
return "-12px";
return "undefined";
}
};
@ -48,6 +50,8 @@ const getRightMenuAlign = (menuAlign: "right" | "left" | "default") => {
switch (menuAlign) {
case "right":
return "0";
case "left":
return "auto";
default:
return "undefined";
}
@ -61,7 +65,7 @@ const CustomDropdownIndicator = (
const color =
isFocused || selectProps.menuIsOpen || variant === "button"
? "core-fleet-blue"
? "ui-fleet-black-75"
: "core-fleet-black";
return (
@ -118,38 +122,99 @@ const ActionsDropdown = ({
}: IActionsDropdownProps): JSX.Element => {
const dropdownClassnames = classnames(baseClass, className);
// Used for brand Action button
const [menuIsOpen, setMenuIsOpen] = useState(false);
const selectRef = useRef<SelectInstance<IDropdownOption, false>>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
// Close on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// If click was outside wrapper, close menu
if (
menuIsOpen &&
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setMenuIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuIsOpen]);
// Shows a brand "Action" button instead
const ButtonControl = (
props: DropdownIndicatorProps<IDropdownOption, false>
) => {
const { selectProps } = props;
const handleButtonClick = () => {
if (selectProps.menuIsOpen) {
setMenuIsOpen(false);
if (selectProps.onMenuClose) selectProps.onMenuClose();
} else {
setMenuIsOpen(true);
if (selectProps.onMenuOpen) selectProps.onMenuOpen();
}
};
return (
<Button
type="button"
onClick={handleButtonClick}
className={`${baseClass}__button`}
disabled={selectProps.isDisabled}
aria-haspopup="listbox"
aria-expanded={selectProps.menuIsOpen}
>
Actions
</Button>
);
};
const handleChange = (newValue: IDropdownOption | null) => {
if (newValue) {
onChange(newValue.value.toString());
setMenuIsOpen(false); // close menu on select
}
};
console.log("variant", variant);
const customStyles: StylesConfig<IDropdownOption, false> = {
control: (provided, state) => ({
...provided,
display: "flex",
flexDirection: "row",
width: "max-content",
padding: "8px 0",
// Need minHeight to override default
minHeight: variant === "small-button" ? "20px" : "32px", // Match button height
padding: variant === "small-button" ? "2px 4px" : "8px", // Match button padding
backgroundColor: "initial",
border: 0,
boxShadow: "none",
cursor: "pointer",
"&:hover": {
background: COLORS["ui-fleet-black-5"], // Match button hover
boxShadow: "none",
".actions-dropdown-select__placeholder": {
color: COLORS["core-vibrant-blue-over"],
color: COLORS["ui-fleet-black-75-over"],
},
".actions-dropdown-select__indicator path": {
stroke: COLORS["core-vibrant-blue-over"],
stroke: COLORS["ui-fleet-black-75-over"],
},
},
"&:active .actions-dropdown-select__indicator path": {
stroke: COLORS["core-vibrant-blue-down"],
stroke: COLORS["ui-fleet-black-75-down"],
},
// TODO: Figure out a way to apply separate &:focus-visible styling
// Currently only relying on &:focus styling for tabbing through app
...(state.menuIsOpen && {
background: COLORS["ui-fleet-black-5"], // Match button hover
".actions-dropdown-select__indicators": {
height: "20px",
},
".actions-dropdown-select__indicator svg": {
transform: "rotate(180deg)",
transition: "transform 0.25s ease",
@ -160,10 +225,10 @@ const ActionsDropdown = ({
...provided,
color:
state.isFocused || variant === "button"
? COLORS["core-fleet-blue"]
? COLORS["ui-fleet-black-75"]
: COLORS["core-fleet-black"],
fontSize: "14px",
fontWeight: variant === "button" ? "bold" : undefined,
fontSize: "13px",
fontWeight: variant === "button" ? "600" : undefined,
lineHeight: "normal",
paddingLeft: 0,
marginTop: "1px",
@ -211,18 +276,18 @@ const ActionsDropdown = ({
...provided,
padding: "10px 8px",
borderRadius: "4px",
fontSize: "14px",
fontSize: "13px",
backgroundColor: getOptionBackgroundColor(state),
whiteSpace: "nowrap",
"&:hover": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-10"],
: COLORS["ui-fleet-black-5"],
},
"&:active": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-25"],
: COLORS["ui-fleet-black-5"],
},
...(state.isDisabled && {
color: COLORS["ui-fleet-black-50"],
@ -232,20 +297,25 @@ const ActionsDropdown = ({
};
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper`} ref={wrapperRef}>
<Select<IDropdownOption, false>
ref={selectRef}
options={options}
placeholder={placeholder}
placeholder={variant === "brand-button" ? "" : placeholder}
onChange={handleChange}
isDisabled={disabled}
isSearchable={isSearchable}
styles={customStyles}
menuIsOpen={menuIsOpen}
onMenuOpen={() => setMenuIsOpen(true)} // Needed abstraction for brand-action button
onMenuClose={() => setMenuIsOpen(false)} // Needed abstraction for brand-action-button
components={{
DropdownIndicator: CustomDropdownIndicator,
IndicatorSeparator: () => null,
Option: CustomOption,
SingleValue: () => null, // Doesn't replace placeholder text with selected text
// Note: react-select doesn't support skipping disabled options when keyboarding through
...(variant === "brand-button" && { Control: ButtonControl }), // Needed for brand-action button
}}
controlShouldRenderValue={false} // Doesn't change placeholder text to selected text
isOptionSelected={() => false} // Hides any styling on selected option
@ -254,7 +324,7 @@ const ActionsDropdown = ({
classNamePrefix={`${baseClass}-select`}
isOptionDisabled={(option) => !!option.disabled}
menuPlacement={menuPlacement}
{...{ variant }} // Allows CustomDropdownIndicator to be blue for variant: "button"
{...{ variant }} // Allows CustomDropdownIndicator to be ui-fleet-black-75 for variant: "button"
/>
</div>
);

View file

@ -1,6 +1,11 @@
// All other styling in customStyles part of react-select-5
.actions-dropdown-select__control {
&:focus-visible {
background-color: $core-fleet-blue;
background-color: $ui-fleet-black-75;
}
}
.actions-dropdown__wrapper {
display: flex;
align-items: center;
}

View file

@ -100,7 +100,7 @@ const ActivityItem = ({
const { actor_email } = activity;
const { gravatar_url } = actor_email
? addGravatarUrlToResource({ email: actor_email })
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
: { gravatar_url: undefined };
// wrapped just in case the date string does not parse correctly
let activityCreatedAt: Date | null = null;

View file

@ -47,10 +47,12 @@
margin-bottom: $pad-large;
&:focus {
outline: 2px solid $ui-vibrant-blue-25;
outline: 2px solid $ui-fleet-black-75;
}
&:hover, &:focus, &:focus-within {
&:hover,
&:focus,
&:focus-within {
border-radius: $border-radius-large;
background-color: $ui-off-white;
cursor: pointer;
@ -72,8 +74,6 @@
}
.button {
height: 16px;
&--icon svg {
padding: 0;
}
@ -89,7 +89,6 @@
visibility: hidden;
}
&__close-icon {
cursor: pointer;
&:hover {
@ -99,7 +98,7 @@
}
}
}
};
}
&__details-topline {
font-size: $x-small;

View file

@ -60,7 +60,7 @@ const AddHostsModal = ({
<span className="info__data">
You have no enroll secrets.{" "}
{openEnrollSecretModal ? (
<Button onClick={onManageEnrollSecretsClick} variant="text-link">
<Button onClick={onManageEnrollSecretsClick} variant="inverse">
Manage enroll secrets
</Button>
) : (

View file

@ -1,6 +1,6 @@
import React, { useContext } from "react";
import { Link } from "react-router";
import CustomLink from "components/CustomLink";
import PATHS from "router/paths";
import { AppContext } from "context/app";
@ -27,9 +27,10 @@ const AndroidPanel = ({ enrollSecret }: IAndroidPanelProps) => {
if (!isAndroidMdmEnabledAndConfigured) {
return (
<p>
<Link to={PATHS.ADMIN_INTEGRATIONS_MDM_ANDROID}>
Turn on Android MDM
</Link>{" "}
<CustomLink
url={PATHS.ADMIN_INTEGRATIONS_MDM_ANDROID}
text="Turn on Android MDM"
/>{" "}
to enroll Android hosts.
</p>
);

View file

@ -1,6 +1,6 @@
import React, { useContext } from "react";
import { Link } from "react-router";
import CustomLink from "components/CustomLink";
import PATHS from "router/paths";
import { AppContext } from "context/app";
@ -32,7 +32,10 @@ const IosIpadosPanel = ({ enrollSecret }: IosIpadosPanelProps) => {
if (!isMacMdmEnabledAndConfigured) {
return (
<p>
<Link to={PATHS.ADMIN_INTEGRATIONS_MDM_APPLE}>Turn on Apple MDM</Link>{" "}
<CustomLink
url={PATHS.ADMIN_INTEGRATIONS_MDM_APPLE}
text="Turn on Apple MDM"
/>{" "}
to enroll iOS & iPadOS hosts.
</p>
);

View file

@ -200,12 +200,12 @@ const PlatformWrapper = ({
</>
)}
<Button
variant="text-icon"
variant="inverse"
className={`${baseClass}__fleet-certificate-download`}
onClick={onDownloadCertificate}
>
Download
<Icon name="download" color="core-fleet-blue" size="small" />
<Icon name="download" size="small" />
</Button>
</p>
) : (
@ -302,7 +302,7 @@ const PlatformWrapper = ({
information below.
</p>
<InfoBanner className={`${baseClass}__chromeos--instructions`}>
For a step-by-step guide, see the documentation page for{" "}
For a step-by-step guide, see the documentation page for&nbsp;
<CustomLink
url="https://fleetdm.com/docs/using-fleet/adding-hosts#enroll-chromebooks"
text="adding hosts"
@ -368,7 +368,7 @@ const PlatformWrapper = ({
<div>
<InfoBanner className={`${baseClass}__chrome--instructions`}>
This works for macOS, Windows, and Linux hosts. To add
Chromebooks,{" "}
Chromebooks,&nbsp;
<Button
variant="text-link-dark"
onClick={() => setSelectedTabIndex(4)}
@ -396,11 +396,11 @@ const PlatformWrapper = ({
Osquery uses an enroll secret to authenticate with the Fleet
server.
<br />
<Button variant="text-icon" onClick={onDownloadEnrollSecret}>
<Button variant="inverse" onClick={onDownloadEnrollSecret}>
Download
<Icon
name="download"
color="core-fleet-blue"
color="ui-fleet-black-75"
size="small"
/>
</Button>
@ -421,13 +421,9 @@ const PlatformWrapper = ({
{fetchCertificateError}
</span>
) : (
<Button variant="text-icon" onClick={onDownloadFlagfile}>
<Button variant="inverse" onClick={onDownloadFlagfile}>
Download
<Icon
name="download"
color="core-fleet-blue"
size="small"
/>
<Icon name="download" size="small" />
</Button>
)}
</p>

View file

@ -1,18 +1,6 @@
@import "../../../../../../../node_modules/react-tabs/style/react-tabs.scss";
.platform-wrapper {
padding: 0; // different to pad sticky subnav properly
.tab-nav {
// negate problematic sticky positioning
position: initial;
.react-tabs {
&__tab-list {
margin: 0 0 $pad-large;
}
}
}
h1 {
margin-bottom: $pad-small;
}
@ -30,7 +18,7 @@
font-family: "SourceCodePro", $monospace;
font-weight: $bold;
color: $core-fleet-blue;
line-height: 20px;
line-height: $line-height;
line-break: anywhere;
padding: 12px $pad-xlarge 12px $pad-medium;
}

View file

@ -1,8 +1,8 @@
.app {
background: $gradients-dark-gradient-vertical;
@include gradient-background;
margin: 0;
& > div {
min-height: 100vh;
min-height: 100vh;
}
}

View file

@ -1,21 +1,61 @@
import React from "react";
import classnames from "classnames";
import Card from "components/Card";
import CardHeader from "components/CardHeader";
// @ts-ignore
import fleetLogoText from "../../../assets/images/fleet-logo-text-white.svg";
import OrgLogoIcon from "components/icons/OrgLogoIcon";
import FleetIcon from "../../../assets/images/fleet-avatar-24x24@2x.png";
interface IAuthenticationFormWrapperProps {
children: React.ReactNode;
header?: string;
headerCta?: React.ReactNode;
/** Only used on the registration page */
breadcrumbs?: React.ReactNode;
className?: string;
}
const baseClass = "auth-form-wrapper";
const AuthenticationFormWrapper = ({
children,
}: IAuthenticationFormWrapperProps) => (
<div className={baseClass}>
<img alt="Fleet" src={fleetLogoText} className={`${baseClass}__logo`} />
{children}
</div>
);
header,
headerCta,
breadcrumbs,
className,
}: IAuthenticationFormWrapperProps) => {
const classNames = classnames(baseClass, className);
return (
<div className="app-wrap">
<nav className="site-nav-container">
<div className="site-nav-content">
<ul className="site-nav-left">
<li className="site-nav-item dup-org-logo" key="dup-org-logo">
<div className="site-nav-item__logo-wrapper">
<div className="site-nav-item__logo">
<OrgLogoIcon className="logo" src={FleetIcon} />
</div>
</div>
</li>
</ul>
</div>
</nav>
{breadcrumbs}
<div className={classNames}>
<Card className={`${baseClass}__card`} paddingSize="xxlarge">
{(header || headerCta) && (
<div className={`${baseClass}__header-container`}>
{header && <CardHeader header={header} />}
{headerCta && headerCta}
</div>
)}
{children}
</Card>
</div>
</div>
);
};
export default AuthenticationFormWrapper;

View file

@ -6,9 +6,18 @@
padding: $pad-medium 0;
gap: 2.5rem;
align-items: center;
@include gradient-background;
&__logo {
width: 120px;
&__card {
@include vertical-card-layout;
width: 470px;
}
&__header-container {
display: flex;
align-items: center;
padding-bottom: $pad-medium;
border-bottom: 1px solid $ui-fleet-black-10;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {

View file

@ -0,0 +1,53 @@
import React, { useEffect } from "react";
import { InjectedRouter, browserHistory } from "react-router";
import paths from "router/paths";
import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
const baseClass = "authentication-nav";
interface IAuthenticationNav {
previousLocation?: string;
router?: InjectedRouter;
}
const AuthenticationNav = ({
previousLocation,
router,
}: IAuthenticationNav): JSX.Element => {
useEffect(() => {
const closeWithEscapeKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && router) {
router.push(paths.LOGIN);
}
};
document.addEventListener("keydown", closeWithEscapeKey);
return () => {
document.removeEventListener("keydown", closeWithEscapeKey);
};
}, []);
const onClick = (): void => {
if (previousLocation) {
browserHistory.push(previousLocation);
} else browserHistory.goBack();
};
return (
<div className={`${baseClass}__back`}>
<Button
onClick={onClick}
className={`${baseClass}__back-link`}
variant="inverse"
>
<Icon name="close" color="core-fleet-black" />
</Button>
</div>
);
};
export default AuthenticationNav;

View file

@ -0,0 +1,52 @@
.authentication-nav {
display: flex;
margin-bottom: $pad-large;
&__back {
margin-left: auto;
}
// transition: opacity 300ms ease-in;
// width: 516px;
// &__box {
// background-color: $core-fleet-white;
// border-radius: 10px;
// box-sizing: border-box;
// padding: $pad-xxlarge;
// font-weight: $regular;
// position: relative;
// display: flex;
// flex-direction: column;
// align-items: center;
// gap: $pad-medium;
// p {
// font-size: $x-small;
// margin: 0;
// }
// }
// &__header-text {
// font-size: $x-small;
// font-weight: $bold;
// color: $core-fleet-black;
// line-height: 32px;
// margin-top: 0;
// margin-bottom: 0;
// }
// &__back {
// text-align: right;
// width: 100%;
// &-link {
// position: absolute;
// top: 36px;
// right: 36px;
// img {
// width: 16px;
// height: 16px;
// }
// }
// }
}

View file

@ -0,0 +1 @@
export { default } from "./AuthenticationNav";

View file

@ -1,6 +1,7 @@
import React, { useState, useCallback } from "react";
import classnames from "classnames";
import { COLORS } from "styles/var/colors";
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
interface IFleetAvatarProps {
@ -94,26 +95,26 @@ const APIOnlyAvatar = ({ className }: IAPIOnlyAvatar) => {
cy="16"
r="15"
fill="white"
stroke="#6A67FE"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="2"
/>
<path
d="M11.5 12.75L8 16L11.5 19.25"
stroke="#6A67FE"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18 11L14.5 21"
stroke="#6A67FE"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21 19.25L24.5 16L21 12.75"
stroke="#6A67FE"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@ -122,6 +123,44 @@ const APIOnlyAvatar = ({ className }: IAPIOnlyAvatar) => {
);
};
interface IDefaultAvatar {
className?: string;
}
const DefaultAvatar = ({ className }: IAPIOnlyAvatar) => {
return (
<svg
data-testid="default-avatar"
className={className}
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="16"
cy="16"
r="15.25"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="1.5"
/>
<circle
cx="16"
cy="13"
r="5.25"
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="1.5"
/>
<path
stroke={COLORS["ui-fleet-black-50"]}
strokeWidth="1.5"
d="M16 18.75c5.084 0 9.21 4.102 9.248 9.178-.01.017-.024.044-.052.08a2.945 2.945 0 0 1-.462.463c-.448.374-1.127.812-1.99 1.23-1.725.833-4.114 1.549-6.744 1.549s-5.019-.716-6.744-1.55c-.863-.417-1.542-.855-1.99-1.23a2.945 2.945 0 0 1-.462-.461c-.028-.037-.043-.064-.053-.081A9.25 9.25 0 0 1 16 18.75Z"
/>
</svg>
);
};
interface IAvatarUserInterface {
gravatar_url?: string;
gravatar_url_dark?: string;
@ -172,16 +211,18 @@ const Avatar = ({
avatar = <FleetAvatar className={avatarClasses} />;
} else if (useApiOnlyAvatar) {
avatar = <APIOnlyAvatar className={avatarClasses} />;
} else {
} else if (gravatar_url) {
avatar = (
<img
alt="User avatar"
className={`${avatarClasses} ${isLoading || isError ? "default" : ""}`}
src={gravatar_url || DEFAULT_GRAVATAR_LINK}
src={gravatar_url}
onError={onError}
onLoad={onLoad}
/>
);
} else {
avatar = <DefaultAvatar className={avatarClasses} />;
}
return <div className="avatar-wrapper">{avatar}</div>;

View file

@ -13,7 +13,7 @@
}
&.has-white-background {
background: $core-white;
background: $core-fleet-white;
border-radius: 100%;
}

View file

@ -1,10 +1,10 @@
import React, { useState, useCallback } from "react";
import classnames from "classnames";
import { DEFAULT_GRAVATAR_LINK_DARK } from "utilities/constants";
import { DEFAULT_GRAVATAR_LINK } from "utilities/constants";
interface IAvatarUserInterface {
gravatar_url_dark?: string;
gravatar_url?: string;
}
export interface IAvatarInterface {
@ -29,14 +29,14 @@ const Avatar = ({ className, size, user }: IAvatarInterface): JSX.Element => {
const avatarClasses = classnames(baseClass, className, {
[`${baseClass}--${size?.toLowerCase()}`]: !!size,
});
const { gravatar_url_dark } = user;
const { gravatar_url } = user;
return (
<div className="avatar-wrapper-top-nav">
<img
alt="User avatar"
className={`${avatarClasses} ${isLoading || isError ? "default" : ""}`}
src={gravatar_url_dark || DEFAULT_GRAVATAR_LINK_DARK}
src={gravatar_url || DEFAULT_GRAVATAR_LINK}
onError={onError}
onLoad={onLoad}
data-testid="user-avatar"

View file

@ -0,0 +1,17 @@
import { Meta, StoryObj } from "@storybook/react";
import BackButton from "./BackButton";
const meta: Meta<typeof BackButton> = {
title: "Components/BackButton",
component: BackButton,
args: {
text: "Back",
},
};
export default meta;
type Story = StoryObj<typeof BackButton>;
export const Basic: Story = {};

View file

@ -1,10 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import BackLink from "./BackLink";
import BackButton from "./BackButton";
describe("BackLink - component", () => {
describe("BackButton - component", () => {
it("renders text and icon", () => {
render(<BackLink text="Back to software" />);
render(<BackButton text="Back to software" />);
const text = screen.getByText("Back to software");
const icon = screen.getByTestId("chevron-left-icon");

View file

@ -0,0 +1,37 @@
import React from "react";
import { browserHistory } from "react-router";
import Icon from "components/Icon";
import classnames from "classnames";
import Button from "components/buttons/Button";
interface IBackButtonProps {
/** Default: "Back" */
text?: string;
path?: string;
className?: string;
}
const baseClass = "back-button";
const BackButton = ({
text = "Back",
path,
className,
}: IBackButtonProps): JSX.Element => {
const classes = classnames(baseClass, className);
const onClick = (): void => {
if (path) {
browserHistory.push(path);
} else browserHistory.goBack();
};
return (
<Button variant="inverse" onClick={onClick} className={classes}>
<Icon name="chevron-left" color="ui-fleet-black-50" />
<span>{text}</span>
</Button>
);
};
export default BackButton;

View file

@ -0,0 +1 @@
export { default } from "./BackButton";

View file

@ -1,17 +0,0 @@
import { Meta, StoryObj } from "@storybook/react";
import BackLink from "./BackLink";
const meta: Meta<typeof BackLink> = {
title: "Components/BackLink",
component: BackLink,
args: {
text: "Back",
},
};
export default meta;
type Story = StoryObj<typeof BackLink>;
export const Basic: Story = {};

View file

@ -1,35 +0,0 @@
import React from "react";
import { browserHistory, Link } from "react-router";
import Icon from "components/Icon";
import classnames from "classnames";
interface IBackLinkProps {
text: string;
path?: string;
className?: string;
}
const baseClass = "back-link";
const BackLink = ({ text, path, className }: IBackLinkProps): JSX.Element => {
const onClick = (): void => {
if (path) {
browserHistory.push(path);
} else browserHistory.goBack();
};
return (
<Link
className={classnames(baseClass, className)}
to={path || ""}
onClick={onClick}
>
<>
<Icon name="chevron-left" color="core-fleet-blue" />
<span>{text}</span>
</>
</Link>
);
};
export default BackLink;

View file

@ -1,3 +0,0 @@
.back-link {
@include direction-link;
}

View file

@ -1 +0,0 @@
export { default } from "./BackLink";

View file

@ -64,7 +64,7 @@
// color styles
&__white {
background-color: $core-white;
background-color: $core-fleet-white;
}
&__grey {

View file

@ -1,5 +1,4 @@
.card-header {
margin: 0 0 $pad-large;
display: flex;
flex-direction: column;
gap: $pad-small;

View file

@ -43,7 +43,7 @@ const CustomLink = ({
case "banner-link":
return "core-fleet-black";
default:
return "core-fleet-blue";
return "ui-fleet-black-75";
}
};

View file

@ -1,4 +1,24 @@
.custom-link {
@include link;
@include animated-bottom-border($core-fleet-black, $ui-fleet-black-25);
.external-link-icon__outline {
transition: fill 0.2s;
}
.external-link-icon__arrow {
transition: stroke 0.2s;
}
// Inverse hover over effect
&:hover {
.external-link-icon__outline {
fill: $ui-fleet-black-75-over;
}
.external-link-icon__arrow {
stroke: $core-fleet-white;
}
}
// Changing display will break multiline links
&:not(.custom-link--multiline) {
display: inline-flex;
@ -22,5 +42,23 @@
&--tooltip-link {
font-size: inherit; // Overrides link default font size with parent tooltip font size
@include animated-bottom-border($core-fleet-white, $ui-off-white);
.external-link-icon__outline {
stroke: $core-fleet-white;
}
// Inverse hover over effect
&:hover {
color: $ui-off-white;
.external-link-icon__outline {
fill: $ui-off-white;
}
.external-link-icon__arrow {
stroke: $tooltip-bg;
}
}
}
}

View file

@ -12,6 +12,8 @@
dt {
font-weight: $bold;
display: flex;
color: $core-fleet-black;
line-height: $line-height;
}
dd {

View file

@ -3,6 +3,8 @@ import React from "react";
import { ITeam } from "interfaces/team";
import { IEnrollSecret } from "interfaces/enroll_secret";
import Card from "components/Card";
import EmptyTable from "components/EmptyTable";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
@ -53,33 +55,9 @@ const EnrollSecretModal = ({
<div className={`${baseClass} form`}>
{teamInfo?.secrets?.length ? (
<>
<div className={`${baseClass}__description`}>
Use these secret(s) to enroll hosts
{primoMode ? (
""
) : (
<>
{" "}
to <b>{teamInfo?.name}</b>
</>
)}
:
</div>
<EnrollSecretTable
secrets={teamInfo?.secrets}
toggleSecretEditorModal={toggleSecretEditorModal}
toggleDeleteSecretModal={toggleDeleteSecretModal}
setSelectedSecret={setSelectedSecret}
/>
</>
) : (
<>
<div className={`${baseClass}__description`}>
<p>
<b>You have no enroll secrets.</b>
</p>
<p>
Add secret(s) to enroll hosts
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__description`}>
Use these secret(s) to enroll hosts
{primoMode ? (
""
) : (
@ -89,27 +67,70 @@ const EnrollSecretModal = ({
</>
)}
.
</p>
</div>
<div className={`${baseClass}__add-secret`}>
<GitOpsModeTooltipWrapper
position="right"
tipOffset={8}
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
onClick={addNewSecretClick}
className={`${baseClass}__add-secret-btn`}
variant="brand-inverse-icon"
iconStroke
>
Add secret <Icon name="plus" color="core-fleet-green" />
</Button>
)}
/>
</div>
</div>
<EnrollSecretTable
secrets={teamInfo?.secrets}
toggleSecretEditorModal={toggleSecretEditorModal}
toggleDeleteSecretModal={toggleDeleteSecretModal}
setSelectedSecret={setSelectedSecret}
/>
</>
) : (
<Card color="grey" paddingSize="small">
<EmptyTable
header="You have no enroll secrets."
info={
<>
Add secret(s) to enroll hosts
{primoMode ? (
""
) : (
<>
{" "}
to <b>{teamInfo?.name}</b>
</>
)}
.
</>
}
primaryButton={
<GitOpsModeTooltipWrapper
position="right"
tipOffset={8}
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
onClick={addNewSecretClick}
className={`${baseClass}__add-secret-btn`}
variant="brand-inverse-icon"
iconStroke
>
Add secret <Icon name="plus" color="core-fleet-green" />
</Button>
)}
/>
}
/>
</Card>
)}
<div className={`${baseClass}__add-secret`}>
<GitOpsModeTooltipWrapper
position="right"
tipOffset={8}
renderChildren={(disableChildren) => (
<Button
disabled={disableChildren}
onClick={addNewSecretClick}
className={`${baseClass}__add-secret-btn`}
variant="text-icon"
iconStroke
>
Add secret <Icon name="plus" />
</Button>
)}
/>
</div>
<div className="modal-cta-wrap">
<Button onClick={onReturnToApp}>Done</Button>
</div>

View file

@ -17,4 +17,14 @@
&__error {
color: $ui-error;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.empty-table__container {
margin: $pad-large auto $pad-medium;
}
}

View file

@ -50,6 +50,7 @@ const EnrollSecretRow = ({
onClick={onEditSecretClick}
className={`${baseClass}__edit-secret-icon`}
variant="icon"
size="small"
>
<Icon name="pencil" />
</Button>
@ -58,6 +59,7 @@ const EnrollSecretRow = ({
disabled={disableChildren}
className={`${baseClass}__delete-secret-icon`}
variant="icon"
size="small"
>
<Icon name="trash" />
</Button>

View file

@ -21,5 +21,6 @@
&__edit-delete-btns {
display: flex;
gap: $pad-xxsmall;
}
}

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useState, useRef } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
@ -49,7 +49,7 @@ interface IFileUploaderProps {
* a link.
* @default "button"
*/
buttonType?: "button" | "link";
buttonType?: "button" | "brand-inverse-icon";
/** renders a tooltip for the button. If `gitopsCompatible` is set to `true`
* this tooltip will not be rendered if gitops mode is enabled. */
buttonTooltip?: React.ReactNode;
@ -95,7 +95,8 @@ export const FileUploader = ({
const classes = classnames(baseClass, className, {
[`${baseClass}__file-preview`]: isFileSelected,
});
const buttonVariant = buttonType === "button" ? "default" : "text-icon";
const buttonVariant =
buttonType === "button" ? "default" : "brand-inverse-icon";
const triggerFileInput = () => {
fileInputRef.current?.click();
@ -159,7 +160,9 @@ export const FileUploader = ({
tabIndex={0}
>
<label htmlFor="upload-file">
{buttonType === "link" && <Icon name="upload" />}
{buttonType === "brand-inverse-icon" && (
<Icon color="core-fleet-green" name="upload" />
)}
<span>{buttonMessage}</span>
</label>
</Button>
@ -187,7 +190,9 @@ export const FileUploader = ({
tabIndex={0}
>
<label htmlFor="upload-file">
{buttonType === "link" && <Icon name="upload" />}
{buttonType === "brand-inverse-icon" && (
<Icon color="core-fleet-green" name="upload" />
)}
<span>{buttonMessage}</span>
</label>
</Button>

View file

@ -3,7 +3,7 @@
flex-direction: column;
align-items: center;
border-radius: $border-radius-medium;
background-color: $ui-fleet-blue-10;
background-color: $core-fleet-white;
border: 1px solid $ui-fleet-black-10;
padding: $pad-xlarge $pad-large;
font-size: $x-small;

View file

@ -17,7 +17,7 @@
display: flex;
align-items: center;
justify-content: center;
color: $core-white;
color: $core-fleet-white;
padding: $pad-small $pad-medium;
z-index: 999;
background-color: $core-vibrant-blue;
@ -72,7 +72,7 @@
}
&__undo {
color: $core-white;
color: $core-fleet-white;
cursor: pointer;
font-size: $small;
text-decoration: underline;
@ -86,11 +86,11 @@
.fleeticon {
transition: color 150ms ease-in-out;
color: $core-white;
color: $core-fleet-white;
font-size: $small;
&:hover {
color: $core-white;
color: $core-fleet-white;
}
}
}

View file

@ -9,13 +9,13 @@
pre {
padding: 12px;
border: 1px solid $ui-blue-gray;
background-color: $ui-light-grey; // copy fleet ace background color
border: 1px solid $ui-fleet-black-10;
background-color: $core-fleet-white; // copy fleet ace background color
.ace_cursor {
// We have the !important here as there doesnt seen a way to programatically
// hide only the cursor in the editor.
display: none !important
}
// We have the !important here as there doesnt seen a way to programatically
// hide only the cursor in the editor.
display: none !important;
}
}
}

View file

@ -9,11 +9,14 @@
&__page-banner {
padding: $pad-large $pad-xlarge;
margin-bottom: $pad-large;
width: auto;
gap: $pad-small;
}
&__info {
display: flex;
align-items: center;
p {
margin: $pad-small 0 0 0; // TOFIX: this causes weird top margin noticed in #28110
}
@ -34,6 +37,6 @@
}
&__close {
margin: -$pad-small; // Offset clickable padding from making banner taller
margin: -$pad-medium; // Offset clickable padding from making banner taller
}
}

View file

@ -1,6 +1,6 @@
import { uniqueId } from "lodash";
import React from "react";
import { PlacesType, Tooltip as ReactTooltip5 } from "react-tooltip-5";
import { PlacesType } from "react-tooltip-5";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "inherited-badge";
@ -13,26 +13,18 @@ const InheritedBadge = ({
tooltipPosition = "top",
tooltipContent,
}: IInheritedBadgeProps) => {
const tooltipId = uniqueId();
return (
<div className={baseClass}>
<span
className={`${baseClass}__element-text`}
data-tooltip-id={tooltipId}
<TooltipWrapper
tipContent={tooltipContent}
showArrow
position={tooltipPosition}
tipOffset={8}
underline={false}
delayInMs={300} // TODO: Apply pattern of delay tooltip for repeated table tooltips
>
Inherited
</span>
<ReactTooltip5
className={`${baseClass}__tooltip-text`}
disableStyleInjection
place={tooltipPosition}
opacity={1}
id={tooltipId}
offset={8}
positionStrategy="fixed"
>
{tooltipContent}
</ReactTooltip5>
<span className={`${baseClass}__element-text`}>Inherited</span>
</TooltipWrapper>
</div>
);
};

View file

@ -5,8 +5,8 @@
color: $core-fleet-black;
line-height: 15px;
border-radius: $border-radius;
background: $ui-vibrant-blue-10;
padding: 4px;
background: $ui-fleet-black-10;
padding: 2px 4px;
}
@include tooltip5-arrow-styles;

View file

@ -8,7 +8,7 @@ import { pick } from "lodash";
const baseClass = "link-with-context";
interface ILinkWithContextProps {
children: React.ReactChild | React.ReactChild[];
children: React.ReactNode;
currentQueryParams: QueryParams;
to: string;
withParams: {

View file

@ -27,6 +27,6 @@
&__actions {
display: flex;
justify-content: flex-end;
gap: $pad-medium;
gap: $pad-xsmall; // Padding on buttons
}
}

View file

@ -588,22 +588,21 @@ const SelectTargets = ({
disablePagination
/>
<div className={`${baseClass}__targets-button-wrap`}>
<Button
className={`${baseClass}__btn`}
onClick={handleClickCancel}
variant="text-link"
>
Cancel
</Button>
<Button
className={`${baseClass}__btn`}
type="button"
variant="success"
disabled={isFetchingCounts || !counts?.targets_count} // TODO: confirm
onClick={onClickRun}
>
Run
</Button>
<Button
className={`${baseClass}__btn`}
onClick={handleClickCancel}
variant="inverse"
>
Cancel
</Button>
<div className={`${baseClass}__targets-total-count`}>
{renderTargetsCount()}
</div>

View file

@ -46,7 +46,7 @@ const TargetChipSelector = ({
data-selected={isSelected}
onClick={(e) => onClick(entity)(e)}
>
<Icon name={isSelected ? "check" : "plus"} />
<Icon name={isSelected ? "check" : "plus"} color="ui-fleet-black-75" />
<span className="selector-name">{displayText()}</span>
</button>
);

View file

@ -1,6 +1,6 @@
.target-chip-selector {
padding: $pad-small;
background-color: $core-white;
background-color: $core-fleet-white;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: $border-radius-medium;
@ -29,21 +29,25 @@
font-weight: $bold;
}
&[data-selected="true"] {
background-color: $ui-vibrant-blue-10;
box-shadow: inset 0 0 0 1px $core-vibrant-blue;
background-color: $ui-off-white;
box-shadow: inset 0 0 0 1px $ui-fleet-black-75;
}
&:hover {
background-color: $ui-vibrant-blue-10;
background-color: $ui-off-white;
box-shadow: inset 0 0 0 1px $core-fleet-black;
color: $core-fleet-black;
}
&:active {
box-shadow: inset 0 0 0 1px $core-vibrant-blue-down;
background-color: $ui-off-white;
box-shadow: inset 0 0 0 1px $core-fleet-black;
color: $core-fleet-black;
}
// When tabbing
&:focus-visible {
outline: 2px solid $ui-vibrant-blue-25;
outline: 1px solid $core-focused-outline;
outline-offset: 1px;
border-radius: $border-radius;
}

View file

@ -1,6 +1,7 @@
.main-content {
padding: $pad-xlarge;
background-color: $core-white;
max-width: 1280px;
margin: 0 auto;
padding: $pad-page;
flex-grow: 1;
// overflow: auto allows for horizontal scrolling
// of the main-content when there is a banner. (e.g. sandbox mode)

View file

@ -61,7 +61,7 @@
&__modal_container {
@include position(absolute, 22px null null null);
box-sizing: border-box;
background-color: $core-white;
background-color: $core-fleet-white;
padding: $pad-xxlarge;
border-radius: 8px;
animation: scale-up 150ms ease-out;

View file

@ -1,14 +1,25 @@
import React from "react";
import classnames from "classnames";
const baseClass = "page-description";
interface IPageDescription {
content: React.ReactNode;
/** Section descriptions styles differ from page level descriptions */
variant?: "card" | "tab-panel" | "right-panel" | "modal";
className?: string;
}
const PageDescription = ({ content }: IPageDescription) => {
const sectionVariants = ["card", "tab-panel", "right-panel", "modal"];
const PageDescription = ({ content, variant, className }: IPageDescription) => {
const classNames = classnames(baseClass, className, {
[`${baseClass}__section-description`]:
variant && sectionVariants.includes(variant),
});
return (
<div className={`${baseClass}`}>
<div className={classNames}>
<p>{content}</p>
</div>
);

View file

@ -1,10 +1,9 @@
.page-description {
margin: 0 0 $pad-xxlarge;
// uses parent gap for bottom margin
p {
color: $ui-fleet-black-75;
margin: 0;
font-size: $x-small;
font-style: italic;
}
}

View file

@ -1,10 +1,12 @@
.paginated-list {
gap: $pad-medium;
&__header {
padding: $pad-medium $pad-large;
font-size: $x-small;
border-bottom: 1px solid $ui-fleet-black-10;
background-color: $ui-off-white;
min-height: 24px; // Include padding: 40px;
}
.loading-overlay {

View file

@ -40,7 +40,7 @@ const Pagination = ({
onClick={onPrevPage}
className={`${baseClass}__pagination-button`}
>
<Icon name="chevron-left" color="core-fleet-blue" /> Previous
<Icon name="chevron-left" color="ui-fleet-black-75" /> Previous
</Button>
<Button
variant="inverse"
@ -48,7 +48,7 @@ const Pagination = ({
onClick={onNextPage}
className={`${baseClass}__pagination-button`}
>
Next <Icon name="chevron-right" color="core-fleet-blue" />
Next <Icon name="chevron-right" color="ui-fleet-black-75" />
</Button>
</div>
);

View file

@ -55,22 +55,24 @@ export const PlatformSelector = ({
);
return (
<span className={`${baseClass}__install-software`}>
<CustomLink text={softwareName} url={softwareLink} /> will only install
on{" "}
<TooltipWrapper
tipContent={
<>
To see targets, select{" "}
<b>{softwareName} &gt; Actions &gt; Edit</b>. Currently, hosts
that aren&apos;t targeted show an empty (---) policy status.
</>
}
>
targeted hosts
</TooltipWrapper>
.
</span>
<div className="form-field__help-text">
<span className={`${baseClass}__install-software`}>
<CustomLink text={softwareName} url={softwareLink} /> will only
install on{" "}
<TooltipWrapper
tipContent={
<>
To see targets, select{" "}
<b>{softwareName} &gt; Actions &gt; Edit</b>. Currently, hosts
that aren&apos;t targeted show an empty (---) policy status.
</>
}
>
targeted hosts
</TooltipWrapper>
.
</span>
</div>
);
};
@ -111,9 +113,7 @@ export const PlatformSelector = ({
ChromeOS
</Checkbox>
</span>
<div className="form-field__help-text">
{renderInstallSoftwareHelpText()}
</div>
{renderInstallSoftwareHelpText()}
</div>
);
};

View file

@ -2,6 +2,7 @@
&__label {
font-size: $x-small;
font-weight: $bold;
color: $core-fleet-black;
&--error {
color: $core-vibrant-red;
@ -9,11 +10,17 @@
&--with-action {
display: flex;
justify-content: space-between;
align-items: center;
button {
height: initial; // aligning space between label and textarea
margin: -$pad-small 0;
animation: fade-in 250ms ease-out;
}
.custom-link {
margin-bottom: 0;
}
}
}

View file

@ -8,16 +8,18 @@
line-height: 24px;
}
.ace_editor.ace-fleet.ace_focus {
box-shadow: inset 0 0 6px 0 rgba(0, 0, 0, 0.16);
.ace_editor.ace-fleet:hover {
border: 1px solid #8b8fa2; /* $ui-fleet-black-50 */
background: transparent;
}
.ace_editor.ace-fleet.ace_focus .ace_gutter {
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.16);
.ace_editor.ace-fleet.ace_focus {
border: 1px solid #192147; /* $core-fleet-black */
background: transparent;
}
.ace_editor.ace-fleet.ace_focus .ace_scroller {
border-bottom: solid 1px #c38dec;
box-shadow: 1px 1px 1px 1px #192147; /* $core-fleet-black */
}
.ace-fleet.ace_autocomplete .ace_content {
@ -29,18 +31,19 @@
}
.ace-fleet .ace_content {
background: #fff; /* $core-fleet-white */
height: 100%;
}
.ace-fleet .ace_gutter {
background: #fff;
color: #c38dec;
background: #fff; /* $core-fleet-white */
color: #515774; /* #$ui-fleet-black-75 */
z-index: 1;
border-right: solid 1px #e3e3e3;
}
.ace-fleet .ace_gutter-active-line {
background-color: rgba(174, 109, 223, 0.15);
background-color: #f4f4f6; /* $ui-fleet-black-5 */
border-radius: 2px;
}
@ -88,8 +91,8 @@
}
.ace-fleet .ace_keyword {
color: #ae6ddf;
font-weight: 700;
color: #515774; /* $ui-fleet-black-75 */
font-weight: 600;
}
.ace-fleet .ace_osquery-token {

View file

@ -8,7 +8,11 @@ import SectionHeader from ".";
const meta: Meta<typeof SectionHeader> = {
title: "Components/SectionHeader",
component: SectionHeader,
args: { title: "Section header title" },
args: {
title: "Section header title",
subTitle: "This is a subtitle",
details: <>These are details</>,
},
};
export default meta;

View file

@ -1,7 +1,6 @@
.section-header {
display: flex;
justify-content: space-between;
margin-bottom: $pad-xlarge;
&__left-header {
display: flex;
@ -15,6 +14,7 @@
&__sub-title {
&--grey {
font-size: $x-small;
@include grey-text;
}
}
@ -26,4 +26,9 @@
width: 100%;
color: $core-fleet-black;
}
// GOAL: rely on gap instead of these bottom margins
&__no-margin {
margin: 0;
}
}

View file

@ -1,8 +1,8 @@
import classnames from "classnames";
import React, { ReactChild } from "react";
import React, { ReactNode } from "react";
interface ISidePanelContentProps {
children: ReactChild;
children: ReactNode;
className?: string;
}

View file

@ -1,5 +1,5 @@
.side-panel-content {
background-color: $core-white;
background-color: $core-fleet-white;
box-sizing: border-box;
border-left: 1px solid $ui-gray;
min-width: 340px;

View file

@ -0,0 +1,24 @@
import classnames from "classnames";
import React, { ReactChild } from "react";
interface ISidePanelContentProps {
children: ReactChild;
className?: string;
}
const baseClass = "side-panel-page";
/**
* A component that controls the layout and styling of the side panel region of
* the application.
*/
const SidePanelPage = ({
children,
className,
}: ISidePanelContentProps): JSX.Element => {
const classes = classnames(baseClass, className);
return <div className={classes}>{children}</div>;
};
export default SidePanelPage;

View file

@ -0,0 +1,64 @@
/** Be extremely careful when editing this CSS
This is calculated to:
- Have the left margin always follow regular page left margins
- Always have a side panel flushed right
- If a side panel runs into the content, it doesn't affect the left margin, only changes the right margin
- If the side panel is closed, it follows regular page margins
Only variables should be updated unless there is a redesign of these pages
*/
$side-panel-width: 340px;
$main-max-width: 1280px; // 1280px plus 64px left/right padding
$main-padding: 32px; // Container for layout $pad-page
$main-max-width-including-padding: 1280px + 32px + 32px; // Cannot use variables in addition
.side-panel-page {
min-height: 100vh;
position: relative;
width: 100%;
// Fixed side panel: always flush right, always 340px wide
.side-panel-content {
position: absolute;
top: 0;
right: 0;
width: $side-panel-width;
height: auto;
z-index: 2;
}
/* Main Content logic per breakpoint */
/* Main Content logic per breakpoint */
.main-content {
position: relative;
box-sizing: border-box;
padding: $main-padding;
/* Default: for < ($main-max-width-including-padding + $side-panel-width) */
margin-right: $side-panel-width;
margin-left: 0;
width: auto;
max-width: unset;
/* For 1684px to 2024px */
@media (min-width: ($main-max-width-including-padding)) {
max-width: auto;
margin-left: calc((100vw - #{$main-max-width + 2 * $main-padding}) / 2);
margin-right: $side-panel-width;
}
/* For ≥ 2364px (perfectly centered) */
@media (min-width: ($main-max-width-including-padding + $side-panel-width + $side-panel-width)) {
max-width: $main-max-width-including-padding;
margin-left: auto;
margin-right: auto;
}
/* Undo centering logic with side panel if main content is the only child */
&:only-child {
margin-right: auto;
margin-left: auto;
max-width: $main-max-width-including-padding;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./SidePanelPage";

View file

@ -11,7 +11,7 @@
}
&.include-container {
background-color: $core-white;
background-color: $core-fleet-white;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
border-radius: 8px;
width: 48px;
@ -59,7 +59,7 @@
padding: 0;
&.white {
.path {
stroke: $core-white;
stroke: $core-fleet-white;
}
}
.loader {
@ -86,13 +86,13 @@
.path {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
stroke: $core-vibrant-blue;
stroke: $core-fleet-green;
animation: dash 1.5s ease-in-out infinite;
stroke-linecap: round;
}
.background {
stroke: $ui-vibrant-blue-25;
stroke: $ui-fleet-black-25;
}
@keyframes rotate {

View file

@ -1,70 +0,0 @@
import React, { useEffect } from "react";
import { InjectedRouter, Link } from "react-router";
import classnames from "classnames";
import paths from "router/paths";
import Icon from "components/Icon/Icon";
const baseClass = "stacked-white-boxes";
interface IStackedWhiteBoxesProps {
children?: JSX.Element;
headerText?: string;
className?: string;
leadText?: string;
previousLocation?: string;
router?: InjectedRouter;
}
const StackedWhiteBoxes = ({
children,
headerText,
className,
leadText,
previousLocation,
router,
}: IStackedWhiteBoxesProps): JSX.Element => {
const boxClass = classnames(baseClass, className);
useEffect(() => {
const closeWithEscapeKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && router) {
router.push(paths.LOGIN);
}
};
document.addEventListener("keydown", closeWithEscapeKey);
return () => {
document.removeEventListener("keydown", closeWithEscapeKey);
};
}, []);
const renderBackButton = () => {
if (!previousLocation) return false;
return (
<div className={`${baseClass}__back`}>
<Link to={previousLocation} className={`${baseClass}__back-link`}>
<Icon name="close" color="core-fleet-black" />
</Link>
</div>
);
};
return (
<div className={boxClass}>
<div className={`${baseClass}__box`}>
{renderBackButton()}
{headerText && (
<p className={`${baseClass}__header-text`}>{headerText}</p>
)}
{leadText && <p className={`${baseClass}__box-text`}>{leadText}</p>}
{children}
</div>
</div>
);
};
export default StackedWhiteBoxes;

View file

@ -1,46 +0,0 @@
.stacked-white-boxes {
transition: opacity 300ms ease-in;
width: 516px;
&__box {
background-color: $core-white;
border-radius: 10px;
box-sizing: border-box;
padding: $pad-xxlarge;
font-weight: $regular;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: $pad-medium;
p {
font-size: $x-small;
margin: 0;
}
}
&__header-text {
font-size: $x-small;
font-weight: $bold;
color: $core-fleet-black;
line-height: 32px;
margin-top: 0;
margin-bottom: 0;
}
&__back {
text-align: right;
width: 100%;
&-link {
position: absolute;
top: 36px;
right: 36px;
img {
width: 16px;
height: 16px;
}
}
}
}

View file

@ -1 +0,0 @@
export { default } from "./StackedWhiteBoxes";

View file

@ -91,3 +91,64 @@ export const Default: Story = {
);
},
};
export const Secondary: Story = {
render: () => {
const [selectedTabIndex, setSelectedTabIndex] = React.useState(0);
const platformSubNav = [
{ name: <TabText>Basic tab</TabText>, type: "type1" },
{ name: <TabText>Basic tab 2</TabText>, type: "type2" },
{
name: <TabText>Disabled tab</TabText>,
type: "type3",
disabled: true,
},
{ name: <TabText count={3}>Tab with count</TabText>, type: "type4" },
{
name: (
<TabText count={20} countVariant="alert">
Tab with error count
</TabText>
),
type: "type5",
},
];
const renderPanel = (type: string) => {
switch (type) {
case "type1":
return <div>Content for Tab 1</div>;
case "type2":
return <div>Content for Tab 2</div>;
case "type3":
return <div>Content for Tab 3</div>;
case "type4":
return <div>Content for Tab 4</div>;
case "type5":
return <div>Content for Tab 5</div>;
default:
return null;
}
};
return (
<TabNav secondary>
<Tabs onSelect={setSelectedTabIndex} selectedIndex={selectedTabIndex}>
<TabList>
{platformSubNav.map((navItem) => (
<Tab disabled={navItem.disabled}>
<TabText>{navItem.name}</TabText>
</Tab>
))}
</TabList>
{platformSubNav.map((navItem) => (
<TabPanel key={navItem.type}>
<div>{renderPanel(navItem.type)}</div>
</TabPanel>
))}
</Tabs>
</TabNav>
);
},
};

View file

@ -2,9 +2,9 @@ import React from "react";
import classnames from "classnames";
interface ITabNavProps {
children: React.ReactChild | React.ReactChild[];
children: React.ReactNode;
className?: string;
sticky?: boolean;
secondary?: boolean;
}
/*
@ -16,10 +16,10 @@ const baseClass = "tab-nav";
const TabNav = ({
children,
className,
sticky = false,
secondary = false,
}: ITabNavProps): JSX.Element => {
const classNames = classnames(baseClass, className, {
[`${baseClass}--sticky`]: sticky,
[`${baseClass}--secondary`]: secondary,
});
return <div className={classNames}>{children}</div>;

View file

@ -1,64 +1,48 @@
.tab-nav {
top: 0;
background-color: $core-white;
&--sticky {
position: sticky;
z-index: 2;
}
// No background color as TabNav is often over a background gradient
.react-tabs {
// Remove 12px top margin react-tab adds causing buggy 12px gap in all uses
// TODO: Find upstream fix
margin-top: -12px;
&__tab-list {
display: inline-flex;
align-items: flex-start;
gap: $pad-xxlarge;
border-bottom: 1px solid $ui-fleet-black-10;
gap: $pad-medium;
border-bottom: none;
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 */
}
}
height: 33px;
margin: 0; // Override react-tab 10px bottom margin
position: relative; // Required for shift left
}
&__tab {
padding: 5px 0 $pad-medium;
font-size: $x-small;
border-radius: $border-radius-large;
border: none;
padding: 6px $pad-small;
display: inline-flex;
flex-direction: column;
align-items: center;
line-height: 21px;
// Undoes blue native shadow
&:focus {
box-shadow: none;
outline: 0;
&:after {
left: 0;
bottom: 0;
}
}
&:hover {
background-color: $ui-fleet-black-5;
}
// focus-visible only highlights when tabbing not clicking
// outline instead of border prevents pixel shifts
&: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;
}
outline: 1px solid $core-focused-outline;
outline-offset: -1px;
}
// // Bolding text when the button is active causes a layout shift
@ -75,51 +59,17 @@
&--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;
}
background-color: $ui-fleet-black-5;
}
&--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 {
&__tab-panel--selected {
margin-top: $gap-page-component;
.no-results-message {
margin-top: $pad-xxlarge;
font-size: $small;
@ -133,5 +83,101 @@
}
}
}
.tab-text {
display: flex; /* Ensure text and count are aligned horizontally */
align-items: center; /* Vertically align items */
.tab-text__text {
// 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 */
}
}
}
}
// All secondary tab styling
&--secondary {
.react-tabs {
&__tab-list {
gap: $pad-xxlarge;
border-bottom: 1px solid $ui-fleet-black-10;
padding-bottom: 10px;
}
&__tab {
padding: 5px 0 $pad-medium;
&:hover {
background-color: transparent;
}
// Aligns underline correctly when focused
&:focus {
&:after {
left: 0;
bottom: 0;
}
}
&:focus-visible {
outline: none; // Undo primary tabbing through app styling as it runs into edges
.tab-text {
outline: 1px solid $core-focused-outline;
outline-offset: 3px;
border-radius: $border-radius;
}
}
&--selected {
background-color: transparent;
&::after {
content: "";
width: 100%;
height: 0;
border-bottom: 2px solid $core-fleet-black;
position: absolute;
bottom: 0;
left: 0;
}
}
&:hover {
&::after {
content: "";
width: 100%;
height: 0;
border-bottom: 2px solid $core-fleet-black;
position: absolute;
bottom: 0;
left: 0;
}
}
&--disabled {
&: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;
}
}
}
}
}

View file

@ -37,7 +37,7 @@ const TabText = ({
return (
<div className={classNames}>
<div className={`${baseClass}__text}`} data-text={children}>
<div className={`${baseClass}__text`} data-text={children}>
{children}
</div>
{renderCount()}

View file

@ -12,7 +12,7 @@
align-items: center;
background-color: $core-vibrant-blue;
border-radius: 29px;
color: $core-white;
color: $core-fleet-white;
font-weight: $bold;
font-size: $xx-small;

View file

@ -78,6 +78,7 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => {
onClick={() => onButtonClick(targetIds)}
variant={variant}
iconStroke={iconStroke}
size="small"
>
<>
{iconPosition === "left" && iconSvg && <Icon name={iconSvg} />}

View file

@ -543,13 +543,18 @@ const DataTable = ({
{shouldRenderToggleAllPages && (
<Button
onClick={onToggleAllPagesClick}
variant="text-link"
variant="inverse"
className="light-text"
size="small"
>
<>Select all matching {resultsTitle}</>
</Button>
)}
<Button onClick={onClearSelectionClick} variant="text-link">
<Button
onClick={onClearSelectionClick}
variant="inverse"
size="small"
>
Clear selection
</Button>
</div>
@ -577,11 +582,24 @@ const DataTable = ({
return (
<th
className={column.id ? `${column.id}__header` : ""}
{...column.getHeaderProps(
column.getSortByToggleProps({ title: null })
)}
{...column.getHeaderProps()}
>
{renderColumnHeader(column)}
{column.canSort ? (
<Button
variant="unstyled"
{...column.getSortByToggleProps({ title: null })}
aria-label={`Sort by ${column.Header} ${
column.isSortedDesc ? "descending" : "ascending"
}`}
tabIndex={0}
className="sortable-header"
>
{renderColumnHeader(column)}
{/* add arrow/icon as needed */}
</Button>
) : (
renderColumnHeader(column)
)}
</th>
);
})}

View file

@ -1,5 +1,5 @@
import React from "react";
import { FilterProps, TableInstance } from "react-table";
import { FilterProps } from "react-table";
import SearchField from "components/forms/fields/SearchField";

View file

@ -1,10 +1,8 @@
.filter-cell {
.input-icon-field__input-wrapper {
margin-top: $pad-xsmall;
}
max-width: 70px;
input {
height: 40px;
height: 36px;
width: 100%;
font-size: $x-small;
background-color: transparent;

View file

@ -28,13 +28,13 @@
&.ascending {
.ascending-arrow {
border-bottom-color: $core-vibrant-blue;
border-bottom-color: $core-fleet-black;
}
}
&.descending {
.descending-arrow {
border-top-color: $core-vibrant-blue;
border-top-color: $core-fleet-black;
}
}
}

View file

@ -35,7 +35,10 @@ const LinkCell = ({
prefix,
suffix,
}: ILinkCellProps): JSX.Element => {
const cellClasses = classnames(baseClass, className);
const cellClasses = classnames(baseClass, className, {
[`${baseClass}--tooltip`]: !!tooltipContent,
[`${baseClass}--tooltip-truncate`]: tooltipTruncate,
});
const onClick = (e: React.MouseEvent): void => {
customOnClick && customOnClick(e);

View file

@ -22,12 +22,12 @@
border-radius: 0;
}
&--minimal {
background-color: $ui-vibrant-blue-10;
background-color: $ui-fleet-black-10;
}
&--considerable {
background-color: $ui-vibrant-blue-25;
background-color: $ui-fleet-black-25;
}
&--excessive {
background-color: $ui-vibrant-blue-50;
background-color: $ui-fleet-black-50;
}
}

View file

@ -11,7 +11,6 @@ $shadow-transition-width: 10px;
position: relative;
border: 1px solid $ui-fleet-black-10;
border-radius: 6px;
margin-top: $pad-small;
flex-grow: 1;
width: 100%;
@ -47,7 +46,7 @@ $shadow-transition-width: 10px;
position: relative;
width: 100%;
border-collapse: collapse;
color: $core-fleet-black;
color: $ui-fleet-black-75;
font-size: $x-small;
}
@ -99,10 +98,11 @@ $shadow-transition-width: 10px;
}
th {
padding: $pad-medium $pad-large;
padding: 0 $pad-large;
white-space: nowrap;
border-left: 1px solid $ui-fleet-black-10;
font-weight: $bold;
height: 40px; // Match body row height
&:first-child {
border-top-left-radius: 6px;
@ -111,7 +111,7 @@ $shadow-transition-width: 10px;
&.selection__header {
width: 22px;
padding: $pad-medium;
padding: 0 $pad-medium;
}
&:last-child {
@ -119,7 +119,7 @@ $shadow-transition-width: 10px;
}
&.actions__header,
&.id__header, // Same as actions__header on some pages
&.id__header // Same as actions__header on some pages
{
border-left: none;
width: 99px;
@ -133,12 +133,37 @@ $shadow-transition-width: 10px;
}
.column-header {
// Filters next to header name
display: flex;
min-width: max-content;
gap: $pad-small;
span {
display: flex;
align-items: center;
gap: 3px;
}
}
.header-cell {
font-weight: $bold;
}
// Sort arrows change color on hover
.sortable-header {
&:hover {
.header-cell:not(.ascending) {
.ascending-arrow {
border-bottom-color: $ui-fleet-black-50;
}
}
.header-cell.ascending {
.descending-arrow {
border-top-color: $ui-fleet-black-50;
}
}
}
}
}
&.active-selection {
@ -168,12 +193,11 @@ $shadow-transition-width: 10px;
border-radius: 6px;
&__checkbox {
padding: 16px;
width: 20px;
width: 16px;
}
&__container {
padding: 0 24px;
padding: 0 20px;
}
&__inner {
@ -218,8 +242,9 @@ $shadow-transition-width: 10px;
background-color: $ui-off-white-opaque; // opaque needed for horizontal scroll shadow
}
&:focus-visible {
outline: 2px solid $ui-vibrant-blue-25;
outline: 2px solid $core-focused-outline;
background: $ui-off-white;
border-radius: $border-radius;
}
}
@ -257,6 +282,8 @@ $shadow-transition-width: 10px;
{
text-align: right;
max-width: 140px;
display: flex;
align-items: center;
}
&.selection__cell {
@ -266,7 +293,6 @@ $shadow-transition-width: 10px;
.link-cell,
.text-cell {
display: block; // inline-block is not vertically centered
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
@ -274,16 +300,48 @@ $shadow-transition-width: 10px;
white-space: normal;
}
}
// Caution: LinkCell depends on this to have animation only under text and not icons etc
.link-cell {
padding: $pad-small 0; // larger clickable area
display: inline-block; // Underline only words
overflow: visible;
@include link;
&:not(.link-cell--tooltip-truncate) {
@include animated-bottom-border;
}
&.link-cell--tooltip-truncate {
.data-table__tooltip-truncated-text-container {
position: relative;
@include bottom-border;
}
&:hover,
&:focus {
margin-bottom: 0; // Undo margin-bottom of animated bottom border
.data-table__tooltip-truncated-text::after,
.data-table__tooltip-truncated-text::after {
transform: scaleX(1);
}
}
}
&:hover {
// Underlines only the text and not the suffix like badges
.tooltip-truncated-cell {
text-decoration: none;
}
.data-table__tooltip-truncated-text {
text-decoration: underline;
.data-table__tooltip-truncated-text-container {
// @include link;
@include animated-bottom-border;
&:hover {
margin-bottom: 0; // Undo margin-bottom of animated bottom border
}
&::after {
transform: scaleX(
1
); // This gets the animation on the text only when hovering over the whole link-cell
}
}
}
@ -291,7 +349,6 @@ $shadow-transition-width: 10px;
display: flex;
align-items: center;
gap: $pad-small;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View file

@ -129,7 +129,7 @@ interface ITableContainerProps<T = any> {
const baseClass = "table-container";
const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_PAGE_SIZE = 20;
const DEFAULT_PAGE_INDEX = 0;
const TableContainer = <T,>({
@ -331,7 +331,9 @@ const TableContainer = <T,>({
>
<>
{actionButton.buttonText}
{actionButton.iconSvg && <Icon name={actionButton.iconSvg} />}
{actionButton.iconSvg && (
<Icon name={actionButton.iconSvg} color="ui-fleet-black-75" />
)}
</>
</Button>
);

View file

@ -1,4 +1,8 @@
.table-container {
display: flex;
flex-direction: column;
gap: $gap-table-elements;
// Container is responsive design used when customFilters is rendered
.container {
display: grid;
@ -6,7 +10,7 @@
grid-template-rows: auto auto; /* Two rows for smaller screens*/
width: 100%;
height: max-content;
gap: $pad-small $pad-medium;
gap: $gap-table-elements;
}
.stackable-header {
@ -14,7 +18,7 @@
align-content: center;
display: flex;
flex-direction: row;
gap: $pad-medium;
gap: $gap-table-elements;
> div {
display: flex;
@ -73,12 +77,7 @@
width: 100%;
justify-content: space-between;
align-items: center;
gap: $pad-small;
margin-top: 3px; // Fits button highlight during tabbing
.input-icon-field {
height: 40px; // Height 40px on table headers
}
gap: $gap-table-elements;
&.stack-table-controls {
align-items: start;
@ -109,7 +108,7 @@
// filter and search bar height
.dropdown__select,
.input-with-icon {
height: 40px;
height: 36px;
}
}
@ -137,11 +136,11 @@
font-weight: $bold;
color: $core-fleet-black;
margin: 0;
height: 40px;
gap: 12px;
height: 36px;
gap: $gap-table-elements;
> span {
line-height: 40px; // Match other header components' height but still align text baseline
line-height: 36px; // Match other header components' height but still align text baseline
min-width: fit-content;
}
@ -224,7 +223,7 @@
.children-wrapper {
overflow: hidden;
white-space: nowrap;
display: block;
display: flex;
text-overflow: ellipsis;
}
@ -236,16 +235,16 @@
width: 120px;
}
// This hides View all host link unless the row is hovered
// This hides View all host button unless the row is hovered
tr {
.row-hover-link {
.row-hover-button {
opacity: 0;
transition: 250ms;
text-overflow: none;
}
&:hover,
&:focus-visible {
.row-hover-link {
.row-hover-button {
opacity: 1;
}
}

View file

@ -1,5 +1,4 @@
import React, { ReactNode } from "react";
import { Link } from "react-router";
import classnames from "classnames";
import PATHS from "router/paths";
@ -12,6 +11,7 @@ import Radio from "components/forms/fields/Radio";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import Checkbox from "components/forms/fields/Checkbox";
import CustomLink from "components/CustomLink";
const baseClass = "target-label-selector";
@ -121,8 +121,8 @@ const LabelChooser = ({
if (!labels.length) {
return (
<div className={`${baseClass}__no-labels`}>
<Link to={PATHS.LABEL_NEW_DYNAMIC}>Add label</Link> to target specific
hosts.
<CustomLink url={PATHS.LABEL_NEW_DYNAMIC} text="Add label" /> to target
specific hosts.
</div>
);
}

View file

@ -7,7 +7,7 @@
top: 70px;
left: 0;
z-index: 1;
background-color: $core-white;
background-color: $core-fleet-white;
border-radius: 0px 0px 8px 8px;
.table-container {
@ -66,7 +66,7 @@
}
// hack because it's creating unwanted space
.table-container {
background-color: $core-white;
background-color: $core-fleet-white;
&__header {
display: none;

View file

@ -59,7 +59,7 @@ const getOptionBackgroundColor = (
GroupBase<INumberDropdownOption>
>
) => {
return state.isFocused ? COLORS["ui-vibrant-blue-10"] : "transparent";
return state.isFocused ? COLORS["ui-fleet-black-5"] : "transparent";
};
interface ITeamsDropdownProps {
@ -114,8 +114,8 @@ const TeamsDropdown = ({
const { isFocused, selectProps } = props;
const color =
isFocused || selectProps.menuIsOpen
? "core-fleet-blue"
: "core-fleet-black";
? "core-fleet-black"
: "ui-fleet-black-75";
return (
<components.DropdownIndicator {...props} className={baseClass}>
@ -160,17 +160,17 @@ const TeamsDropdown = ({
"&:hover": {
boxShadow: "none",
".team-dropdown__single-value": {
color: COLORS["core-fleet-blue"],
color: COLORS["core-fleet-black"],
},
".team-dropdown__indicator path": {
stroke: COLORS["core-vibrant-blue-over"],
stroke: COLORS["ui-fleet-black-75-over"],
},
},
// When tabbing
// Relies on --is-focused for styling as &:focus-visible cannot be applied
"&.team-dropdown__control--is-focused": {
".team-dropdown__indicator path": {
stroke: COLORS["core-vibrant-blue-over"],
stroke: COLORS["ui-fleet-black-75-over"],
},
},
...(state.isDisabled && {
@ -184,10 +184,10 @@ const TeamsDropdown = ({
// When clicking
"&:active": {
".team-dropdown__single-value": {
color: COLORS["core-vibrant-blue-down"],
color: COLORS["ui-fleet-black-75-down"],
},
".team-dropdown__indicator path": {
stroke: COLORS["core-vibrant-blue-down"],
stroke: COLORS["ui-fleet-black-75-down"],
},
},
...(state.menuIsOpen && {
@ -255,20 +255,20 @@ const TeamsDropdown = ({
option: (baseStyles, state) => ({
...baseStyles,
padding: "10px 8px",
fontSize: "14px",
fontSize: "13px",
borderRadius: "4px",
backgroundColor: getOptionBackgroundColor(state),
fontWeight: state.isSelected ? "bold" : "normal",
fontWeight: state.isSelected ? "600" : "normal",
color: COLORS["core-fleet-black"],
"&:hover": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-10"],
: COLORS["ui-fleet-black-5"],
},
"&:active": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-25"],
: COLORS["ui-fleet-black-5"],
},
...(state.isDisabled && {
color: COLORS["ui-fleet-black-50"],

View file

@ -19,6 +19,7 @@ export interface ITooltipWrapper {
and mouseout from the element. If a boolean, sets delay to the default below. If a number, sets to that
* many milliseconds. Overrides `delayShow` and `delayHide` */
delayShowHide?: boolean | number;
delayInMs?: number;
underline?: boolean;
// Below two props used here to maintain the API of the old TooltipWrapper
// A clearer system would be to use the 3 below commented props, which describe exactly where they
@ -64,6 +65,7 @@ const TooltipWrapper = ({
delayShow = true,
delayHide,
delayShowHide,
delayInMs, // TODO: Apply pattern of delay tooltip for repeated table tooltips
underline = true,
className,
tooltipClass,
@ -117,7 +119,7 @@ const TooltipWrapper = ({
<ReactTooltip5
className={tipClassNames}
id={tipId}
delayShow={delayShowVal}
delayShow={delayShowVal || delayInMs}
delayHide={delayHideVal}
noArrow={!showArrow}
place={position}

View file

@ -1,10 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import ViewAllHostsLink from "./ViewAllHostsLink";
import ViewAllHostsButton from "./ViewAllHostsButton";
describe("ViewAllHostsLink - component", () => {
describe("ViewAllHostsButton - component", () => {
it("renders View all hosts text and icon", () => {
render(<ViewAllHostsLink />);
render(<ViewAllHostsButton />);
const text = screen.getByText("View all hosts");
const icon = screen.getByTestId("chevron-right-icon");
@ -14,7 +14,7 @@ describe("ViewAllHostsLink - component", () => {
});
it("hides text when set to condensed ", async () => {
render(<ViewAllHostsLink queryParams={{ status: "online" }} condensed />);
render(<ViewAllHostsButton queryParams={{ status: "online" }} condensed />);
const text = screen.queryByText("View all hosts");
expect(text).toBeNull();

View file

@ -1,8 +1,9 @@
import React from "react";
import PATHS from "router/paths";
import { Link } from "react-router";
import { browserHistory } from "react-router";
import classnames from "classnames";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import { getPathWithQueryParams, QueryParams } from "utilities/url";
@ -18,13 +19,13 @@ interface IHostLinkProps {
customContent?: React.ReactNode;
/** Table links shows on row hover and tab focus only */
rowHover?: boolean;
/** Don't actually create a link, useful when click is handled by an ancestor */
/** Don't actually create a button, useful when click is handled by an ancestor */
noLink?: boolean;
}
const baseClass = "view-all-hosts-link";
const baseClass = "view-all-hosts-button";
const ViewAllHostsLink = ({
const ViewAllHostsButton = ({
queryParams,
className,
platformLabelId,
@ -35,9 +36,9 @@ const ViewAllHostsLink = ({
rowHover = false,
noLink = false,
}: IHostLinkProps): JSX.Element => {
const viewAllHostsLinkClass = classnames(baseClass, className, {
const viewAllHostsButtonClass = classnames(baseClass, className, {
[`${baseClass}__condensed`]: condensed,
"row-hover-link": rowHover,
"row-hover-button": rowHover,
});
const endpoint = platformLabelId
@ -46,20 +47,21 @@ const ViewAllHostsLink = ({
const path = getPathWithQueryParams(endpoint, queryParams);
const onClick = (e: MouseEvent): void => {
if (!noLink) {
e.stopPropagation(); // Allows for button to have different onClick behavior than the row's onClick behavior
}
if (path) {
browserHistory.push(path);
}
};
return (
<Link
className={viewAllHostsLinkClass}
to={noLink ? "" : path}
onClick={(e) => {
if (!noLink) {
e.stopPropagation(); // Allows for link to have different onClick behavior than the row's onClick behavior
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.stopPropagation(); // Allows for link to be keyboard accessible in a clickable row
}
}}
<Button
className={viewAllHostsButtonClass}
onClick={onClick}
variant="inverse"
size="small"
>
{!condensed && (
<span
@ -72,10 +74,11 @@ const ViewAllHostsLink = ({
<Icon
name="chevron-right"
className={`${baseClass}__icon`}
color="core-fleet-blue"
color="ui-fleet-black-75"
/>
)}
</Link>
</Button>
);
};
export default ViewAllHostsLink;
export default ViewAllHostsButton;

View file

@ -1,5 +1,4 @@
.view-all-hosts-link {
@include table-link;
.view-all-hosts-button {
&__text {
&--responsive {
@media (max-width: $break-md) {
@ -13,7 +12,7 @@
}
// For tabbing through the app
&:focus-visible.row-hover-link {
&:focus-visible.row-hover-button {
opacity: 1;
}
}

View file

@ -1 +1 @@
export { default } from "./ViewAllHostsLink";
export { default } from "./ViewAllHostsButton";

View file

@ -83,7 +83,7 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
<GitOpsModeTooltipWrapper
renderChildren={(disableChildren) => (
<Button
variant="text-icon"
variant="inverse"
onClick={action.onClick}
disabled={disableChildren}
>
@ -97,7 +97,7 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
);
}
return (
<Button variant="text-icon" onClick={action.onClick}>
<Button variant="inverse" onClick={action.onClick}>
<>
{action.label}
{action.iconName && <Icon name={action.iconName} />}
@ -112,7 +112,7 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
<DropdownButton
showCaret={false}
options={secondaryActions}
variant="text-icon"
variant="inverse"
>
More options <Icon name="more" />
</DropdownButton>

View file

@ -30,7 +30,7 @@
&:hover,
&:focus {
background-color: $ui-vibrant-blue-10;
background-color: $ui-fleet-black-10;
color: $core-fleet-black;
}
}

View file

@ -63,7 +63,6 @@ const createLoadingVariant = (variant: ButtonVariant): Story => ({
// Variants with loading state
export const DefaultVariant = createLoadingVariant("default");
export const SuccessVariant = createLoadingVariant("success");
export const AlertVariant = createLoadingVariant("alert");
export const InverseVariant = Template("inverse");
export const InverseAlertVariant = Template("inverse-alert");
@ -77,6 +76,13 @@ export const TextIconVariant = Template(
Button text <Icon name="plus" size="small" />
</>
);
export const BrandInverseIconVariant = Template(
"brand-inverse-icon",
<>
<Icon name="plus" size="small" />
Button text
</>
);
export const IconVariant = Template("text-icon", <Icon name="trash" />);
export const UnstyledVariant = Template("unstyled");

View file

@ -6,11 +6,11 @@ const baseClass = "button";
export type ButtonVariant =
| "default"
| "success"
| "alert"
| "pill"
| "text-link" // Underlines on hover
| "text-link-dark" // underline on hover, dark text
| "brand-inverse-icon" // Green icon with text, no underline on hover
| "text-icon"
| "icon" // Buttons without text
| "inverse"
@ -41,6 +41,18 @@ export interface IButtonProps {
customOnKeyDown?: (e: React.KeyboardEvent) => void;
/** Required for buttons that contain SVG icons using`stroke` instead of`fill` for proper hover styling */
iconStroke?: boolean;
ariaHasPopup?:
| boolean
| "false"
| "true"
| "menu"
| "listbox"
| "tree"
| "grid"
| "dialog";
ariaExpanded?: boolean;
/** Small: 1/2 the padding */
size?: "small" | "default";
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -112,12 +124,16 @@ class Button extends React.Component<IButtonProps, IButtonState> {
isLoading,
customOnKeyDown,
iconStroke,
ariaHasPopup,
ariaExpanded,
size,
} = this.props;
const fullClassName = classnames(
baseClass,
`${baseClass}--${variant}`,
className,
{
[`${baseClass}--${variant}__small`]: size === "small",
[`${baseClass}--disabled`]: disabled,
[`${baseClass}--icon-stroke`]: iconStroke,
}
@ -125,6 +141,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
const onWhite =
variant === "text-link" ||
variant === "inverse" ||
variant === "brand-inverse-icon" ||
variant === "text-icon" ||
variant === "pill";
@ -138,6 +155,8 @@ class Button extends React.Component<IButtonProps, IButtonState> {
type={type}
title={title}
ref={setRef}
aria-haspopup={ariaHasPopup}
aria-expanded={ariaExpanded}
>
<div className={isLoading ? "transparent-text" : "children-wrapper"}>
{children}

View file

@ -4,20 +4,42 @@ $base-class: "button";
outline-color: $core-focused-outline;
outline-offset: $offset;
outline-style: solid;
outline-width: 2px;
outline-width: 1px;
}
// Buttons with background colors
@mixin button-pad-8px-16px {
padding: $pad-small $pad-medium;
height: 36px;
}
// Buttons without background colors
@mixin button-pad-8px-8px {
padding: $pad-small $pad-small;
height: 36px;
}
// Size small buttons (with and without background colors)
@mixin button-pad-4px-8px {
padding: $pad-xsmall $pad-small;
height: 24px;
}
@mixin button-variant($color, $hover: null, $active: null, $inverse: false) {
background-color: $color;
@if $inverse {
padding: $pad-small;
@include button-pad-8px-8px;
&__small {
@include button-pad-4px-8px;
}
&:hover {
background-color: rgba($core-fleet-black, 0.05);
&:active {
background-color: $ui-fleet-black-10;
background-color: $ui-fleet-black-5;
}
}
@ -32,7 +54,7 @@ $base-class: "button";
width: 100%;
height: 100%;
position: absolute;
border: 1px solid $core-vibrant-blue;
border: 1px solid $core-fleet-black;
border-radius: 6px;
}
}
@ -52,22 +74,21 @@ $base-class: "button";
}
.#{$base-class} {
@include button-variant($core-vibrant-blue);
@include button-variant($core-fleet-green);
@include button-pad-8px-16px;
transition: color 150ms ease-in-out, background 150ms ease-in-out,
top 50ms ease-in-out, box-shadow 50ms ease-in-out, border 50ms ease-in-out;
position: relative;
color: $core-white;
color: $core-fleet-white;
text-decoration: none;
flex-direction: row;
justify-content: center;
align-items: center;
padding: $pad-small $pad-medium;
border-radius: 6px;
font-size: $x-small;
font-family: "Inter", sans-serif;
font-weight: $bold;
display: inline-flex;
height: 38px;
top: 0;
border: 0;
position: relative;
@ -89,9 +110,9 @@ $base-class: "button";
&--default {
@include button-variant(
$core-vibrant-blue,
$core-vibrant-blue-over,
$core-vibrant-blue-down
$core-fleet-green,
$core-fleet-green-over,
$core-fleet-green-down
);
display: flex;
text-wrap: nowrap;
@ -127,8 +148,8 @@ $base-class: "button";
null,
$inverse: true
);
color: $core-vibrant-blue;
border: 1px solid $core-vibrant-blue;
color: $core-fleet-green;
border: 1px solid $core-fleet-green;
box-sizing: border-box;
font-size: $xx-small;
padding: $pad-xsmall 10px;
@ -141,7 +162,7 @@ $base-class: "button";
&:hover,
&:focus {
border: 1px solid $core-vibrant-blue;
border: 1px solid $core-fleet-green;
}
}
@ -149,7 +170,7 @@ $base-class: "button";
@include button-variant(transparent);
border: 0;
box-shadow: none;
color: $core-vibrant-blue;
color: $ui-fleet-black-75;
font-size: $x-small;
cursor: pointer;
margin: 0;
@ -164,14 +185,14 @@ $base-class: "button";
&:hover,
&:focus {
color: $core-vibrant-blue-over;
color: $ui-fleet-black-75-over;
background-color: transparent;
box-shadow: none;
text-decoration: underline;
}
&:active {
color: $core-vibrant-blue-down;
color: $ui-fleet-black-75-down;
box-shadow: none;
top: 0;
}
@ -214,11 +235,17 @@ $base-class: "button";
// &--icon is used for svg icon buttons without text
&--text-icon,
&--icon {
@include button-variant(transparent);
@include button-variant(
$core-fleet-white,
$core-fleet-green-over,
$core-fleet-green-down,
$inverse: true
);
background-color: transparent;
padding: 0;
border: 0;
box-shadow: none;
color: $core-vibrant-blue;
color: $ui-fleet-black-75;
font-size: $x-small;
font-weight: $bold;
cursor: pointer;
@ -240,27 +267,27 @@ $base-class: "button";
width: 100%;
height: 100%;
position: absolute;
border: 1px solid $core-vibrant-blue;
border: 1px solid $ui-fleet-black-75;
border-radius: 6px;
}
}
&:hover,
&:focus {
color: $core-vibrant-blue-over;
color: $ui-fleet-black-75-over;
svg {
path {
fill: $core-vibrant-blue-over;
fill: $ui-fleet-black-75-over;
}
}
&:active {
color: $core-vibrant-blue-down;
color: $ui-fleet-black-75-down;
svg {
path {
fill: $core-vibrant-blue-down;
fill: $ui-fleet-black-75-down;
}
}
}
@ -275,13 +302,13 @@ $base-class: "button";
svg {
path {
fill: none; // Prevent fill from interfering
stroke: $core-vibrant-blue-over;
stroke: $ui-fleet-black-75-over;
}
}
&:active {
svg {
path {
stroke: $core-vibrant-blue-down;
stroke: $ui-fleet-black-75-down;
}
}
}
@ -295,21 +322,112 @@ $base-class: "button";
}
}
&--icon {
height: initial; // Override 38px height
svg {
padding: $pad-small;
// Used for primary buttons with green icon and green text, no underline on hover
&--brand-inverse-icon {
@include button-variant(transparent);
@include button-pad-8px-8px;
border: 0;
box-shadow: none;
color: $core-fleet-green;
font-size: $x-small;
font-weight: $bold;
cursor: pointer;
white-space: nowrap;
&__small {
@include button-pad-4px-8px;
}
img {
transform: scale(0.5);
}
&:focus {
outline: none;
}
&:focus-visible {
@include button-focus-outline();
&::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
border: 1px solid $ui-fleet-black-75;
border-radius: 6px;
}
}
&:hover,
&:focus {
background-color: rgba($core-fleet-black, 0.05);
color: $core-fleet-green-over;
svg {
path {
fill: $core-fleet-green-over;
}
}
&:active {
color: $core-fleet-green-down;
svg {
path {
fill: $core-fleet-green-down;
}
}
}
}
// If .button--icon-stroke is present, use stroke instead of fill
// Some SVG icons in these buttons contain a `stroke` instead of a `fill`,
// so we need to modify that property instead. Adding a custom `fill`
// could make these icons render incorrectly.
&.button--icon-stroke {
&:hover,
&:focus {
svg {
path {
fill: none; // Prevent fill from interfering
stroke: $core-fleet-green-over;
}
}
&:active {
svg {
path {
stroke: $core-fleet-green-over;
}
}
}
}
}
// globally styled gap between text and icon
.children-wrapper {
gap: $pad-small;
}
}
&--icon {
@include button-variant(
$core-fleet-white,
$core-fleet-green-over,
$core-fleet-green-down,
$inverse: true
);
background-color: transparent;
}
&--inverse {
@include button-variant(
$core-white,
$core-vibrant-blue-over,
$core-vibrant-blue-down,
$core-fleet-white,
$core-fleet-green-over,
$core-fleet-green-down,
$inverse: true
);
color: $core-vibrant-blue;
background-color: transparent;
color: $ui-fleet-black-75;
box-sizing: border-box;
.children-wrapper {
@ -319,7 +437,7 @@ $base-class: "button";
&--inverse-alert {
@include button-variant(
$core-white,
$core-fleet-white,
$core-vibrant-red-over,
$core-vibrant-red-down,
$inverse: true
@ -376,7 +494,7 @@ $base-class: "button";
&:hover,
&:focus {
background-color: $ui-vibrant-blue-10;
background-color: $ui-fleet-black-5;
box-shadow: none;
}
@ -408,4 +526,16 @@ $base-class: "button";
font-size: $medium;
width: 100%;
}
// Designed to offset padding for text to justify left of parent component
&.button--justify-left {
left: -$pad-small;
margin-right: -$pad-small;
}
// Designed to offset padding for text to justify right of parent component
&.button--justify-right {
right: -$pad-small;
margin-left: -$pad-small;
}
}

View file

@ -1,5 +1,4 @@
.dropdown-button {
padding: 8px 0;
&__wrapper {
display: flex;
position: relative;
@ -21,7 +20,7 @@
display: none;
z-index: 99;
border-radius: 2px;
background-color: $core-white;
background-color: $core-fleet-white;
box-shadow: 0 4px 10px rgba(52, 59, 96, 0.15);
animation: fade-in 150ms ease-out;
&--opened {
@ -33,7 +32,7 @@
display: block;
.button {
color: $core-fleet-blue;
color: $core-fleet-black;
text-transform: none;
text-align: left;
font-weight: $regular;
@ -43,13 +42,13 @@
white-space: nowrap;
width: 100%;
font-size: 15px;
height: 38px;
height: 32px;
letter-spacing: -0.5px;
&:hover,
&:focus {
background-color: $ui-vibrant-blue-10;
color: $core-fleet-blue;
background-color: $ui-fleet-black-10;
color: $core-fleet-black;
}
}
}

View file

@ -50,14 +50,14 @@ const RevealButton = ({
{caretPosition === "before" && (
<Icon
name={isShowing ? "chevron-down" : "chevron-right"}
color="core-fleet-blue"
color="ui-fleet-black-75"
/>
)}
{buttonText}
{caretPosition === "after" && (
<Icon
name={isShowing ? "chevron-up" : "chevron-down"}
color="core-fleet-blue"
color="ui-fleet-black-75"
/>
)}
</>
@ -66,7 +66,7 @@ const RevealButton = ({
const button = (
<Button
variant="text-icon"
variant="inverse"
className={classNames}
onClick={onClick}
autofocus={autofocus}

View file

@ -2,6 +2,7 @@
display: inline-flex;
align-items: center;
padding: $pad-small $pad-xxsmall; // larger clickable area
min-width: max-content;
svg {
path {
@ -12,12 +13,12 @@
&:hover,
&:focus {
.component__tooltip-wrapper__element {
color: $core-vibrant-blue-over;
color: $ui-fleet-black-75-over;
}
svg {
path {
stroke: $core-vibrant-blue-over;
stroke: $ui-fleet-black-75-over;
}
}
}

View file

@ -134,13 +134,15 @@ const ConfirmInviteForm = ({
error={formErrors.password_confirmation}
parseTarget
/>
<Button
type="submit"
disabled={Object.keys(formErrors).length > 0}
className="confirm-invite-button"
>
Submit
</Button>
<div className="button-wrap">
<Button
type="submit"
disabled={Object.keys(formErrors).length > 0}
className="confirm-invite-button"
>
Submit
</Button>
</div>
</form>
);
};

Some files were not shown because too many files have changed in this diff Show more