Skip to content

Commit 582c39e

Browse files
committed
Add language options from providers & select by app tags
Move to simple multi select menu in set targets step Drive target selection options from targets list Address missing checkbox on deselect Add provider to target card header Signed-off-by: Ian Bolton <[email protected]>
1 parent c2f4f06 commit 582c39e

File tree

7 files changed

+206
-32
lines changed

7 files changed

+206
-32
lines changed

client/src/app/api/models.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ export interface Target {
425425
labels?: TargetLabel[];
426426
image?: RulesetImage;
427427
ruleset: Ruleset;
428-
provider?: string;
428+
provider?: string[];
429429
}
430430

431431
export interface Metadata {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from "react";
2+
import {
3+
Select,
4+
SelectOption,
5+
SelectList,
6+
MenuToggle,
7+
Badge,
8+
SelectOptionProps,
9+
MenuToggleElement,
10+
} from "@patternfly/react-core";
11+
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
12+
13+
export interface ISimpleSelectBasicProps {
14+
onChange: (selection: string | string[]) => void;
15+
options: SelectOptionProps[];
16+
value?: string[];
17+
placeholderText?: string;
18+
id?: string;
19+
toggleId?: string;
20+
toggleAriaLabel?: string;
21+
selectMultiple?: boolean;
22+
width?: number;
23+
noResultsFoundText?: string;
24+
hideClearButton?: false;
25+
}
26+
27+
export const SimpleSelectCheckbox: React.FC<ISimpleSelectBasicProps> = ({
28+
onChange,
29+
options,
30+
value,
31+
placeholderText = "Select...",
32+
id,
33+
toggleId,
34+
toggleAriaLabel,
35+
width,
36+
}) => {
37+
const [isOpen, setIsOpen] = React.useState(false);
38+
const [selectedItems, setSelectedItems] = React.useState<string[]>([]);
39+
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(
40+
[{ value: "show-all", label: "Show All", children: "Show All" }, ...options]
41+
);
42+
43+
React.useEffect(() => {
44+
setSelectedItems(value || []);
45+
}, [value]);
46+
47+
React.useEffect(() => {
48+
const updatedOptions = [
49+
{ value: "show-all", label: "Show All", children: "Show All" },
50+
...options,
51+
];
52+
setSelectOptions(updatedOptions);
53+
}, [options]);
54+
55+
const onToggleClick = () => {
56+
setIsOpen(!isOpen);
57+
};
58+
59+
const onSelect = (
60+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
61+
selectionValue: string | number | undefined
62+
) => {
63+
const value = selectionValue as string;
64+
if (value === "show-all") {
65+
if (selectedItems.length === options.length) {
66+
setSelectedItems([]);
67+
onChange([]);
68+
} else {
69+
const allItemValues = options.map((option) => option.value as string);
70+
setSelectedItems(allItemValues);
71+
onChange(allItemValues);
72+
}
73+
} else {
74+
if (selectedItems.includes(value)) {
75+
const newSelections = selectedItems.filter((item) => item !== value);
76+
setSelectedItems(newSelections);
77+
onChange(newSelections);
78+
} else {
79+
const newSelections = [...selectedItems, value];
80+
setSelectedItems(newSelections);
81+
onChange(newSelections);
82+
}
83+
}
84+
};
85+
86+
return (
87+
<Select
88+
role="menu"
89+
id={id}
90+
isOpen={isOpen}
91+
selected={selectedItems}
92+
onSelect={onSelect}
93+
onOpenChange={setIsOpen}
94+
toggle={(toggleref: React.Ref<MenuToggleElement>) => (
95+
<MenuToggle
96+
ref={toggleref}
97+
onClick={onToggleClick}
98+
style={{ width: width && width + "px" }}
99+
isExpanded={isOpen}
100+
id={toggleId}
101+
>
102+
<span className={spacing.mrSm}>{placeholderText}</span>
103+
{selectedItems.length > 0 && (
104+
<Badge isRead>{selectedItems.length}</Badge>
105+
)}
106+
</MenuToggle>
107+
)}
108+
aria-label={toggleAriaLabel}
109+
>
110+
<SelectList>
111+
{selectOptions.map((option, index) => (
112+
<SelectOption
113+
hasCheckbox
114+
key={option.value}
115+
isFocused={index === 0}
116+
onClick={() => onSelect(undefined, option.value)}
117+
isSelected={
118+
option.value === "show-all"
119+
? selectedItems.length === options.length
120+
: selectedItems.includes(option.value as string)
121+
}
122+
{...option}
123+
>
124+
{option.children || option.value}
125+
</SelectOption>
126+
))}
127+
</SelectList>
128+
</Select>
129+
);
130+
};

client/src/app/components/target-card/target-card.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
SelectVariant,
2626
SelectOptionObject,
2727
} from "@patternfly/react-core/deprecated";
28-
import { GripVerticalIcon } from "@patternfly/react-icons";
28+
import { GripVerticalIcon, InfoCircleIcon } from "@patternfly/react-icons";
2929
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
3030
import { useTranslation } from "react-i18next";
3131

