fleet/frontend/components/TargetLabelSelector/TargetLabelSelector.tsx
Gabriel Hernandez 0876a9d695
Add UI for scoping software to fleet apps and custom packages via labels (#24793)
relates to #24538, #24542, #24540, #24537

implements the UI for scoping software to fleet maintained apps and
custom packages. This includes:

**adding custom target label selection to fleet maintained app form**

<img width="824" alt="image"
src="https://github.com/user-attachments/assets/b0e18841-e5c5-406a-b83f-ce2b5a8f8472"
/>

**adding custom target label selection to custom package form**

<img width="821" alt="image"
src="https://github.com/user-attachments/assets/06279121-69bd-4663-aebd-930cd7c02190"
/>

***adding custom target label selection on edit software modals**

<img width="796" alt="image"
src="https://github.com/user-attachments/assets/c15a8236-1b02-4d17-9245-e24967190eaf"
/>

also includes various small copy changes.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [ ] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-12-17 10:41:57 -06:00

210 lines
5.5 KiB
TypeScript

import React, { ReactNode } from "react";
import { Link } from "react-router";
import classnames from "classnames";
import PATHS from "router/paths";
import { IDropdownOption } from "interfaces/dropdownOption";
import { ILabelSummary } from "interfaces/label";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import Radio from "components/forms/fields/Radio";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import Checkbox from "components/forms/fields/Checkbox";
const baseClass = "target-label-selector";
export const listNamesFromSelectedLabels = (dict: Record<string, boolean>) => {
return Object.entries(dict).reduce((acc, [labelName, isSelected]) => {
if (isSelected) {
acc.push(labelName);
}
return acc;
}, [] as string[]);
};
export const generateLabelKey = (
target: string,
customTargetOption: string,
selectedLabels: Record<string, boolean>
) => {
if (target !== "Custom") {
return {};
}
return {
[customTargetOption]: listNamesFromSelectedLabels(selectedLabels),
};
};
interface ITargetChooserProps {
selectedTarget: string;
onSelect: (val: string) => void;
}
const TargetChooser = ({ selectedTarget, onSelect }: ITargetChooserProps) => {
return (
<div className={`form-field`}>
<div className="form-field__label">Target</div>
<Radio
className={`${baseClass}__radio-input`}
label="All hosts"
id="all-hosts-target-radio-btn"
checked={selectedTarget === "All hosts"}
value="All hosts"
name="target-type"
onChange={onSelect}
/>
<Radio
className={`${baseClass}__radio-input`}
label="Custom"
id="custom-target-radio-btn"
checked={selectedTarget === "Custom"}
value="Custom"
name="target-type"
onChange={onSelect}
/>
</div>
);
};
interface ILabelChooserProps {
isError: boolean;
isLoading: boolean;
labels: ILabelSummary[];
selectedLabels: Record<string, boolean>;
selectedCustomTarget: string;
customTargetOptions: IDropdownOption[];
dropdownHelpText?: ReactNode;
onSelectCustomTarget: (val: string) => void;
onSelectLabel: ({ name, value }: { name: string; value: boolean }) => void;
}
const LabelChooser = ({
isError,
isLoading,
labels,
dropdownHelpText,
selectedLabels,
selectedCustomTarget,
customTargetOptions,
onSelectCustomTarget,
onSelectLabel,
}: ILabelChooserProps) => {
const getHelpText = (value: string) => {
if (dropdownHelpText) return dropdownHelpText;
return customTargetOptions.find((option) => option.value === value)
?.helpText;
};
const renderLabels = () => {
if (isLoading) {
return <Spinner centered={false} />;
}
if (isError) {
return <DataError />;
}
if (!labels.length) {
return (
<div className={`${baseClass}__no-labels`}>
<span>
<Link to={PATHS.LABEL_NEW_DYNAMIC}>Add labels</Link> to target
specific hosts.
</span>
</div>
);
}
return labels.map((label) => {
return (
<div className={`${baseClass}__label`} key={label.name}>
<Checkbox
className={`${baseClass}__checkbox`}
name={label.name}
value={!!selectedLabels[label.name]}
onChange={onSelectLabel}
parseTarget
/>
<div className={`${baseClass}__label-name`}>{label.name}</div>
</div>
);
});
};
return (
<div className={`${baseClass}__custom-label-chooser`}>
<Dropdown
value={selectedCustomTarget}
options={customTargetOptions}
searchable={false}
onChange={onSelectCustomTarget}
/>
<div className={`${baseClass}__description`}>
{getHelpText(selectedCustomTarget)}
</div>
<div className={`${baseClass}__checkboxes`}>{renderLabels()}</div>
</div>
);
};
interface ITargetLabelSelectorProps {
selectedTargetType: string;
selectedCustomTarget: string;
customTargetOptions: IDropdownOption[];
selectedLabels: Record<string, boolean>;
labels: ILabelSummary[];
/** set this prop to show a help text. If it is encluded then it will override
* the selected options defined `helpText`
*/
dropdownHelpText?: ReactNode;
isLoadingLabels?: boolean;
isErrorLabels?: boolean;
className?: string;
onSelectTargetType: (val: string) => void;
onSelectCustomTarget: (val: string) => void;
onSelectLabel: ({ name, value }: { name: string; value: boolean }) => void;
}
const TargetLabelSelector = ({
selectedTargetType,
selectedCustomTarget,
customTargetOptions,
selectedLabels,
dropdownHelpText,
className,
labels,
isLoadingLabels = false,
isErrorLabels = false,
onSelectTargetType,
onSelectCustomTarget,
onSelectLabel,
}: ITargetLabelSelectorProps) => {
const classNames = classnames(baseClass, className);
return (
<div className={classNames}>
<TargetChooser
selectedTarget={selectedTargetType}
onSelect={onSelectTargetType}
/>
{selectedTargetType === "Custom" && (
<LabelChooser
selectedCustomTarget={selectedCustomTarget}
customTargetOptions={customTargetOptions}
isError={isErrorLabels}
isLoading={isLoadingLabels}
labels={labels || []}
selectedLabels={selectedLabels}
dropdownHelpText={dropdownHelpText}
onSelectCustomTarget={onSelectCustomTarget}
onSelectLabel={onSelectLabel}
/>
)}
</div>
);
};
export default TargetLabelSelector;