Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI for scoping software to fleet apps and custom packages via labels #24793

Merged
merged 15 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/24538-24542-UI-for-scope-software-via-labels
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add UI for scoping software via labels
5 changes: 3 additions & 2 deletions frontend/components/PlatformSelector/PlatformSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const PlatformSelector = ({

return (
<div className={`${parentClass}__${baseClass} ${baseClass} form-field`}>
<span className={labelClasses}>Checks on:</span>
<span className={labelClasses}>Targets:</span>
<span className={`${baseClass}__checkboxes`}>
<Checkbox
value={checkDarwin}
Expand Down Expand Up @@ -71,7 +71,8 @@ export const PlatformSelector = ({
</Checkbox>
</span>
<div className="form-field__help-text">
Your policy will only be checked on the selected platform(s).
To apply the profile to new hosts, you&apos;ll have to delete it and
upload a new profile.
</div>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions frontend/components/PlatformSelector/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
.form-field__label--disabled {
color: $ui-fleet-black-50;
}

&__platform-checkbox-wrapper {
width: auto;
}
}
210 changes: 210 additions & 0 deletions frontend/components/TargetLabelSelector/TargetLabelSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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;
57 changes: 57 additions & 0 deletions frontend/components/TargetLabelSelector/_styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.target-label-selector {
font-size: $x-small;

&__custom-label-chooser {
margin-top: $pad-medium;
}

&__description {
margin: $pad-medium 0;
}

&__no-labels {
display: flex;
height: 187px;
flex-direction: column;
align-items: center;
gap: $pad-small;
justify-content: center;

span {
color: $ui-fleet-black-75;
}
}

&__checkboxes {
display: flex;
max-height: 187px;
flex-direction: column;
border-radius: $border-radius;
border: 1px solid $ui-fleet-black-10;
overflow-y: auto;

.loading-spinner {
margin: 69.5px auto;
}
}

&__label {
width: 100%;
padding: $pad-small $pad-medium;
box-sizing: border-box;
display: flex;
align-items: center;

&:not(:last-child) {
border-bottom: 1px solid $ui-fleet-black-10;
}

.form-field--checkbox {
width: auto;
}
}

&__label-name {
padding-left: $pad-large;
}
}
1 change: 1 addition & 0 deletions frontend/components/TargetLabelSelector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./TargetLabelSelector";
3 changes: 3 additions & 0 deletions frontend/interfaces/software.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from "prop-types";
import { IconNames } from "components/icons";

import vulnerabilityInterface from "./vulnerability";
import { ILabelSummary } from "./label";

export default PropTypes.shape({
type: PropTypes.string,
Expand Down Expand Up @@ -126,6 +127,8 @@ export interface ISoftwareTitleDetails {
bundle_identifier?: string;
browser?: BrowserType;
versions_count?: number;
labels_include_any?: ILabelSummary[];
labels_exclude_any?: ILabelSummary[];
}

export interface ISoftwareVulnerability {
Expand Down
Loading
Loading