@@ -86,6 +86,7 @@ export const TargetCard: React.FC<TargetCardProps> = ({
8686

8787
const handleCardClick = (event: React.MouseEvent) => {
8888
// Stop 'select' event propagation
89+
event.preventDefault();
8990
const eventTarget: any = event.target;
9091
if (eventTarget.type === "button") return;
9192

@@ -110,16 +111,25 @@ export const TargetCard: React.FC<TargetCardProps> = ({
110111
return (
111112
<Card
112113
onClick={handleCardClick}
113-
isSelectable={!!cardSelected}
114+
isSelectable
114115
isSelected={isCardSelected}
115116
className="pf-v5-l-stack pf-v5-l-stack__item pf-m-fill"
116117
>
117118
<CardHeader
118119
selectableActions={{
119120
selectableActionId: "" + target.id,
121+
selectableActionAriaLabelledby: `${target.name}-selectable-action-label`,
120122
isChecked: isCardSelected,
121123
}}
122-
/>
124+
>
125+
<Label
126+
id={`${target.provider}-selectable-action-label`}
127+
variant="outline"
128+
icon={<InfoCircleIcon />}
129+
>
130+
{target.provider}
131+
</Label>
132+
</CardHeader>
123133
<CardBody>
124134
<Flex>
125135
<FlexItem>

client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export const AnalysisWizard: React.FC<IAnalysisWizard> = ({
359359
isDisabled={!isStepEnabled(StepId.SetTargets)}
360360
footer={{ isNextDisabled: !isStepEnabled(StepId.SetTargets + 1) }}
361361
>
362-
<SetTargets />
362+
<SetTargets applications={applications} />
363363
</WizardStep>,
364364
<WizardStep
365365
key={StepId.Scope}

client/src/app/pages/applications/analysis-wizard/set-targets.tsx

+56-25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GalleryItem,
88
Form,
99
Alert,
10+
SelectOptionProps,
1011
} from "@patternfly/react-core";
1112
import { useTranslation } from "react-i18next";
1213
import { useFormContext } from "react-hook-form";
@@ -15,24 +16,53 @@ import { TargetCard } from "@app/components/target-card/target-card";
1516
import { AnalysisWizardFormValues } from "./schema";
1617
import { useSetting } from "@app/queries/settings";
1718
import { useFetchTargets } from "@app/queries/targets";
18-
import { Target } from "@app/api/models";
19-
import { SimpleSelectTypeahead } from "@app/components/SimpleSelectTypeahead";
20-
21-
export const SetTargets: React.FC = () => {
19+
import { Application, TagCategory, Target } from "@app/api/models";
20+
import { useFetchTagCategories } from "@app/queries/tags";
21+
import { SimpleSelectCheckbox } from "@app/components/SimpleSelectCheckbox";
22+
interface SetTargetsProps {
23+
applications: Application[];
24+
}
25+
26+
export const SetTargets: React.FC<SetTargetsProps> = ({ applications }) => {
2227
const { t } = useTranslation();
2328

2429
const { targets } = useFetchTargets();
2530

26-
const [provider, setProvider] = useState("Java");
27-
2831
const targetOrderSetting = useSetting("ui.target.order");
2932

3033
const { watch, setValue, getValues } =
3134
useFormContext<AnalysisWizardFormValues>();
35+
3236
const values = getValues();
3337
const formLabels = watch("formLabels");
3438
const selectedTargets = watch("selectedTargets");
3539

40+
const { tagCategories, isFetching, fetchError } = useFetchTagCategories();
41+
42+
const findCategoryForTag = (tagId: number) => {
43+
return tagCategories.find(
44+
(category: TagCategory) =>
45+
category.tags?.some((categoryTag) => categoryTag.id === tagId)
46+
);
47+
};
48+
49+
const initialProviders = Array.from(
50+
new Set(
51+
applications
52+
.flatMap((app) => app.tags || [])
53+
.map((tag) => {
54+
return {
55+
category: findCategoryForTag(tag.id),
56+
tag,
57+
};
58+
})
59+
.filter((tagWithCat) => tagWithCat?.category?.name === "Language")
60+
.map((tagWithCat) => tagWithCat.tag.name)
61+
)
62+
).filter(Boolean);
63+
64+
const [provider, setProvider] = useState(initialProviders);
65+
3666
const handleOnSelectedCardTargetChange = (selectedLabelName: string) => {
3767
const otherSelectedLabels = formLabels?.filter((formLabel) => {
3868
return formLabel.name !== selectedLabelName;
@@ -124,6 +154,10 @@ export const SetTargets: React.FC = () => {
124154
}
125155
};
126156

157+
const allProviders = targets.flatMap((target) => target.provider);
158+
159+
const languageOptions = Array.from(new Set(allProviders));
160+
127161
return (
128162
<Form
129163
onSubmit={(event) => {
@@ -136,26 +170,21 @@ export const SetTargets: React.FC = () => {
136170
</Title>
137171
<Text>{t("wizard.label.setTargets")}</Text>
138172
</TextContent>
139-
<SimpleSelectTypeahead
140-
width={200}
173+
<SimpleSelectCheckbox
174+
placeholderText="Filter by language..."
175+
width={300}
141176
value={provider}
142-
toggleAriaLabel="Action select dropdown toggle"
143-
toggleId="action-select-toggle"
144-
hideClearButton
145-
id="action-select"
146-
options={[
147-
{
148-
value: "Java",
149-
children: "Java",
150-
},
151-
{
152-
value: "Go",
153-
children: "Go",
154-
},
155-
]}
177+
options={languageOptions?.map((language): SelectOptionProps => {
178+
return {
179+
children: <div>{language}</div>,
180+
181+
value: language,
182+
};
183+
})}
156184
onChange={(selection) => {
157-
setProvider(selection as string);
185+
setProvider(selection as string[]);
158186
}}
187+
toggleId="language-select-toggle"
159188
/>
160189
{values.selectedTargets.length === 0 &&
161190
values.customRulesFiles.length === 0 &&
@@ -172,8 +201,10 @@ export const SetTargets: React.FC = () => {
172201
const matchingTarget = targets.find((target) => target.id === id);
173202

174203
const isSelected = selectedTargets?.includes(id);
175-
176-
if (matchingTarget && matchingTarget.provider === provider) {
204+
if (
205+
matchingTarget &&
206+
provider?.some((p) => matchingTarget?.provider?.includes(p))
207+
) {
177208
return (
178209
<GalleryItem key={index}>
179210
<TargetCard

client/src/app/pages/migration-targets/components/custom-target-form.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
307307
},
308308
}),
309309
},
310-
provider: providerType || "Java",
310+
provider: [providerType] || ["Java"],
311311
};
312312

313313
if (target) {

client/src/app/pages/migration-targets/migration-targets.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ export const MigrationTargets: React.FC = () => {
235235
const matchingTarget = targets.find(
236236
(target) => target.id === id
237237
);
238-
if (matchingTarget && matchingTarget.provider === provider) {
238+
if (
239+
matchingTarget &&
240+
matchingTarget.provider?.includes(provider)
241+
) {
239242
return (
240243
<SortableItem
241244
key={id}

0 commit comments

Comments
 (0)