Fleet UI: Component fixes (styling bugs and code cleanup) (#26149)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Fleet UI: Constistent behavior for table overflow and not hiding badges when user names overflow table cell
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from "react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import CustomLink from ".";
|
||||
|
||||
const meta: Meta<typeof CustomLink> = {
|
||||
@@ -24,3 +27,79 @@ export const ExternalLink: Story = {
|
||||
newTab: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Multiline: Story = {
|
||||
render: (args) => (
|
||||
<div
|
||||
style={{
|
||||
width: "400px",
|
||||
}}
|
||||
>
|
||||
Here's a CustomLink in a that might be split up across two lines{" "}
|
||||
<CustomLink {...args} />
|
||||
</div>
|
||||
),
|
||||
args: {
|
||||
url: "https://www.google.com",
|
||||
text:
|
||||
"This is a custom link that has multiple words that might span multiple lines and the icon should stick with the last word onto the new line",
|
||||
multiline: true,
|
||||
newTab: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const TooltipVariant: Story = {
|
||||
render: (args) => (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Tip content with a custom link <CustomLink {...args} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
Hover to see custom link in tooltip{" "}
|
||||
</TooltipWrapper>
|
||||
),
|
||||
args: {
|
||||
url: "https://www.google.com",
|
||||
text: "Tooltip link",
|
||||
variant: "tooltip-link",
|
||||
newTab: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const BannerVariant: Story = {
|
||||
render: (args) => (
|
||||
<InfoBanner>
|
||||
Here's a CustomLink in a banner <CustomLink {...args} />
|
||||
</InfoBanner>
|
||||
),
|
||||
args: {
|
||||
url: "https://www.google.com",
|
||||
text: "Banner link",
|
||||
variant: "banner-link",
|
||||
newTab: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const FlashMessageVariant: Story = {
|
||||
args: {
|
||||
url: "https://www.google.com",
|
||||
text: "Flash message link",
|
||||
variant: "flash-message-link",
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledKeyboardNav: Story = {
|
||||
render: (args) => (
|
||||
<>
|
||||
Here, you can't tab to this link even if you wanted to which is
|
||||
useful when within a disabled component. <CustomLink {...args} />
|
||||
</>
|
||||
),
|
||||
args: {
|
||||
url: "https://www.google.com",
|
||||
text: "Disabled Keyboard Navigation",
|
||||
disableKeyboardNavigation: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,22 +14,14 @@ interface ICustomLinkProps {
|
||||
newTab?: boolean;
|
||||
/** Icon wraps on new line with last word */
|
||||
multiline?: boolean;
|
||||
// TODO: Refactor to use variant
|
||||
iconColor?: Colors;
|
||||
// TODO: Refactor to use variant
|
||||
color?: "core-fleet-blue" | "core-fleet-black" | "core-fleet-white";
|
||||
/** Restricts access via keyboard when CustomLink is part of disabled UI */
|
||||
disableKeyboardNavigation?: boolean;
|
||||
/**
|
||||
* Changes the appearance of the link.
|
||||
*
|
||||
* @default "default"
|
||||
*
|
||||
* TODO:
|
||||
* Longterm: refactor 14 instances away from iconColor/color combo, which
|
||||
* usually are identical and repetitive, toward variants e.g. "banner-link"
|
||||
*/
|
||||
variant?: "tooltip-link" | "default" | "flash-message-link";
|
||||
variant?: "tooltip-link" | "banner-link" | "flash-message-link" | "default";
|
||||
}
|
||||
|
||||
const baseClass = "custom-link";
|
||||
@@ -40,8 +32,6 @@ const CustomLink = ({
|
||||
className,
|
||||
newTab = false,
|
||||
multiline = false,
|
||||
iconColor = "core-fleet-blue",
|
||||
color = "core-fleet-blue",
|
||||
disableKeyboardNavigation = false,
|
||||
variant = "default",
|
||||
}: ICustomLinkProps): JSX.Element => {
|
||||
@@ -50,15 +40,16 @@ const CustomLink = ({
|
||||
case "tooltip-link":
|
||||
case "flash-message-link":
|
||||
return "core-fleet-white";
|
||||
case "banner-link":
|
||||
return "core-fleet-black";
|
||||
default:
|
||||
return iconColor;
|
||||
return "core-fleet-blue";
|
||||
}
|
||||
};
|
||||
|
||||
const customLinkClass = classnames(baseClass, className, {
|
||||
[`${baseClass}--black`]: color === "core-fleet-black",
|
||||
[`${baseClass}--white`]: color === "core-fleet-white",
|
||||
[`${baseClass}--${variant}`]: variant !== "default",
|
||||
[`${baseClass}--multiline`]: multiline,
|
||||
});
|
||||
|
||||
const target = newTab ? "_blank" : "";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
.custom-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $pad-xsmall;
|
||||
// Changing display will break multiline links
|
||||
&:not(.custom-link--multiline) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
|
||||
&__no-wrap {
|
||||
white-space: nowrap;
|
||||
@@ -10,18 +13,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy
|
||||
&--black {
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
|
||||
&--white {
|
||||
color: $core-fleet-white;
|
||||
}
|
||||
|
||||
// Variants
|
||||
&--tooltip-link,
|
||||
&--banner-link,
|
||||
&--flash-messsage-link {
|
||||
color: inherit; // Overrides fleet blue link color with parent color
|
||||
}
|
||||
|
||||
&--tooltip-link {
|
||||
color: $core-fleet-white;
|
||||
font-size: $xx-small;
|
||||
font-size: inherit; // Overrides link default font size with parent tooltip font size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ const LicenseExpirationBanner = (): JSX.Element => {
|
||||
url="https://fleetdm.com/learn-more-about/downgrading"
|
||||
text="Downgrade or renew"
|
||||
newTab
|
||||
iconColor="core-fleet-black"
|
||||
color="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -19,8 +19,7 @@ const AppleBMRenewalMessage = ({ expired }: IAppleBMRenewalMessageProps) => {
|
||||
url="/settings/integrations/mdm/abm"
|
||||
text="Renew ABM"
|
||||
className={`${baseClass}`}
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -16,8 +16,7 @@ const AppleBMTermsMessage = () => {
|
||||
text="Go to ABM"
|
||||
className={`${baseClass}__new-tab`}
|
||||
newTab
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -20,8 +20,7 @@ const ApplePNCertRenewalMessage = ({ expired }: IApplePNCertRenewalMessage) => {
|
||||
text="Renew APNs"
|
||||
className={`${baseClass}__new-tab`}
|
||||
newTab
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -19,8 +19,7 @@ const VppRenewalMessage = ({ expired }: IVppRenewalMessageProps) => {
|
||||
url="/settings/integrations/mdm/vpp"
|
||||
text="Renew VPP"
|
||||
className={`${baseClass}`}
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Icon from "components/Icon";
|
||||
import { uniqueId } from "lodash";
|
||||
import React from "react";
|
||||
import ReactTooltip, { Place } from "react-tooltip";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
interface IPremiumFeatureIconWithTooltip {
|
||||
tooltipPlace?: Place;
|
||||
tooltipDelayHide?: number;
|
||||
tooltipPositionOverrides?: {
|
||||
leftAdj?: number;
|
||||
topAdj?: number;
|
||||
};
|
||||
}
|
||||
const PremiumFeatureIconWithTooltip = ({
|
||||
tooltipPlace,
|
||||
tooltipDelayHide = 100,
|
||||
tooltipPositionOverrides,
|
||||
}: IPremiumFeatureIconWithTooltip) => {
|
||||
const [leftAdj, topAdj] = [
|
||||
tooltipPositionOverrides?.leftAdj ?? 0,
|
||||
tooltipPositionOverrides?.topAdj ?? 0,
|
||||
];
|
||||
const tipId = uniqueId();
|
||||
return (
|
||||
<span className="premium-icon-tip">
|
||||
<span data-tip data-for={tipId}>
|
||||
<Icon name="premium-feature" className="premium-feature-icon" />
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place={tooltipPlace ?? "top"}
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={tipId}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
delayHide={tooltipDelayHide}
|
||||
delayUpdate={500}
|
||||
overridePosition={(pos: { left: number; top: number }) => {
|
||||
return {
|
||||
left: pos.left + leftAdj,
|
||||
top: pos.top + topAdj,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{`This is a Fleet Premium feature. `}
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/upgrade"
|
||||
text="Learn more"
|
||||
newTab
|
||||
multiline={false}
|
||||
iconColor="core-fleet-white"
|
||||
/>
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PremiumFeatureIconWithTooltip;
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import PremiumFeatureIconWithTooltip from "./PremiumFeatureIconWithTooltip";
|
||||
|
||||
const meta: Meta<typeof PremiumFeatureIconWithTooltip> = {
|
||||
title: "Components/PremiumFeatureIconWithTooltip",
|
||||
component: PremiumFeatureIconWithTooltip,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PremiumFeatureIconWithTooltip>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
@@ -1,5 +0,0 @@
|
||||
.premium-icon-tip {
|
||||
font-style: normal;
|
||||
font-size: $x-small;
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./PremiumFeatureIconWithTooltip";
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { kebabCase, noop } from "lodash";
|
||||
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
|
||||
|
||||
import { ButtonVariant } from "components/buttons/Button/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
@@ -17,7 +16,6 @@ export interface IActionButtonProps {
|
||||
hideButton?: boolean | ((targetIds: number[]) => boolean);
|
||||
iconSvg?: IconNames;
|
||||
iconPosition?: string;
|
||||
indicatePremiumFeature?: boolean;
|
||||
}
|
||||
|
||||
function useActionCallback(
|
||||
@@ -41,7 +39,6 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => {
|
||||
hideButton,
|
||||
iconSvg,
|
||||
iconPosition,
|
||||
indicatePremiumFeature,
|
||||
} = buttonProps;
|
||||
const onButtonClick = useActionCallback(onActionButtonClick || noop);
|
||||
|
||||
@@ -62,14 +59,7 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => {
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}__${kebabCase(name)}`}>
|
||||
{indicatePremiumFeature && (
|
||||
<PremiumFeatureIconWithTooltip tooltipDelayHide={500} />
|
||||
)}
|
||||
<Button
|
||||
disabled={indicatePremiumFeature}
|
||||
onClick={() => onButtonClick(targetIds)}
|
||||
variant={variant}
|
||||
>
|
||||
<Button onClick={() => onButtonClick(targetIds)} variant={variant}>
|
||||
<>
|
||||
{iconPosition === "left" && iconSvg && <Icon name={iconSvg} />}
|
||||
{buttonText}
|
||||
|
||||
@@ -402,7 +402,6 @@ const DataTable = ({
|
||||
hideButton,
|
||||
iconSvg,
|
||||
iconPosition,
|
||||
indicatePremiumFeature,
|
||||
} = actionButtonProps;
|
||||
return (
|
||||
<div className={`${baseClass}__${kebabCase(name)}`}>
|
||||
@@ -414,7 +413,6 @@ const DataTable = ({
|
||||
targetIds={targetIds}
|
||||
variant={variant}
|
||||
hideButton={hideButton}
|
||||
indicatePremiumFeature={indicatePremiumFeature}
|
||||
iconSvg={iconSvg}
|
||||
iconPosition={iconPosition}
|
||||
/>
|
||||
|
||||
+7
-3
@@ -17,6 +17,8 @@ interface ITooltipTruncatedTextCellProps {
|
||||
/** @deprecated use the prop `className` in order to add custom classes to this component */
|
||||
classes?: string;
|
||||
className?: string;
|
||||
/** Content does not get truncated */
|
||||
suffix?: React.ReactNode;
|
||||
}
|
||||
|
||||
const baseClass = "tooltip-truncated-cell";
|
||||
@@ -27,13 +29,14 @@ const TooltipTruncatedTextCell = ({
|
||||
tooltipBreakOnWord = false,
|
||||
classes = "w250",
|
||||
className,
|
||||
suffix,
|
||||
}: ITooltipTruncatedTextCellProps): JSX.Element => {
|
||||
const classNames = classnames(baseClass, classes, className, {
|
||||
"tooltip-break-on-word": tooltipBreakOnWord,
|
||||
});
|
||||
|
||||
// Tooltip visibility logic: Enable only when text is truncated
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [tooltipDisabled, setTooltipDisabled] = useState(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -51,14 +54,14 @@ const TooltipTruncatedTextCell = ({
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div
|
||||
className="data-table__tooltip-truncated-text"
|
||||
className="data-table__tooltip-truncated-text-container"
|
||||
data-tip
|
||||
data-for={tooltipId}
|
||||
data-tip-disable={isDefaultValue || tooltipDisabled}
|
||||
>
|
||||
<span
|
||||
ref={ref}
|
||||
className={`data-table__tooltip-truncated-text--cell ${
|
||||
className={`data-table__tooltip-truncated-text ${
|
||||
isDefaultValue ? "text-muted" : ""
|
||||
} ${tooltipDisabled ? "" : "truncated"}`}
|
||||
>
|
||||
@@ -81,6 +84,7 @@ const TooltipTruncatedTextCell = ({
|
||||
{/* Fixes triple click selecting next element in Safari */}
|
||||
</>
|
||||
</ReactTooltip>
|
||||
{suffix && <span className="data-table__suffix">{suffix}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
.tooltip-truncated-cell {
|
||||
display: flex;
|
||||
align-items: center; // For badges, etc
|
||||
|
||||
.text-muted {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
.data-table__tooltip-truncated-text-container {
|
||||
display: flex; // Flex container for text and suffix
|
||||
align-items: center; // Keep text and suffix aligned
|
||||
min-width: 0; // Prevent flex child from growing beyond container
|
||||
}
|
||||
|
||||
.data-table__tooltip-truncated-text {
|
||||
white-space: nowrap; /* Prevent wrapping */
|
||||
overflow: hidden; /* Hide overflowing text */
|
||||
text-overflow: ellipsis; /* Add ellipsis for truncated text */
|
||||
|
||||
&--cell {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
@@ -35,4 +49,9 @@
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.data-table__suffix {
|
||||
margin-left: 8px; /* Add spacing between text and suffix */
|
||||
flex-shrink: 0; /* Prevent suffix from shrinking */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import React, { KeyboardEvent } from "react";
|
||||
import React from "react";
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import AutoSizeInputField from ".";
|
||||
import { IAutoSizeInputFieldProps } from "./AutoSizeInputField";
|
||||
|
||||
import "../../../../index.scss";
|
||||
|
||||
interface IAutoSizeInputFieldProps {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
inputClassName?: string;
|
||||
maxLength: number;
|
||||
hasError?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isFocused?: boolean;
|
||||
onFocus: () => void;
|
||||
onBlur: () => void;
|
||||
onChange: (newSelectedValue: string) => void;
|
||||
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
export default {
|
||||
component: AutoSizeInputField,
|
||||
title: "Components/FormFields/Input/AutoSizeInputField",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { KeyboardEvent, useEffect, useRef } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
interface IAutoSizeInputFieldProps {
|
||||
export interface IAutoSizeInputFieldProps {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
.status__header {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.hosts__header {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.linkToFilteredHosts__header {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +30,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
.hosts_count__header {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
width: 9%;
|
||||
}
|
||||
@@ -30,9 +29,6 @@
|
||||
border-left: 0;
|
||||
width: 40px;
|
||||
}
|
||||
.linkToFilteredHosts__header {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
.hosts_count__header {
|
||||
border-right: 0;
|
||||
padding-right: 0;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
-4
@@ -10,10 +10,6 @@
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.data-table-block th:nth-last-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $break-lg) {
|
||||
.view-hosts-link {
|
||||
span {
|
||||
|
||||
-2
@@ -51,8 +51,6 @@ const SetupAssistantProfileUploader = ({
|
||||
text="Learn more"
|
||||
className={`${baseClass}__new-tab`}
|
||||
newTab
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-white"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+1
@@ -129,6 +129,7 @@ const FleetMaintainedAppDetailsPage = ({
|
||||
}
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext(
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
.software-title-details-table {
|
||||
.data-table {
|
||||
.hosts_count__header {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,6 @@
|
||||
width: $col-md;
|
||||
}
|
||||
|
||||
.hosts_count__header {
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $break-lg) {
|
||||
// expand the width of version header at larger screen sizes
|
||||
.versions__header {
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
&__wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
th:nth-last-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// used to position header text with premium icon correctly
|
||||
|
||||
+38
-48
@@ -1,17 +1,16 @@
|
||||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
import stringUtils from "utilities/strings";
|
||||
import { IUser, UserRole } from "interfaces/user";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
|
||||
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
|
||||
import ActionsDropdown from "components/ActionsDropdown";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import stringUtils from "utilities/strings";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
@@ -59,6 +58,32 @@ export interface ITeamUsersTableData {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const renderApiUserIndicator = () => {
|
||||
return (
|
||||
<TooltipWrapper
|
||||
className="api-only-tooltip"
|
||||
tipContent={
|
||||
<>
|
||||
This user was created using fleetctl and
|
||||
<br /> only has API access.{" "}
|
||||
<CustomLink
|
||||
text="Learn more"
|
||||
newTab
|
||||
url="https://fleetdm.com/docs/using-fleet/fleetctl-cli#using-fleetctl-with-an-api-only-user"
|
||||
variant="tooltip-link"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
tipOffset={14}
|
||||
position="top"
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<span className="team-users__api-only-user">API</span>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
const generateColumnConfigs = (
|
||||
@@ -72,52 +97,17 @@ const generateColumnConfigs = (
|
||||
sortType: "caseInsensitive",
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
const formatter = (val: string) => {
|
||||
const apiOnlyUser =
|
||||
"api_only" in cellProps.row.original
|
||||
? cellProps.row.original.api_only
|
||||
: false;
|
||||
const apiOnlyUser =
|
||||
"api_only" in cellProps.row.original
|
||||
? cellProps.row.original.api_only
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{val}
|
||||
{apiOnlyUser && (
|
||||
<>
|
||||
<span
|
||||
className="team-users__api-only-user"
|
||||
data-tip
|
||||
data-for={`api-only-tooltip-${cellProps.row.original.id}`}
|
||||
>
|
||||
API
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="api-only-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`api-only-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
clickable
|
||||
delayHide={200} // need delay set to hover using clickable
|
||||
>
|
||||
<>
|
||||
This user was created using fleetctl and
|
||||
<br /> only has API access.{" "}
|
||||
<CustomLink
|
||||
text="Learn more"
|
||||
newTab
|
||||
url="https://fleetdm.com/docs/using-fleet/fleetctl-cli#using-fleetctl-with-an-api-only-user"
|
||||
iconColor="core-fleet-white"
|
||||
/>
|
||||
</>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <TextCell value={cellProps.cell.value} formatter={formatter} />;
|
||||
return (
|
||||
<TooltipTruncatedTextCell
|
||||
value={cellProps.cell.value}
|
||||
suffix={apiOnlyUser && renderApiUserIndicator()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,25 +13,17 @@
|
||||
.data-table__table {
|
||||
thead {
|
||||
.name__header {
|
||||
width: $col-md;
|
||||
width: initial; // Allows name to take up as much space on table as it can before truncating
|
||||
}
|
||||
|
||||
.role__header {
|
||||
width: $col-md;
|
||||
}
|
||||
|
||||
.actions__header {
|
||||
width: auto;
|
||||
width: $col-sm;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
.name__cell {
|
||||
max-width: $col-md;
|
||||
}
|
||||
|
||||
.role__cell {
|
||||
max-width: $col-md;
|
||||
max-width: $col-sm;
|
||||
}
|
||||
|
||||
.actions__cell {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
border-radius: 30px;
|
||||
margin-left: $pad-medium;
|
||||
padding: 4px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-tabs__tab-list {
|
||||
@@ -39,4 +40,9 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name-text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,16 @@
|
||||
}
|
||||
|
||||
tbody {
|
||||
.name__cell {
|
||||
width: auto;
|
||||
// Override data-table td max-width at low widths to fit screen
|
||||
@media (max-width: ($break-md - 1)) {
|
||||
.name__cell {
|
||||
max-width: 430px;
|
||||
}
|
||||
}
|
||||
@media (max-width: ($break-sm - 1)) {
|
||||
.name__cell {
|
||||
max-width: 330px;
|
||||
}
|
||||
}
|
||||
|
||||
.host_count__cell,
|
||||
|
||||
@@ -3,104 +3,108 @@
|
||||
@include grey-badge;
|
||||
}
|
||||
|
||||
thead {
|
||||
// need specificity to override datatable css
|
||||
th {
|
||||
&.actions__header {
|
||||
padding-left: 0;
|
||||
}
|
||||
&.status__header,
|
||||
&.role__header {
|
||||
width: 86px; // set to prevent expanding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
// need specificity to override datatable css
|
||||
td.name__cell,
|
||||
td.role__cell,
|
||||
td.teams__cell,
|
||||
td.status__cell,
|
||||
td.email__cell {
|
||||
max-width: $col-sm;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.status__cell,
|
||||
td.role__cell {
|
||||
white-space: nowrap; // Prevent No access from wrapping
|
||||
}
|
||||
|
||||
td.actions__cell {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: ($break-lg)) {
|
||||
.name__header,
|
||||
.name__cell {
|
||||
max-width: $col-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: ($break-sm - 1)) {
|
||||
.email__header,
|
||||
.email__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: ($break-xs - 1)) {
|
||||
.status__header,
|
||||
.status__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: ($break-mobile-md - 1)) {
|
||||
.teams__header,
|
||||
.teams__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
// Splits header to 2 lines with user count on the first line
|
||||
.table-container__header {
|
||||
align-items: end;
|
||||
&-left {
|
||||
flex-direction: column;
|
||||
width: initial;
|
||||
align-items: start;
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
thead {
|
||||
// need specificity to override datatable css
|
||||
th {
|
||||
&.actions__header {
|
||||
padding-left: 0;
|
||||
}
|
||||
&.status__header,
|
||||
&.role__header {
|
||||
width: 86px; // set to prevent expanding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container__search {
|
||||
width: 100%;
|
||||
tbody {
|
||||
// need specificity to override datatable css
|
||||
td.name__cell,
|
||||
td.role__cell,
|
||||
td.teams__cell,
|
||||
td.status__cell,
|
||||
td.email__cell {
|
||||
max-width: $col-sm;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.status__cell,
|
||||
td.role__cell {
|
||||
white-space: nowrap; // Prevent No access from wrapping
|
||||
}
|
||||
|
||||
td.actions__cell {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: ($break-mobile-sm - 1)) {
|
||||
.role__header,
|
||||
.role__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
@media (min-width: ($break-lg)) {
|
||||
.name__header,
|
||||
.name__cell {
|
||||
max-width: $col-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Splits header to 3 lines; user count, wide add user button, wide search
|
||||
.table-container__header {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
@media (max-width: ($break-sm - 1)) {
|
||||
.email__header,
|
||||
.email__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-left {
|
||||
width: 100%;
|
||||
@media (max-width: ($break-xs - 1)) {
|
||||
.status__header,
|
||||
.status__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.controls,
|
||||
.button {
|
||||
@media (max-width: ($break-mobile-md - 1)) {
|
||||
.teams__header,
|
||||
.teams__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
// Splits header to 2 lines with user count on the first line
|
||||
.table-container__header {
|
||||
align-items: end;
|
||||
&-left {
|
||||
flex-direction: column;
|
||||
width: initial;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.table-container__search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: ($break-mobile-sm - 1)) {
|
||||
.role__header,
|
||||
.role__cell {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
// Splits header to 3 lines; user count, wide add user button, wide search
|
||||
.table-container__header {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
|
||||
&-left {
|
||||
width: 100%;
|
||||
|
||||
.controls,
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-48
@@ -1,17 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
import StatusIndicator from "components/StatusIndicator";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import { IInvite } from "interfaces/invite";
|
||||
import { IUser, UserRole } from "interfaces/user";
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
import { generateRole, generateTeam, greyCell } from "utilities/helpers";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { renderApiUserIndicator } from "pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig";
|
||||
import ActionsDropdown from "../../../../../components/ActionsDropdown";
|
||||
|
||||
interface IHeaderProps {
|
||||
@@ -75,52 +74,17 @@ const generateTableHeaders = (
|
||||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
const formatter = (val: string) => {
|
||||
const apiOnlyUser =
|
||||
"api_only" in cellProps.row.original
|
||||
? cellProps.row.original.api_only
|
||||
: false;
|
||||
const apiOnlyUser =
|
||||
"api_only" in cellProps.row.original
|
||||
? cellProps.row.original.api_only
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{val}
|
||||
{apiOnlyUser && (
|
||||
<>
|
||||
<span
|
||||
className="user-management__api-only-user"
|
||||
data-tip
|
||||
data-for={`api-only-tooltip-${cellProps.row.original.id}`}
|
||||
>
|
||||
API
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="api-only-tooltip"
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`api-only-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
clickable
|
||||
delayHide={200} // need delay set to hover using clickable
|
||||
>
|
||||
<>
|
||||
This user was created using fleetctl and
|
||||
<br /> only has API access.{" "}
|
||||
<CustomLink
|
||||
text="Learn more"
|
||||
newTab
|
||||
url="https://fleetdm.com/docs/using-fleet/fleetctl-cli#using-fleetctl-with-an-api-only-user"
|
||||
iconColor="core-fleet-white"
|
||||
/>
|
||||
</>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <TextCell value={cellProps.cell.value} formatter={formatter} />;
|
||||
return (
|
||||
<TooltipTruncatedTextCell
|
||||
value={cellProps.cell.value}
|
||||
suffix={apiOnlyUser && renderApiUserIndicator()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -149,13 +149,11 @@ const ManageHostsPage = ({
|
||||
isOnlyObserver,
|
||||
isPremiumTier,
|
||||
isFreeTier,
|
||||
isSandboxMode,
|
||||
userSettings,
|
||||
setFilteredHostsPath,
|
||||
setFilteredPoliciesPath,
|
||||
setFilteredQueriesPath,
|
||||
setFilteredSoftwarePath,
|
||||
setUserSettings,
|
||||
} = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
@@ -1563,7 +1561,6 @@ const ManageHostsPage = ({
|
||||
variant: "text-icon",
|
||||
iconSvg: "transfer",
|
||||
hideButton: !isPremiumTier || (!isGlobalAdmin && !isGlobalMaintainer),
|
||||
indicatePremiumFeature: isPremiumTier && isSandboxMode,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1704,7 +1701,7 @@ const ManageHostsPage = ({
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
{renderHeader()}
|
||||
<div className={`${baseClass} button-wrap`}>
|
||||
{!isSandboxMode && canEnrollHosts && !hasErrors && (
|
||||
{canEnrollHosts && !hasErrors && (
|
||||
<Button
|
||||
onClick={() => setShowEnrollSecretModal(true)}
|
||||
className={`${baseClass}__enroll-hosts button`}
|
||||
|
||||
+1
-2
@@ -88,8 +88,7 @@ const DeviceUserBanners = ({
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/learn-more-about/encrypt-linux-device"
|
||||
text="Guide"
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
/>
|
||||
}
|
||||
color="yellow"
|
||||
|
||||
+1
-2
@@ -124,8 +124,7 @@ const HostDetailsBanners = ({
|
||||
<CustomLink
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/mdm-disk-encryption`}
|
||||
text="Guide"
|
||||
color="core-fleet-black"
|
||||
iconColor="core-fleet-black"
|
||||
variant="banner-link"
|
||||
newTab
|
||||
/>
|
||||
}
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ const generateErrorTooltip = (
|
||||
text="Learn more"
|
||||
url="https://fleetdm.com/learn-more-about/idp-email"
|
||||
newTab
|
||||
iconColor="core-fleet-white"
|
||||
variant="tooltip-link"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user