Skip to content

Commit

Permalink
✨ [AnalysisWizard] Language discovery changes (#1951)
Browse files Browse the repository at this point in the history
Resolves #1950

UI Tests PR: 1136

Needs: konveyor/tackle-ui-tests#1136

Includes: 
- Provide a "(Show All)" option to display all targets regardless of
language tags. Adds a new component to handle this new menu type.
- Populate the options list from the Provider field of the fetched
targets.
- Initially select the languages based on the tags, of tag category
"Language", across the applications selected for analysis.
- Update TS type for Target to reflect the provider type changing to a
string[].
- Covers the hub change to the task model described here:
>The application Analysis fields used to correlated to task by addon.
This needs to be updated to correlate by kind == "analyzer" (when not
blank) else fallback on addon == "analyzer". The fallback is needed to
correlate tasks created in previous releases.
- Each target card should always display a label with it's provider.
This will allow cards that belong to different providers to be
differentiated when more than one language is selected.


<img width="1360" alt="Screenshot 2024-06-12 at 3 37 47 PM"
src="https://github.com/konveyor/tackle2-ui/assets/11218376/085de6c1-3b56-4532-ab38-81732da377e6">

---------

Signed-off-by: Ian Bolton <ibolton@redhat.com>
Co-authored-by: Radoslaw Szwajkowski <rszwajko@redhat.com>
  • Loading branch information
ibolton336 and rszwajko committed Jun 20, 2024
1 parent 0cce0d8 commit ea2005b
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 37 deletions.
121 changes: 121 additions & 0 deletions client/src/app/components/SimpleSelectCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from "react";
import {
Select,
SelectOption,
SelectList,
MenuToggle,
Badge,
SelectOptionProps,
MenuToggleElement,
} from "@patternfly/react-core";
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";

export interface ISimpleSelectBasicProps {
onChange: (selection: string | string[]) => void;
options: SelectOptionProps[];
value?: string[];
placeholderText?: string;
id?: string;
toggleId?: string;
toggleAriaLabel?: string;
selectMultiple?: boolean;
width?: number;
noResultsFoundText?: string;
hideClearButton?: false;
}

export const SimpleSelectCheckbox: React.FC<ISimpleSelectBasicProps> = ({
onChange,
options,
value,
placeholderText = "Select...",
id,
toggleId,
toggleAriaLabel,
width,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(
[{ value: "show-all", label: "Show All", children: "Show All" }, ...options]
);

React.useEffect(() => {
const updatedOptions = [
{ value: "show-all", label: "Show All", children: "Show All" },
...options,
];
setSelectOptions(updatedOptions);
}, [options]);

const onToggleClick = () => {
setIsOpen(!isOpen);
};

const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
selectionValue: string | number | undefined
) => {
if (!value || !selectionValue) {
return;
}
let newValue: string[] = [];
if (selectionValue === "show-all") {
newValue =
value.length === options.length ? [] : options.map((opt) => opt.value);
} else {
if (value.includes(selectionValue as string)) {
newValue = value.filter((item) => item !== selectionValue);
} else {
newValue = [...value, selectionValue as string];
}
}
onChange(newValue);
};

return (
<Select
role="menu"
id={id}
isOpen={isOpen}
selected={value}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={(toggleref: React.Ref<MenuToggleElement>) => (
<MenuToggle
aria-label={toggleAriaLabel}
id={toggleId}
ref={toggleref}
onClick={onToggleClick}
style={{ width: width && width + "px" }}
isExpanded={isOpen}
>
<span className={spacing.mrSm}>{placeholderText}</span>
{value && value.length > 0 && <Badge isRead>{value.length}</Badge>}
</MenuToggle>
)}
aria-label={toggleAriaLabel}
>
<SelectList>
{selectOptions.map((option, index) => (
<SelectOption
id={`checkbox-for-${option.value}`}
hasCheckbox
key={option.value}
isFocused={index === 0}
onClick={(e) => {
onSelect(e, option.value);
}}
isSelected={
option.value === "show-all"
? value?.length === options.length
: value?.includes(option.value as string)
}
{...option}
>
{option.children || option.value}
</SelectOption>
))}
</SelectList>
</Select>
);
};
30 changes: 21 additions & 9 deletions client/src/app/components/target-card/target-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
SelectVariant,
SelectOptionObject,
} from "@patternfly/react-core/deprecated";
import { GripVerticalIcon } from "@patternfly/react-icons";
import { GripVerticalIcon, InfoCircleIcon } from "@patternfly/react-icons";
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { useTranslation } from "react-i18next";

