Skip to content

Commit

Permalink
🐛 Analysis - Update the TaskGroup to id selected rulesets (#2034)
Browse files Browse the repository at this point in the history
Supports: https://issues.redhat.com/browse/MTA-3330

The way custom targets are handled by the analysis wizard has been
updated. There are no visual changes.

At analysis start time, a `Taskgroup` is submitted containing all of the
user's choices. To better support custom targets, the `ruleSet` of any
selected custom targets will no be submitted as part of the
`Taskgroup.TaskData.rules`. Identifying the `ruleSet` will allow the
custom target's rules to be enabled in analysis.

In support of this change:
  - The wizard's form data now stores a `Target[]` instead of a `number[]`
    to make working with the selected targets more convenient across the
    wizard components.
  - Any code working with just the `targetId` has been updated to use the
    full `Target` object
  - The handlers for changes to the target labels on the wizard's options
    step better handle mapping the selected label changes to target card
    selection

---------

Signed-off-by: Scott J Dickerson <[email protected]>
  • Loading branch information
sjd78 authored Aug 2, 2024
1 parent a342f27 commit e920acb
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 63 deletions.
1 change: 1 addition & 0 deletions client/src/app/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export interface TaskData {
included: string[];
excluded: string[];
};
ruleSets?: Ref[]; // Target.ruleset.{ id, name }
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
import {
Application,
New,
Ref,
TaskData,
Taskgroup,
TaskgroupTask,
Expand Down Expand Up @@ -223,6 +224,16 @@ export const AnalysisWizard: React.FC<IAnalysisWizard> = ({
const matchingSourceCredential = identities.find(
(identity) => identity.name === fieldValues.associatedCredentials
);

const ruleSetRefsFromSelectedTargets: Ref[] = fieldValues.selectedTargets
.map(({ ruleset }) => ruleset)
.filter(Boolean)
.map<Ref>(({ id, name }) => ({ id: id ?? 0, name: name ?? "" }));
// TODO: Type `Ruleset` has the id and name as optional/undefined to support
// object creation. At runtime, id and name will always be defined on
// existing objects. In future, update the Ruleset creation code to use
// New<Ruleset> or similar to avoid these issues.

return {
...currentTaskgroup,
tasks: analyzableApplications.map((app: Application) => initTask(app)),
Expand Down Expand Up @@ -286,6 +297,9 @@ export const AnalysisWizard: React.FC<IAnalysisWizard> = ({
name: matchingSourceCredential.name,
},
}),
...(ruleSetRefsFromSelectedTargets.length > 0 && {
ruleSets: ruleSetRefsFromSelectedTargets,
}),
},
},
};
Expand Down
4 changes: 2 additions & 2 deletions client/src/app/pages/applications/analysis-wizard/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IReadFile,
FileLoadError,
TargetLabel,
Target,
} from "@app/api/models";
import { useTranslation } from "react-i18next";
import { useAnalyzableApplicationsByMode } from "./utils";
Expand Down Expand Up @@ -55,7 +56,7 @@ const useModeStepSchema = ({

export interface TargetsStepValues {
formLabels: TargetLabel[];
selectedTargets: number[];
selectedTargets: Target[];
}

const useTargetsStepSchema = (): yup.SchemaOf<TargetsStepValues> => {
Expand Down Expand Up @@ -114,7 +115,6 @@ export const customRulesFilesSchema: yup.SchemaOf<IReadFile> = yup.object({
});

const useCustomRulesStepSchema = (): yup.SchemaOf<CustomRulesStepValues> => {
const { t } = useTranslation();
return yup.object({
rulesKind: yup.string().defined(),
customRulesFiles: yup
Expand Down
32 changes: 9 additions & 23 deletions client/src/app/pages/applications/analysis-wizard/set-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ import { DEFAULT_SELECT_MAX_HEIGHT } from "@app/Constants";
import { useFetchTargets } from "@app/queries/targets";
import defaultSources from "./sources";
import { QuestionCircleIcon } from "@patternfly/react-icons";
import {
findLabelBySelector,
isLabelInFormLabels,
updateSelectedTargetsBasedOnLabels,
} from "./utils";
import { updateSelectedTargetsBasedOnLabels } from "./utils";
import { toggle } from "radash";

export const SetOptions: React.FC = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -124,25 +121,14 @@ export const SetOptions: React.FC = () => {
selections={targetSelections}
isOpen={isSelectTargetsOpen}
onSelect={(_, selection) => {
const selectionWithLabelSelector = `konveyor.io/target=${selection}`;
const matchingLabel = findLabelBySelector(
defaultTargetsAndTargetsLabels,
selectionWithLabelSelector
const selectionLabel = `konveyor.io/target=${selection}`;
const matchingLabel = defaultTargetsAndTargetsLabels.find(
(label) => label.label === selectionLabel
);
let updatedFormLabels = [];
if (
matchingLabel &&
!isLabelInFormLabels(formLabels, matchingLabel.label)
) {
updatedFormLabels = [...formLabels, matchingLabel];
onChange(updatedFormLabels);
} else {
updatedFormLabels = formLabels.filter(
(formLabel) =>
formLabel.label !== selectionWithLabelSelector
);
onChange(updatedFormLabels);
}
const updatedFormLabels = !matchingLabel
? formLabels
: toggle(formLabels, matchingLabel, (tl) => tl.label);
onChange(updatedFormLabels);

const updatedSelectedTargets =
updateSelectedTargetsBasedOnLabels(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useFetchTargets } from "@app/queries/targets";
import { Application, TagCategory, Target } from "@app/api/models";
import { useFetchTagCategories } from "@app/queries/tags";
import { SimpleSelectCheckbox } from "@app/components/SimpleSelectCheckbox";
import { getUpdatedFormLabels, updateSelectedTargets } from "./utils";
import { getUpdatedFormLabels, toggleSelectedTargets } from "./utils";

interface SetTargetsProps {
applications: Application[];
Expand Down Expand Up @@ -103,20 +103,19 @@ export const SetTargets: React.FC<SetTargetsProps> = ({ applications }) => {
selectedLabelName: string,
target: Target
) => {
const updatedSelectedTargets = updateSelectedTargets(
target.id,
const updatedSelectedTargets = toggleSelectedTargets(
target,
selectedTargets
);
setValue("selectedTargets", updatedSelectedTargets);

const updatedFormLabels = getUpdatedFormLabels(
isSelecting,
selectedLabelName,
target,
formLabels
);

setValue("formLabels", updatedFormLabels);
setValue("selectedTargets", updatedSelectedTargets);
};

const allProviders = targets.flatMap((target) => target.provider);
Expand Down Expand Up @@ -176,7 +175,7 @@ export const SetTargets: React.FC<SetTargetsProps> = ({ applications }) => {
<TargetCard
readOnly
item={target}
cardSelected={selectedTargets?.includes(target.id)}
cardSelected={selectedTargets.some(({ id }) => id === target.id)}
onSelectedCardTargetChange={(selectedTarget) => {
handleOnSelectedCardTargetChange(selectedTarget);
}}
Expand Down
72 changes: 40 additions & 32 deletions client/src/app/pages/applications/analysis-wizard/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from "react";
import { Application, Target, TargetLabel } from "@app/api/models";
import { AnalysisMode, ANALYSIS_MODES } from "./schema";
import { toggle, unique } from "radash";
import { getParsedLabel } from "@app/utils/rules-utils";

export const isApplicationBinaryEnabled = (
application: Application
Expand Down Expand Up @@ -61,14 +63,14 @@ export const useAnalyzableApplicationsByMode = (
[applications]
);

export const updateSelectedTargets = (
targetId: number,
selectedTargetIDs: number[]
) => {
const isSelected = selectedTargetIDs.includes(targetId);
return isSelected
? selectedTargetIDs.filter((id) => id !== targetId)
: [...selectedTargetIDs, targetId];
/**
* Toggle the existence of a target within the array and return the array
*/
export const toggleSelectedTargets = (
target: Target,
selectedTargets: Target[]
): Target[] => {
return toggle(selectedTargets, target, (t) => t.id);
};

export const getUpdatedFormLabels = (
Expand Down Expand Up @@ -100,40 +102,46 @@ export const getUpdatedFormLabels = (
return otherSelectedLabels;
}
};
export const findLabelBySelector = (labels: TargetLabel[], selector: string) =>
labels.find((label) => label.label === selector) || "";

export const isLabelInFormLabels = (formLabels: TargetLabel[], label: string) =>
formLabels.some((formLabel) => formLabel.label === label);
/**
* Match a target to a set of target type labels based on if the target supports
* label choice.
*/
const matchTargetToLabels = (target: Target, labels: TargetLabel[]) => {
if (!target.labels?.length) {
return false;
}

export const labelToTargetId = (labelName: string, targets: Target[]) => {
const target = targets.find(
(t) => t.labels?.some((l) => l.name === labelName)
const targetTargetLabelCount = target.labels?.reduce(
(count, tl) =>
getParsedLabel(tl.label).labelType === "target" ? count + 1 : count,
0
);
return target ? target.id : null;

const matches = labels
.map((l) => target.labels?.find((tl) => tl.label === l.label) ?? false)
.filter(Boolean).length;

return target.choice ? matches >= 1 : matches === targetTargetLabelCount;
};

/**
* Given a set of selected labels, return a set of targets where (1) the target's labels
* properly match the select labels or (2) the target is selected but has no labels.
*/
export const updateSelectedTargetsBasedOnLabels = (
currentFormLabels: TargetLabel[],
selectedTargets: number[],
selectedTargets: Target[],
targets: Target[]
) => {
const newSelectedTargets = currentFormLabels.reduce(
(acc: number[], formLabel) => {
const targetId = labelToTargetId(formLabel.name, targets);
if (targetId && !acc.includes(targetId)) {
acc.push(targetId);
}
return acc;
},
[]
): Target[] => {
const targetsFromLabels = unique(
targets.filter((target) => matchTargetToLabels(target, currentFormLabels)),
(target) => target.id
);

const filteredSelectedTargets = selectedTargets.filter((targetId) =>
currentFormLabels.some(
(formLabel) => labelToTargetId(formLabel.name, targets) === targetId
)
const selectedTargetsWithNoLabel = selectedTargets.filter(
(target) => (target.labels?.length ?? 0) === 0
);

return [...new Set([...newSelectedTargets, ...filteredSelectedTargets])];
return [...targetsFromLabels, ...selectedTargetsWithNoLabel];
};

0 comments on commit e920acb

Please sign in to comment.