**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" />
270 lines
6.4 KiB
React
270 lines
6.4 KiB
React
import React, { Component } from "react";
|
|
import PropTypes from "prop-types";
|
|
import classnames from "classnames";
|
|
import { isEqual, noop } from "lodash";
|
|
|
|
import targetsAPI from "services/entities/targets";
|
|
import targetInterface from "interfaces/target";
|
|
import { formatSelectedTargetsForApi } from "utilities/helpers";
|
|
|
|
import Button from "components/buttons/Button";
|
|
import Icon from "components/Icon";
|
|
import Input from "./SelectTargetsInput";
|
|
import Menu from "./SelectTargetsMenu";
|
|
|
|
const baseClass = "target-select";
|
|
|
|
class SelectTargetsDropdown extends Component {
|
|
static propTypes = {
|
|
disabled: PropTypes.bool,
|
|
error: PropTypes.string,
|
|
label: PropTypes.string,
|
|
onFetchTargets: PropTypes.func,
|
|
onSelect: PropTypes.func.isRequired,
|
|
selectedTargets: PropTypes.arrayOf(targetInterface),
|
|
targetsCount: PropTypes.number,
|
|
queryId: PropTypes.number,
|
|
isPremiumTier: PropTypes.bool,
|
|
};
|
|
|
|
static defaultProps = {
|
|
disabled: false,
|
|
onFetchTargets: noop,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
isEmpty: false,
|
|
isLoadingTargets: false,
|
|
moreInfoTarget: null,
|
|
query: "",
|
|
targets: [],
|
|
};
|
|
}
|
|
|
|
componentWillMount() {
|
|
this.mounted = true;
|
|
this.wrapperHeight = 0;
|
|
this.fetchTargets();
|
|
return false;
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
const { selectedTargets } = nextProps;
|
|
const { query } = this.state;
|
|
const { queryId } = this.props;
|
|
|
|
if (!isEqual(selectedTargets, this.props.selectedTargets)) {
|
|
this.fetchTargets(query, queryId, selectedTargets);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.mounted = false;
|
|
}
|
|
|
|
onInputClose = () => {
|
|
const { document } = global;
|
|
const coreWrapper = document.querySelector(".core-wrapper");
|
|
|
|
this.setState({ moreInfoTarget: null, query: "" });
|
|
coreWrapper.style.height = "auto";
|
|
|
|
return false;
|
|
};
|
|
|
|
onInputFocus = () => {
|
|
const { document } = global;
|
|
this.wrapperHeight = document.querySelector(".core-wrapper").scrollHeight;
|
|
|
|
return false;
|
|
};
|
|
|
|
onInputOpen = () => {
|
|
const { document } = global;
|
|
const { wrapperHeight } = this;
|
|
|
|
const lookForOuterMenu = setInterval(() => {
|
|
if (document.querySelectorAll(".Select-menu-outer")) {
|
|
clearInterval(lookForOuterMenu);
|
|
const coreWrapper = document.querySelector(".core-wrapper");
|
|
|
|
const currentWrapperHeight = coreWrapper.scrollHeight;
|
|
if (wrapperHeight < currentWrapperHeight) {
|
|
coreWrapper.style.height = `${
|
|
wrapperHeight + (currentWrapperHeight - wrapperHeight) + 15
|
|
}px`;
|
|
}
|
|
}
|
|
}, 5);
|
|
|
|
return false;
|
|
};
|
|
|
|
onTargetSelectMoreInfo = (moreInfoTarget) => {
|
|
return (evt) => {
|
|
evt.preventDefault();
|
|
|
|
const currentMoreInfoTarget = this.state.moreInfoTarget || {};
|
|
|
|
if (isEqual(moreInfoTarget.id, currentMoreInfoTarget.id)) {
|
|
return false;
|
|
}
|
|
|
|
this.setState({ moreInfoTarget });
|
|
|
|
return false;
|
|
};
|
|
};
|
|
|
|
onBackToResults = () => {
|
|
this.setState({ moreInfoTarget: null });
|
|
};
|
|
|
|
fetchTargets = (
|
|
query = "",
|
|
queryId = this.props.queryId,
|
|
selectedTargets = this.props.selectedTargets
|
|
) => {
|
|
const { onFetchTargets } = this.props;
|
|
|
|
if (!this.mounted) {
|
|
return false;
|
|
}
|
|
|
|
this.setState({ isLoadingTargets: true, query });
|
|
|
|
return targetsAPI
|
|
.DEPRECATED_loadAll(
|
|
query,
|
|
queryId,
|
|
formatSelectedTargetsForApi(selectedTargets)
|
|
)
|
|
.then((response) => {
|
|
const { targets } = response;
|
|
const isEmpty = targets.length === 0;
|
|
|
|
if (!this.mounted) {
|
|
return false;
|
|
}
|
|
|
|
if (isEmpty) {
|
|
// We don't want the lib's default "No Results" so we fake it
|
|
targets.push({});
|
|
}
|
|
|
|
onFetchTargets(query, response);
|
|
|
|
this.setState({
|
|
isEmpty,
|
|
isLoadingTargets: false,
|
|
targets,
|
|
});
|
|
|
|
return query;
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error getting targets:", error);
|
|
|
|
if (this.mounted) {
|
|
this.setState({ isLoadingTargets: false });
|
|
}
|
|
});
|
|
};
|
|
|
|
renderLabel = () => {
|
|
const { error, label, targetsCount } = this.props;
|
|
|
|
const labelClassName = classnames(`${baseClass}__label`, {
|
|
[`${baseClass}__label--error`]: error,
|
|
});
|
|
|
|
if (!label) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<p className={labelClassName}>
|
|
<span className={`${baseClass}__select-targets`}>{error || label}</span>
|
|
<span className={`${baseClass}__targets-count`}>
|
|
{" "}
|
|
{targetsCount} unique {targetsCount === 1 ? "host" : "hosts"}
|
|
</span>
|
|
</p>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const { isEmpty, isLoadingTargets, moreInfoTarget, targets } = this.state;
|
|
const {
|
|
fetchTargets,
|
|
onBackToResults,
|
|
onInputClose,
|
|
onInputOpen,
|
|
onInputFocus,
|
|
onTargetSelectMoreInfo,
|
|
renderLabel,
|
|
} = this;
|
|
const { disabled, onSelect, selectedTargets, isPremiumTier } = this.props;
|
|
|
|
const menuRenderer = Menu(
|
|
onTargetSelectMoreInfo,
|
|
moreInfoTarget,
|
|
onBackToResults,
|
|
isPremiumTier
|
|
);
|
|
|
|
const inputClasses = classnames({
|
|
"show-preview": moreInfoTarget,
|
|
"is-empty": isEmpty,
|
|
});
|
|
|
|
const ArrowRenderer = ({ isOpen }) => (
|
|
<div
|
|
className={classnames("target-select__arrow", {
|
|
"target-select__arrow--open": isOpen,
|
|
})}
|
|
>
|
|
<Icon name="chevron-down" />
|
|
</div>
|
|
);
|
|
|
|
const ClearRenderer = () => (
|
|
<Button
|
|
type="button"
|
|
className="target-select__clear"
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
variant="subdued"
|
|
>
|
|
<Icon name="close" />
|
|
</Button>
|
|
);
|
|
|
|
return (
|
|
<div className={`${baseClass}__wrapper form-field`}>
|
|
{renderLabel()}
|
|
<Input
|
|
className={inputClasses}
|
|
disabled={disabled}
|
|
isLoading={isLoadingTargets}
|
|
menuRenderer={menuRenderer}
|
|
onClose={onInputClose}
|
|
onOpen={onInputOpen}
|
|
onFocus={onInputFocus}
|
|
onTargetSelect={onSelect}
|
|
onTargetSelectInputChange={fetchTargets}
|
|
selectedTargets={selectedTargets}
|
|
targets={targets}
|
|
isPremiumTier={isPremiumTier}
|
|
arrowRenderer={ArrowRenderer}
|
|
clearRenderer={ClearRenderer}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default SelectTargetsDropdown;
|