Expand Down Expand Up @@ -85,14 +85,16 @@ export const TargetCard: React.FC<TargetCardProps> = ({
);

const handleCardClick = (event: React.MouseEvent) => {
// Stop 'select' event propagation
const eventTarget: any = event.target;
if (eventTarget.type === "button") return;
const eventTarget = event.target as HTMLElement;

if (eventTarget.tagName === "BUTTON" || eventTarget.tagName === "LABEL") {
event.preventDefault();
}

setCardSelected(!isCardSelected);
onCardClick &&
selectedLabelName &&
if (onCardClick && selectedLabelName) {
onCardClick(!isCardSelected, selectedLabelName, target);
}
};

const handleLabelSelection = (
Expand All @@ -106,20 +108,30 @@ export const TargetCard: React.FC<TargetCardProps> = ({
onSelectedCardTargetChange(selection as string);
}
};

return (
<Card
id={`target-card-${target.name.replace(/\s/g, "-")}`}
onClick={handleCardClick}
isSelectable={!!cardSelected}
isSelected={isCardSelected}
className="pf-v5-l-stack pf-v5-l-stack__item pf-m-fill"
>
<CardHeader
checked={isCardSelected}
selectableActions={{
selectableActionId: "" + target.id,
selectableActionId: "target-name-" + target.name,
selectableActionAriaLabelledby: `${target.name}-selectable-action-label`,
isChecked: isCardSelected,
}}
/>
>
<Label
id={`${target.provider}-selectable-action-label`}
variant="outline"
icon={<InfoCircleIcon />}
>
{target.provider}
</Label>
</CardHeader>
<CardBody>
<Flex>
<FlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export const AnalysisWizard: React.FC<IAnalysisWizard> = ({
isDisabled={!isStepEnabled(StepId.SetTargets)}
footer={{ isNextDisabled: !isStepEnabled(StepId.SetTargets + 1) }}
>
<SetTargets />
<SetTargets applications={applications} />
</WizardStep>,
<WizardStep
key={StepId.Scope}
Expand Down
83 changes: 57 additions & 26 deletions client/src/app/pages/applications/analysis-wizard/set-targets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
GalleryItem,
Form,
Alert,
SelectOptionProps,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
Expand All @@ -15,24 +16,53 @@ import { TargetCard } from "@app/components/target-card/target-card";
import { AnalysisWizardFormValues } from "./schema";
import { useSetting } from "@app/queries/settings";
import { useFetchTargets } from "@app/queries/targets";
import { Target } from "@app/api/models";
import { SimpleSelectTypeahead } from "@app/components/SimpleSelectTypeahead";

export const SetTargets: React.FC = () => {
import { Application, TagCategory, Target } from "@app/api/models";
import { useFetchTagCategories } from "@app/queries/tags";
import { SimpleSelectCheckbox } from "@app/components/SimpleSelectCheckbox";
interface SetTargetsProps {
applications: Application[];
}

export const SetTargets: React.FC<SetTargetsProps> = ({ applications }) => {
const { t } = useTranslation();

const { targets } = useFetchTargets();

const [provider, setProvider] = useState("Java");

const targetOrderSetting = useSetting("ui.target.order");

const { watch, setValue, getValues } =
useFormContext<AnalysisWizardFormValues>();

const values = getValues();
const formLabels = watch("formLabels");
const selectedTargets = watch("selectedTargets");

const { tagCategories, isFetching, fetchError } = useFetchTagCategories();

const findCategoryForTag = (tagId: number) => {
return tagCategories.find(
(category: TagCategory) =>
category.tags?.some((categoryTag) => categoryTag.id === tagId)
);
};

const initialProviders = Array.from(
new Set(
applications
.flatMap((app) => app.tags || [])
.map((tag) => {
return {
category: findCategoryForTag(tag.id),
tag,
};
})
.filter((tagWithCat) => tagWithCat?.category?.name === "Language")
.map((tagWithCat) => tagWithCat.tag.name)
)
).filter(Boolean);

const [providers, setProviders] = useState(initialProviders);

const handleOnSelectedCardTargetChange = (selectedLabelName: string) => {
const otherSelectedLabels = formLabels?.filter((formLabel) => {
return formLabel.name !== selectedLabelName;
Expand Down Expand Up @@ -124,6 +154,10 @@ export const SetTargets: React.FC = () => {
}
};

const allProviders = targets.flatMap((target) => target.provider);

const languageOptions = Array.from(new Set(allProviders));

return (
<Form
onSubmit={(event) => {
Expand All @@ -136,26 +170,21 @@ export const SetTargets: React.FC = () => {
</Title>
<Text>{t("wizard.label.setTargets")}</Text>
</TextContent>
<SimpleSelectTypeahead
width={200}
value={provider}
toggleAriaLabel="Action select dropdown toggle"
toggleId="action-select-toggle"
hideClearButton
id="action-select"
options={[
{
value: "Java",
children: "Java",
},
{
value: "Go",
children: "Go",
},
]}
<SimpleSelectCheckbox
placeholderText="Filter by language..."
width={300}
value={providers}
options={languageOptions?.map((language): SelectOptionProps => {
return {
children: <div>{language}</div>,

value: language,
};
})}
onChange={(selection) => {
setProvider(selection as string);
setProviders(selection as string[]);
}}
toggleId="action-select-toggle"
/>
{values.selectedTargets.length === 0 &&
values.customRulesFiles.length === 0 &&
Expand All @@ -170,10 +199,12 @@ export const SetTargets: React.FC = () => {
{targetOrderSetting.isSuccess
? targetOrderSetting.data.map((id, index) => {
const matchingTarget = targets.find((target) => target.id === id);

const isSelected = selectedTargets?.includes(id);

if (matchingTarget && matchingTarget.provider === provider) {
if (
matchingTarget &&
providers?.some((p) => matchingTarget?.provider?.includes(p))
) {
return (
<GalleryItem key={index}>
<TargetCard
Expand Down
5 changes: 4 additions & 1 deletion client/src/app/pages/migration-targets/migration-targets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ export const MigrationTargets: React.FC = () => {
const matchingTarget = targets.find(
(target) => target.id === id
);
if (matchingTarget && matchingTarget.provider === provider) {
if (
matchingTarget &&
matchingTarget.provider?.includes(provider)
) {
return (
<SortableItem
key={id}
Expand Down

0 comments on commit ea2005b

Please sign in to comment.