Fleet UI: Improve internal links/buttons (#43470)

This commit is contained in:
RachelElysia 2026-04-14 09:30:26 -04:00 committed by GitHub
parent 8209dbebee
commit ecf2bad9a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 74 additions and 144 deletions

View file

@ -0,0 +1 @@
- Fleet UI: Improved button and link styling

View file

@ -3,7 +3,7 @@ import { useQuery } from "react-query";
import configAPI from "services/entities/config";
import { AppContext } from "context/app";
import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button";
import DataError from "components/DataError";
import Modal from "components/Modal";
import Spinner from "components/Spinner";
@ -60,10 +60,9 @@ const AddHostsModal = ({
<span className="info__data">
You have no enroll secrets.{" "}
{openEnrollSecretModal ? (
<CustomLink
customClickHandler={onManageEnrollSecretsClick}
text="Manage enroll secrets"
/>
<Button variant="link" onClick={onManageEnrollSecretsClick}>
Manage enroll secrets
</Button>
) : (
"Manage enroll secrets"
)}{" "}

View file

@ -369,12 +369,9 @@ const PlatformWrapper = ({
<div>
<InfoBanner className={`${baseClass}__chrome--instructions`}>
This works for macOS, Windows, and Linux hosts. To add
Chromebooks,&nbsp;
<Button
variant="text-link-dark"
onClick={() => setSelectedTabIndex(3)}
>
click here
Chromebooks,{" "}
<Button variant="link" onClick={() => setSelectedTabIndex(3)}>
visit the ChromeOS tab
</Button>
.
</InfoBanner>

View file

@ -22,7 +22,6 @@ interface ICustomLinkProps {
* @default "default"
*/
variant?: "tooltip-link" | "banner-link" | "flash-message-link" | "default";
customClickHandler?: (e: React.MouseEvent) => void;
}
const baseClass = "custom-link";
@ -35,7 +34,6 @@ const CustomLink = ({
multiline = false,
disableKeyboardNavigation = false,
variant = "default",
customClickHandler,
}: ICustomLinkProps): JSX.Element => {
const getIconColor = (): Colors => {
switch (variant) {
@ -58,27 +56,6 @@ const CustomLink = ({
// e.g. cell/row handlers with a tooltip that has a custom link inside
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
// prevent navigation when were handling it ourselves
// e.g. designed underline links opening modals
if (customClickHandler) {
e.preventDefault();
customClickHandler(e);
}
};
// Handle "Enter" key presses for accessibility when a custom click handler is provided
const handleKeyDown = (e: React.KeyboardEvent<HTMLAnchorElement>) => {
if (!customClickHandler) {
return;
}
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
// Reuse the same logic as click
customClickHandler((e as unknown) as React.MouseEvent<HTMLAnchorElement>);
}
};
const target = newTab ? "_blank" : "";
@ -121,7 +98,6 @@ const CustomLink = ({
className={customLinkClass}
tabIndex={disableKeyboardNavigation ? -1 : 0}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{content}
</a>

View file

@ -6,7 +6,7 @@
position: relative;
}
.button--text-link {
.button--link {
font-weight: $regular;
}

View file

@ -68,8 +68,7 @@ export const InverseVariant = Template("inverse");
export const InverseAlertVariant = Template("inverse-alert");
export const PillVariant = Template("pill");
export const TextLinkVariant = Template("text-link");
export const TextLinkDarkVariant = Template("text-link-dark");
export const LinkVariant = Template("link");
export const TextIconVariant = Template(
"text-icon",
<>

View file

@ -9,8 +9,7 @@ export type ButtonVariant =
| "alert"
| "pill"
| "grey-pill"
| "text-link" // Underlines on hover
| "text-link-dark" // underline on hover, dark text
| "link" // Looks like CustomLink with animated underline on hover
| "brand-inverse-icon" // Green icon with text, no underline on hover
| "text-icon"
| "icon" // Buttons without text
@ -143,7 +142,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
}
);
const onWhite =
variant === "text-link" ||
variant === "link" ||
variant === "inverse" ||
variant === "brand-inverse-icon" ||
variant === "text-icon" ||

View file

@ -217,64 +217,28 @@ $base-class: "button";
}
}
&--text-link {
@include button-variant(transparent);
// Looks exactly like a CustomLink but is a <button>
&--link {
@include link;
@include animated-bottom-border($core-fleet-black, $ui-fleet-black-25);
background: transparent;
border: 0;
box-shadow: none;
color: $ui-fleet-black-75;
font-size: $x-small;
cursor: pointer;
margin: 0;
padding: 0;
height: auto;
line-height: normal;
text-align: left;
display: inline-flex;
align-items: center;
gap: $pad-xsmall;
&:focus {
outline: none;
}
&:hover,
&:focus {
color: $ui-fleet-black-75-over;
background-color: transparent;
box-shadow: none;
text-decoration: underline;
}
&:active {
color: $ui-fleet-black-75-down;
box-shadow: none;
top: 0;
}
&.light-text {
font-weight: $regular;
}
}
&--text-link-dark {
@include button-variant(transparent);
border: 0;
box-shadow: none;
color: $core-fleet-black;
font-size: $x-small;
cursor: pointer;
margin: 0;
padding: 0;
height: auto;
line-height: normal;
text-align: left;
&:focus {
outline: none;
}
&:hover,
&:focus {
background-color: transparent;
box-shadow: none;
text-decoration: underline;
&:focus-visible {
@include unstyled-button-focus-outline(3px);
}
&:active {
@ -283,6 +247,8 @@ $base-class: "button";
}
}
// &--icon is used for svg icon buttons without text
&--text-icon,
&--icon {

View file

@ -34,7 +34,7 @@ const meta: Meta<typeof DropdownButton> = {
"success",
"alert",
"pill",
"text-link",
"link",
"text-icon",
"icon",
"inverse",

View file

@ -141,7 +141,7 @@ const ScriptListItem = ({
<ListItem
className={baseClass}
graphic={graphicName}
title={<Button variant="text-link">{script.name}</Button>}
title={<Button variant="link">{script.name}</Button>}
details={
<ScriptListItemDetails
platform={platform}

View file

@ -83,7 +83,7 @@ const HashCell = ({
// 2 or more hashes
return (
<Button variant="text-link" onClick={onClickMultipleHashes}>
<Button variant="link" onClick={onClickMultipleHashes}>
{uniqueHashCount.toString()} hashes
</Button>
);

View file

@ -36,7 +36,7 @@ const InstalledPathCell = ({
// 2 or more installed versions
return (
<Button variant="text-link" onClick={onClickMultiplePaths}>
<Button variant="link" onClick={onClickMultiplePaths}>
{uniquePathsCount.toString()} paths
</Button>
);

View file

@ -62,14 +62,8 @@ const AddUsersModal = ({
</div>
<p>
User not here?&nbsp;
<Button
onClick={onCreateNewTeamUser}
variant="text-link"
className="light-text"
>
<>
<strong>Create a user</strong>
</>
<Button onClick={onCreateNewTeamUser} variant="link">
Create a user
</Button>
</p>
<div className="modal-cta-wrap">

View file

@ -1,5 +1,4 @@
.user-form {
.form {
width: 100%;
}
@ -81,7 +80,7 @@
.sublabel-nosso {
font-size: 0.75rem;
button.button--text-link {
button.button--link {
display: inline-flex;
font-size: 0.75rem;
}

View file

@ -21,7 +21,7 @@ const getHostStatusPreview = (teamScope?: boolean) => {
return {
text:
"More than X% of your hosts have not checked into Fleet for more than Y days. Youve been sent this message because the Host status webhook is enabled in your Fleet instance.",
"More than X% of your hosts have not checked into Fleet for more than Y days. You've been sent this message because the Host status webhook is enabled in your Fleet instance.",
data,
};
};

View file

@ -85,7 +85,6 @@ import { strToBool } from "utilities/strings/stringUtils";
import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
import CustomLink from "components/CustomLink";
import { SingleValue } from "react-select-5";
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
@ -1936,10 +1935,12 @@ const ManageHostsPage = ({
<div>
<span>
You have no enroll secrets.{" "}
<CustomLink
customClickHandler={() => setShowEnrollSecretModal(true)}
text="Manage enroll secrets"
/>{" "}
<Button
variant="link"
onClick={() => setShowEnrollSecretModal(true)}
>
Manage enroll secrets
</Button>{" "}
to enroll hosts to{" "}
<b>{isAnyTeamSelected ? currentTeamName : "Fleet"}</b>.
</span>

View file

@ -66,7 +66,7 @@ const DeviceUserBanners = ({
variant="banner-link"
/>
) : (
<Button variant="text-link-dark" onClick={onClickTurnOnMdm}>
<Button variant="link" onClick={onClickTurnOnMdm}>
Turn on MDM
</Button>
);
@ -153,7 +153,7 @@ const DeviceUserBanners = ({
<InfoBanner
color="yellow"
cta={
<Button variant="text-link-dark" onClick={onClickCreatePIN}>
<Button variant="link" onClick={onClickCreatePIN}>
Create PIN
</Button>
}

View file

@ -79,7 +79,7 @@ const RecoveryLockPasswordModal = ({
if (canRotatePassword) {
return (
<Button
variant="text-link"
variant="inverse"
onClick={onRotatePassword}
disabled={isRotating}
className={`${baseClass}__rotate-button`}

View file

@ -145,7 +145,7 @@ const SelectQueryModal = ({
<>
{" "}
or{" "}
<Button variant="text-link" onClick={onQueryHostCustom}>
<Button variant="link" onClick={onQueryHostCustom}>
create your own report
</Button>
</>

View file

@ -4,7 +4,7 @@ import { IHostMdmProfile, MdmProfileStatus } from "interfaces/mdm";
import Icon from "components/Icon";
import { IconNames } from "components/icons";
import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button";
const baseClass = "os-settings-indicator";
@ -122,15 +122,16 @@ const OSSettingsIndicator = ({
const statusDisplayOption = STATUS_DISPLAY_OPTIONS[displayStatus];
// Using custom link for underline styling
return (
<span className={`${baseClass} info-flex__data`}>
<Icon name={statusDisplayOption.iconName} />
<CustomLink
text={displayStatus}
customClickHandler={onClick}
<Button
variant="link"
onClick={onClick}
className={`${baseClass}__button`}
/>
>
{displayStatus}
</Button>
</span>
);
};

View file

@ -2,10 +2,6 @@
display: flex;
gap: 4px;
&__button {
font-weight: normal;
}
.icon {
width: 16px;
height: 16px;

View file

@ -544,7 +544,7 @@ const InstallStatusCell = ({
return (
<Button
className={`${baseClass}__item-status-button`}
variant="text-link"
variant="link"
onClick={match.onClick}
>
{resolvedDisplayText}

View file

@ -31,7 +31,7 @@ import DataSet from "components/DataSet";
import CardHeader from "components/CardHeader";
import TooltipWrapperArchLinuxRolling from "components/TooltipWrapperArchLinuxRolling";
import Icon from "components/Icon/Icon";
import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button";
import DiskSpaceIndicator from "pages/hosts/components/DiskSpaceIndicator";
import { getCityCountryLocation } from "../../modals/LocationModal/LocationModal";
@ -365,14 +365,10 @@ const Vitals = ({
isIosOrIpadosHost && mdm?.enrollment_status === "On (automatic)";
if (isAdeIDevice || geolocation) {
// Using custom link for underline styling
const geoLocationButton = (
<CustomLink
customClickHandler={toggleLocationModal}
text={
isAdeIDevice ? "Show location" : getCityCountryLocation(geolocation)
}
/>
<Button variant="link" onClick={toggleLocationModal}>
{isAdeIDevice ? "Show location" : getCityCountryLocation(geolocation)}
</Button>
);
vitals.push({
sortKey: "Location",
@ -418,13 +414,12 @@ const Vitals = ({
value={
<>
{mdm.dep_profile_error && <Icon name="error" />}
<CustomLink
text={
<Button variant="link" onClick={toggleMDMStatusModal}>
{
MDM_ENROLLMENT_STATUS_UI_MAP[mdm.enrollment_status]
.displayName
}
customClickHandler={toggleMDMStatusModal}
/>
</Button>
</>
}
/>

View file

@ -17,7 +17,7 @@
.form-header {
display: flex;
justify-content: space-between;
.button--text-link {
.button--inverse {
white-space: nowrap;
}
}

View file

@ -1,6 +1,7 @@
import React, { useContext, useRef, useState } from "react";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import PATHS from "router/paths";
import TooltipTruncatedText from "components/TooltipTruncatedText";
import CriticalPolicyBadge from "components/CriticalPolicyBadge";
@ -139,8 +140,19 @@ const ConditionalAccessModal = ({
connect Fleet to {providerText}.
<br />
<br />
This can be configured in <b>Settings</b> &gt; <b>Integrations</b> &gt;{" "}
<b>Conditional access</b>.
This can be configured in{" "}
{isGlobalAdmin ? (
<CustomLink
url={PATHS.ADMIN_INTEGRATIONS_CONDITIONAL_ACCESS}
text="Settings > Integrations > Conditional access"
/>
) : (
<>
<b>Settings</b> &gt; <b>Integrations</b> &gt;{" "}
<b>Conditional access</b>
</>
)}
.
<br />
<br />
{learnMoreLink}

View file

@ -492,7 +492,7 @@ $max-width: 2560px;
display: block;
position: absolute;
left: 0;
bottom: -1px;
bottom: 0;
height: 1px;
width: 100%;
background: $inactive-color;
@ -509,7 +509,7 @@ $max-width: 2560px;
display: block;
position: absolute;
left: 0;
bottom: -1px;
bottom: 0;
height: 1px;
width: 100%;
background: $active-color;
@ -526,11 +526,6 @@ $max-width: 2560px;
$inactive-color: $ui-fleet-black-25
) {
position: relative;
margin-bottom: -1px;
&:hover {
margin-bottom: -1px; // Account for underline height, removed for link-cell truncated
}
@include bottom-border($active-color, $inactive-color);