**Related issue:** Resolves #49276 **New features** - Added new "Secondary" (bordered, off-white fill) and "Subdued" (borderless, low-emphasis) button variants to match the Figma spec, alongside the existing Primary style. - Allowed rows to be selected in Controls > OS updates. **Cleanup** - Once nothing referenced the old styles anymore, fully removed the old `text-icon`, `brand-inverse-icon`, `inverse-alert`, `inverse`, and `icon` button variants (type, styles, and Storybook entries) from the shared `Button` component. - Removed the `iconStroke` prop, which had become a no-op once the old variants it supported were gone. - Renamed `ActionsDropdown`'s variants (`button`/`brand-button`/`small-button`) to `subdued`/`primary`/`secondary` to match the same naming used everywhere else. - Replaced a one-off dropdown implementation on the Software title page with the shared `ActionsDropdown` component, instead of maintaining duplicate styling logic. - Changed the button name on Host details > Reports > Report details from "View data for all hosts" to "View report for all hosts" (to match the previous page's Actions drop-down options). # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <img width="1475" height="241" alt="Screenshot 2026-07-21 at 06 35 49" src="https://github.com/user-attachments/assets/7cfbd444-7837-40e8-854e-bc5989d57d85" /> <img width="661" height="306" alt="Screenshot 2026-07-21 at 06 37 18" src="https://github.com/user-attachments/assets/5d0c4873-8179-4089-b115-7e8cd3a53b4d" /> <img width="1427" height="423" alt="Screenshot 2026-07-21 at 06 37 30" src="https://github.com/user-attachments/assets/a4850a60-f44a-4902-b45e-0094f23a52f8" /> <img width="1427" height="640" alt="Screenshot 2026-07-21 at 06 37 46" src="https://github.com/user-attachments/assets/738a4a7f-cd7d-4162-b659-6f649c32204d" /> <img width="1445" height="479" alt="Screenshot 2026-07-22 at 07 03 22" src="https://github.com/user-attachments/assets/4f672dc0-5c6d-4eb8-8465-ed5233fcd1b2" /> <img width="811" height="871" alt="Screenshot 2026-07-21 at 06 41 20" src="https://github.com/user-attachments/assets/5421c96e-2dab-492a-af26-be0e5a7791ca" />
173 lines
4.3 KiB
TypeScript
173 lines
4.3 KiB
TypeScript
import React, { useCallback, useEffect, useRef } from "react";
|
|
import classnames from "classnames";
|
|
|
|
import { ICON_MAP } from "components/icons";
|
|
import Icon from "components/Icon/Icon";
|
|
import TooltipWrapper from "components/TooltipWrapper";
|
|
import Button from "components/buttons/Button";
|
|
|
|
const baseClass = "input-icon-field";
|
|
|
|
type IconName = keyof typeof ICON_MAP;
|
|
|
|
export interface IInputFieldWithIconProps {
|
|
autofocus?: boolean;
|
|
error?: string | null;
|
|
helpText?: string[] | string;
|
|
iconSvg?: IconName;
|
|
label?: string;
|
|
name?: string;
|
|
onChange?: (value: string) => void;
|
|
onClick?: (evt: React.MouseEvent<HTMLInputElement>) => void;
|
|
clearButton?: boolean;
|
|
placeholder?: string;
|
|
tabIndex?: number;
|
|
type?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
inputOptions?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
tooltip?: string;
|
|
/**
|
|
* Whether 1Password should skip this field.
|
|
* Defaults to `true` because most Fleet inputs are not credential fields.
|
|
*/
|
|
ignore1Password?: boolean;
|
|
value?: string;
|
|
}
|
|
|
|
const InputFieldWithIcon = ({
|
|
autofocus = false,
|
|
error,
|
|
helpText,
|
|
iconSvg,
|
|
label,
|
|
name,
|
|
onChange,
|
|
onClick,
|
|
clearButton,
|
|
placeholder,
|
|
tabIndex,
|
|
type,
|
|
className,
|
|
disabled,
|
|
inputOptions,
|
|
tooltip,
|
|
ignore1Password = true,
|
|
value,
|
|
}: IInputFieldWithIconProps): JSX.Element => {
|
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (autofocus && inputRef.current) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [autofocus]);
|
|
|
|
const onInputChange = useCallback(
|
|
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
evt.preventDefault();
|
|
|
|
return onChange?.(evt.target.value);
|
|
},
|
|
[onChange]
|
|
);
|
|
|
|
const renderHeading = () => {
|
|
const labelClasses = classnames(`${baseClass}__label`, {
|
|
[`${baseClass}__errors`]: !!error,
|
|
[`${baseClass}__label--disabled`]: disabled,
|
|
});
|
|
|
|
return (
|
|
<label
|
|
htmlFor={name}
|
|
className={labelClasses}
|
|
data-has-tooltip={!!tooltip}
|
|
>
|
|
{tooltip && !error ? (
|
|
<TooltipWrapper position="bottom-start" tipContent={tooltip}>
|
|
{label}
|
|
</TooltipWrapper>
|
|
) : (
|
|
<>{error || label}</>
|
|
)}
|
|
</label>
|
|
);
|
|
};
|
|
|
|
const renderHelpText = () => {
|
|
if (helpText) {
|
|
return (
|
|
<span className={`${baseClass}__help-text form-field__help-text`}>
|
|
{helpText}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const wrapperClasses = classnames(baseClass, "form-field");
|
|
|
|
const inputClasses = classnames(
|
|
`${baseClass}__input`,
|
|
className,
|
|
{ "input-with-icon": !!iconSvg },
|
|
{ [`${baseClass}__input--error`]: !!error },
|
|
{ [`${baseClass}__input--password`]: !!(type === "password" && value) }
|
|
);
|
|
|
|
const inputWrapperClasses = classnames(`${baseClass}__input-wrapper`, {
|
|
[`${baseClass}__input-wrapper--disabled`]: disabled,
|
|
});
|
|
|
|
const iconClasses = classnames(
|
|
`${baseClass}__icon`,
|
|
{ [`${baseClass}__icon--error`]: !!error },
|
|
{ [`${baseClass}__icon--active`]: !!value }
|
|
);
|
|
|
|
const handleClear = () => {
|
|
onChange?.("");
|
|
};
|
|
|
|
return (
|
|
<div className={wrapperClasses}>
|
|
{label && renderHeading()}
|
|
<div className={inputWrapperClasses}>
|
|
<input
|
|
id={name}
|
|
name={name}
|
|
onChange={onInputChange}
|
|
onClick={onClick}
|
|
className={inputClasses}
|
|
placeholder={placeholder}
|
|
ref={inputRef}
|
|
tabIndex={tabIndex}
|
|
type={type}
|
|
value={value}
|
|
disabled={disabled}
|
|
{...inputOptions}
|
|
// 1Password only checks for the presence (not the value) of `data-1p-ignore`,
|
|
// so we omit the attribute entirely when the field is not meant to be ignored.
|
|
// See https://developer.1password.com/docs/web/compatible-website-design/
|
|
data-1p-ignore={ignore1Password || undefined}
|
|
/>
|
|
{iconSvg && <Icon name={iconSvg} className={iconClasses} />}
|
|
{clearButton && !!value && (
|
|
<Button
|
|
onClick={() => handleClear()}
|
|
variant="subdued"
|
|
className={`${baseClass}__clear-button`}
|
|
>
|
|
<Icon name="close-filled" color="core-fleet-black" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{renderHelpText()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InputFieldWithIcon;
|