add ability to create manual labels (#18303)

relates to #17031

Adds functionality to create manual labels in fleet.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] M0anual QA for all new/changed functionality

---------

Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
This commit is contained in:
Gabriel Hernandez
2024-04-16 17:22:08 +01:00
committed by GitHub
co-authored by Martin Angers
parent de94299b65
commit 2fc4e520b8
59 changed files with 1607 additions and 726 deletions
@@ -30,6 +30,7 @@ import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
import { generateTableHeaders } from "./TargetsInput/TargetsInputHostsTableConfig";
interface ITargetPillSelectorProps {
entity: ISelectLabel | ISelectTeam;
@@ -305,9 +306,8 @@ const SelectTargets = ({
: setTargetedTeams(newTargets as ITeam[]);
};
const handleRowSelect = (row: Row) => {
const selectedHost = row.original as IHost;
setTargetedHosts((prevHosts) => prevHosts.concat(selectedHost));
const handleRowSelect = (row: Row<IHost>) => {
setTargetedHosts((prevHosts) => prevHosts.concat(row.original));
setSearchText("");
// If "all hosts" is already selected when using host target picker, deselect "all hosts"
@@ -434,6 +434,9 @@ const SelectTargets = ({
);
}
const resultsTableConfig = generateTableHeaders();
const selectedHostsTableConfig = generateTableHeaders(handleRowRemove);
return (
<div className={`${baseClass}__wrapper`}>
<h1>Select targets</h1>
@@ -451,6 +454,9 @@ const SelectTargets = ({
renderTargetEntityList("Labels", labels.other)}
</div>
<TargetsInput
autofocus
searchResultsTableConfig={resultsTableConfig}
selectedHostsTableConifg={selectedHostsTableConfig}
tabIndex={inputTabIndex || 0}
searchText={searchText}
searchResults={searchResults || []}
@@ -459,7 +465,7 @@ const SelectTargets = ({
hasFetchError={!!errorSearchResults}
setSearchText={setSearchText}
handleRowSelect={handleRowSelect}
handleRowRemove={handleRowRemove}
disablePagination
/>
<div className={`${baseClass}__targets-button-wrap`}>
<Button
@@ -9,22 +9,31 @@ import DataError from "components/DataError";
// @ts-ignore
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon/InputFieldWithIcon";
import TableContainer from "components/TableContainer";
import { generateTableHeaders } from "./TargetsInputHostsTableConfig";
import { ITargestInputHostTableConfig } from "./TargetsInputHostsTableConfig";
interface ITargetsInputProps {
tabIndex: number;
tabIndex?: number;
searchText: string;
searchResults: IHost[];
isTargetsLoading: boolean;
hasFetchError: boolean;
targetedHosts: IHost[];
searchResultsTableConfig: ITargestInputHostTableConfig[];
selectedHostsTableConifg: ITargestInputHostTableConfig[];
/** disabled pagination for the results table. The pagination is currently
* client side pagination. Defaults to `false` */
disablePagination?: boolean;
label?: string;
placeholder?: string;
autofocus?: boolean;
setSearchText: (value: string) => void;
handleRowSelect: (value: Row) => void;
handleRowRemove: (value: Row<IHost>) => void;
handleRowSelect: (value: Row<IHost>) => void;
}
const baseClass = "targets-input";
const DEFAULT_LABEL = "Target specific hosts";
const TargetsInput = ({
tabIndex,
searchText,
@@ -32,12 +41,15 @@ const TargetsInput = ({
isTargetsLoading,
hasFetchError,
targetedHosts,
searchResultsTableConfig,
selectedHostsTableConifg,
disablePagination = false,
label = DEFAULT_LABEL,
placeholder = HOSTS_SEARCH_BOX_PLACEHOLDER,
autofocus = false,
handleRowSelect,
handleRowRemove,
setSearchText,
}: ITargetsInputProps): JSX.Element => {
const resultsDropdownTableHeaders = generateTableHeaders();
const selectedTableHeaders = generateTableHeaders(handleRowRemove);
const dropdownHosts =
searchResults && pullAllBy(searchResults, targetedHosts, "display_name");
const isActiveSearch =
@@ -48,20 +60,20 @@ const TargetsInput = ({
<div>
<div className={baseClass}>
<InputFieldWithIcon
autofocus
autofocus={autofocus}
type="search"
iconSvg="search"
value={searchText}
tabIndex={tabIndex}
iconPosition="start"
label="Target specific hosts"
placeholder={HOSTS_SEARCH_BOX_PLACEHOLDER}
label={label}
placeholder={placeholder}
onChange={setSearchText}
/>
{isActiveSearch && (
<div className={`${baseClass}__hosts-search-dropdown`}>
<TableContainer
columnConfigs={resultsDropdownTableHeaders}
<TableContainer<Row<IHost>>
columnConfigs={searchResultsTableConfig}
data={dropdownHosts}
isLoading={isTargetsLoading}
resultsTitle=""
@@ -81,7 +93,7 @@ const TargetsInput = ({
disableCount
disablePagination
disableMultiRowSelect
onSelectSingleRow={handleRowSelect}
onClickRow={handleRowSelect}
/>
</div>
)}
@@ -92,14 +104,15 @@ const TargetsInput = ({
)}
<div className={`${baseClass}__hosts-selected-table`}>
<TableContainer
columnConfigs={selectedTableHeaders}
columnConfigs={selectedHostsTableConifg}
data={targetedHosts}
isLoading={false}
resultsTitle=""
showMarkAllPages={false}
isAllPagesSelected={false}
disableCount
disablePagination
disablePagination={disablePagination}
isClientSidePagination={!disablePagination}
emptyComponent={() => <></>}
/>
</div>
@@ -11,14 +11,14 @@ import LiveQueryIssueCell from "components/TableContainer/DataTable/LiveQueryIss
import StatusIndicator from "components/StatusIndicator";
import Icon from "components/Icon/Icon";
type ITargestInputhostTableConfig = Column<IHost>;
export type ITargestInputHostTableConfig = Column<IHost>;
type ITableStringCellProps = IStringCellProps<IHost>;
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
export const generateTableHeaders = (
handleRowRemove?: (value: Row<IHost>) => void
): ITargestInputhostTableConfig[] => {
): ITargestInputHostTableConfig[] => {
const deleteHeader = handleRowRemove
? [
{
@@ -93,5 +93,15 @@
width: 100%;
overflow: auto;
}
.delete__cell {
text-align: center;
}
}
// override the default styles for the spinner.
// TODO: set better default styles for the spinner
.loading-spinner.centered {
margin: auto;
}
}