mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 14:58:33 +00:00
## Addresses #15325 - Define shared global styles for forms (`form` and `.form`s) and `.form-field`s - Sweep through the app, updating each form from being locally styled to first prioritizing the global styles and only defining local styles where needed for custom behavior - Remove lots of unnecessary nesting of elements - Other small bug fixes and improvements ### Samples, before (L) | after (R): **Save query modal**  **Edit query form**  **Add hosts modal**  ## QA Plan: @xpkoala here's the same list from the issue, freshly de-checked for you to use if it's helpful: * Please check error states of each field #### Specified by issue: ##### In "Save query" modal: - [ ] Reduce space between checkboxes and their help text to 8px/0.5rem for the following fields: - [ ] Observers can run - [ ] Discard data - [ ] Update the following checkbox labels to have normal font weight (not bold): - [ ] Discard data ##### On "Edit query" page: - [ ] Update the following checkbox labels to have normal font weight (not bold): - [ ] Observers can run - [ ] Discard data ##### In "Add hosts" modal, for copy text fields: - [ ] match typical form form field styles - [ ] Adjust the positioning of the copy icon to keep it from being too far down ##### Further locations to check - [ ] ChangeEmailForm.jsx - [ ] ChangePasswordForm.jsx - [ ] ConfirmInviteForm.jsx - [ ] ConfirmSSOInviteForm.jsx - [ ] EnrollSecretModal.tsx - [ ] ForgotPasswordForm.jsx - [ ] LoginForm.tsx - [ ] EditPackForm.tsx - [ ] (New)PackForm.tsx - [ ] AdminDetails.jsx - [ ] ConfirmationPage.tsx - [ ] FleetDetails.jsx - [ ] OrgDetails.jsx - [ ] ResetPasswordForm.tsx - [ ] UserSettingsForm.jsx - [ ] EditTeamModal.tsx - [ ] IdpSection.tsx - [ ] DeleteIntegrationModal.tsx - [ ] IntegrationForm.tsx - [ ] EndUserMigrationSection.tsx - [ ] RequestCSRModal.tsx - [ ] Advanced.tsx - [ ] Agents.tsx - [ ] FleetDesktop.tsx - [ ] HostStatusWebhook.tsx front - [ ] Info.tsx - [ ] Smtp.tsx - [ ] Sso.tsx - [ ] Statistics.tsx - [ ] WebAddress.tsx - [ ] CreateTeamModal.tsx - [ ] DeleteTeamModal.tsx - [ ] EditTeamModal.tsx - [ ] AgentOptionsPage.tsx - updated the layout of this page to align with the rest of the forms in the UI – can easily revert if it's not what we want - [ ] AddMemberModal.tsx - [ ] RemoveMemberModal.tsx - [ ] UserForm.tsx - Used by both `EditUserModal` and `CreateUserModal` - A few different conditions that cause different rendering behavior - [ ] DeleteHostModal.tsx - [ ] TransferHostModal.tsx - [ ] LabelForm.tsx - [ ] MacOSTargetForm.tsx - [ ] WindowsTargetForm.tsx - [ ] BootstrapPackageListltem.ts - [ ] EndUserAuthForm.tsx - [ ] PackQueryEditorModal.tsx - [ ] PolicyForm.tsx - [ ] SaveNewPolicyModal.tsx - [ ] ConfirmSaveChangesModal.tsx - [ ] Query automations modal - [ ] Policy automations modal - addresses #16010 - [ ] SoftwareAutomationsModal ## Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com> Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
243 lines
5.8 KiB
JavaScript
243 lines
5.8 KiB
JavaScript
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 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,
|
|
});
|
|
|
|
return (
|
|
<div className={`${baseClass} 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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default SelectTargetsDropdown;
|