mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fleet UI [Feature]: UI reskin (#33558)
This commit is contained in:
parent
c6474eca82
commit
efc64389b1
469 changed files with 4765 additions and 4108 deletions
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<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,
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
.app {
|
||||
background: $gradients-dark-gradient-vertical;
|
||||
@include gradient-background;
|
||||
margin: 0;
|
||||
|
||||
& > div {
|
||||
min-height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
53
frontend/components/AuthenticationNav/AuthenticationNav.tsx
Normal file
53
frontend/components/AuthenticationNav/AuthenticationNav.tsx
Normal 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;
|
||||
52
frontend/components/AuthenticationNav/_styles.scss
Normal file
52
frontend/components/AuthenticationNav/_styles.scss
Normal 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
1
frontend/components/AuthenticationNav/index.ts
Normal file
1
frontend/components/AuthenticationNav/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AuthenticationNav";
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
|
||||
&.has-white-background {
|
||||
background: $core-white;
|
||||
background: $core-fleet-white;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
17
frontend/components/BackButton/BackButton.stories.tsx
Normal file
17
frontend/components/BackButton/BackButton.stories.tsx
Normal 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 = {};
|
||||
|
|
@ -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");
|
||||
37
frontend/components/BackButton/BackButton.tsx
Normal file
37
frontend/components/BackButton/BackButton.tsx
Normal 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;
|
||||
1
frontend/components/BackButton/index.ts
Normal file
1
frontend/components/BackButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./BackButton";
|
||||
|
|
@ -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 = {};
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.back-link {
|
||||
@include direction-link;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./BackLink";
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
|
||||
// color styles
|
||||
&__white {
|
||||
background-color: $core-white;
|
||||
background-color: $core-fleet-white;
|
||||
}
|
||||
|
||||
&__grey {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
.card-header {
|
||||
margin: 0 0 $pad-large;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const CustomLink = ({
|
|||
case "banner-link":
|
||||
return "core-fleet-black";
|
||||
default:
|
||||
return "core-fleet-blue";
|
||||
return "ui-fleet-black-75";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
dt {
|
||||
font-weight: $bold;
|
||||
display: flex;
|
||||
color: $core-fleet-black;
|
||||
line-height: $line-height;
|
||||
}
|
||||
|
||||
dd {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@
|
|||
|
||||
&__edit-delete-btns {
|
||||
display: flex;
|
||||
gap: $pad-xxsmall;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,6 @@
|
|||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $pad-medium;
|
||||
gap: $pad-xsmall; // Padding on buttons
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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} > Actions > Edit</b>. Currently, hosts
|
||||
that aren'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} > Actions > Edit</b>. Currently, hosts
|
||||
that aren'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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
24
frontend/components/SidePanelPage/SidePanelPage.tsx
Normal file
24
frontend/components/SidePanelPage/SidePanelPage.tsx
Normal 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;
|
||||
64
frontend/components/SidePanelPage/_styles.scss
Normal file
64
frontend/components/SidePanelPage/_styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/components/SidePanelPage/index.ts
Normal file
1
frontend/components/SidePanelPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SidePanelPage";
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./StackedWhiteBoxes";
|
||||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from "./ViewAllHostsLink";
|
||||
export { default } from "./ViewAllHostsButton";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $ui-vibrant-blue-10;
|
||||
background-color: $ui-fleet-black-10;
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue