Skip to content

Commit

Permalink
Add language options from providers & select by app tags
Browse files Browse the repository at this point in the history
Move to simple multi select menu in set targets step

Drive target selection options from targets list

Add kind support for tasks

Address missing checkbox on deselect

Add provider to target card header

Signed-off-by: Ian Bolton <[email protected]>
  • Loading branch information
ibolton336 committed Jun 12, 2024
1 parent 460fded commit 9a17217
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 37 deletions.
8 changes: 5 additions & 3 deletions client/src/app/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ export interface Task {
createTime?: string;
application: { id: number };
name: string;
addon: string;
kind?: string;
addon?: string;
data: TaskData;
error?: string;
image?: string;
Expand Down Expand Up @@ -377,7 +378,8 @@ export interface TaskgroupTask {
export interface Taskgroup {
id?: number;
name: string;
addon: string;
kind?: string;
addon?: string;
data: TaskData;
tasks: TaskgroupTask[];
}
Expand Down Expand Up @@ -425,7 +427,7 @@ export interface Target {
labels?: TargetLabel[];
image?: RulesetImage;
ruleset: Ruleset;
provider?: string;
provider?: string[];
}

export interface Metadata {
Expand Down
133 changes: 133 additions & 0 deletions client/src/app/components/SimpleSelectCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 [selectedItems, setSelectedItems] = React.useState<string[]>([]);
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(
[
{ value: "select-all", label: "Select All", children: "Select All" },
...options,
]
);

React.useEffect(() => {
setSelectedItems(value || []);
}, [value]);

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

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

const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
selectionValue: string | number | undefined
) => {
const value = selectionValue as string;
if (value === "select-all") {
if (selectedItems.length === options.length) {
setSelectedItems([]);
onChange([]);
} else {
const allItemValues = options.map((option) => option.value as string);
setSelectedItems(allItemValues);
onChange(allItemValues);
}
} else {
if (selectedItems.includes(value)) {
const newSelections = selectedItems.filter((item) => item !== value);
setSelectedItems(newSelections);
onChange(newSelections);
} else {
const newSelections = [...selectedItems, value];
setSelectedItems(newSelections);
onChange(newSelections);
}
}
};

return (
<Select
role="menu"
id={id}
isOpen={isOpen}
selected={selectedItems}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={(toggleref: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleref}
onClick={onToggleClick}
style={{ width: width && width + "px" }}
isExpanded={isOpen}
id={toggleId}
>
<span className={spacing.mrSm}>{placeholderText}</span>
{selectedItems.length > 0 && (
<Badge isRead>{selectedItems.length}</Badge>
)}
</MenuToggle>
)}
aria-label={toggleAriaLabel}
>
<SelectList>
{selectOptions.map((option, index) => (
<SelectOption
hasCheckbox
key={option.value}
isFocused={index === 0}
onClick={() => onSelect(undefined, option.value)}
isSelected={
option.value === "select-all"
? selectedItems.length === options.length
: selectedItems.includes(option.value as string)
}
{...option}
>
{option.children || option.value}
</SelectOption>
))}
</SelectList>
</Select>
);
};
16 changes: 13 additions & 3 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 @@ -86,6 +86,7 @@ export const TargetCard: React.FC<TargetCardProps> = ({

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

Expand All @@ -110,16 +111,25 @@ export const TargetCard: React.FC<TargetCardProps> = ({
return (
<Card
onClick={handleCardClick}
isSelectable={!!cardSelected}
isSelectable
isSelected={isCardSelected}
className="pf-v5-l-stack pf-v5-l-stack__item pf-m-fill"
>
<CardHeader
selectableActions={{
selectableActionId: "" + target.id,
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 @@ -72,7 +72,7 @@ const defaultTaskData: TaskData = {

export const defaultTaskgroup: Taskgroup = {
name: `taskgroup.analyzer`,
addon: "analyzer",
kind: "analyzer",
data: {
...defaultTaskData,
},
Expand Down 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
81 changes: 56 additions & 25 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((name) => name !== undefined);

const [provider, setProvider] = 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}
<SimpleSelectCheckbox
placeholderText="Select a language..."
width={300}
value={provider}
toggleAriaLabel="Action select dropdown toggle"
toggleId="action-select-toggle"
hideClearButton
id="action-select"
options={[
{
value: "Java",
children: "Java",
},
{
value: "Go",
children: "Go",
},
]}
options={languageOptions?.map((language): SelectOptionProps => {
return {
children: <div>{language}</div>,

value: language,
};
})}
onChange={(selection) => {
setProvider(selection as string);
setProvider(selection as string[]);
}}
toggleId="language-select-toggle"
/>
{values.selectedTargets.length === 0 &&
values.customRulesFiles.length === 0 &&
Expand All @@ -172,8 +201,10 @@ export const SetTargets: React.FC = () => {
const matchingTarget = targets.find((target) => target.id === id);

const isSelected = selectedTargets?.includes(id);

if (matchingTarget && matchingTarget.provider === provider) {
if (
matchingTarget &&
provider?.some((p) => matchingTarget?.provider?.includes(p))
) {
return (
<GalleryItem key={index}>
<TargetCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const ApplicationsTable: React.FC = () => {
tasks.find((task: Task) => task.application?.id === application.id);

const { tasks, hasActiveTasks } = useFetchTasks(
{ addon: "analyzer" },
{ kind: "analyzer", addon: "analyzer" },
isAnalyzeModalOpen
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
},
}),
},
provider: providerType || "Java",
provider: [providerType] || ["Java"],
};

if (target) {
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
Loading

0 comments on commit 9a17217

Please sign in to comment.