diff --git a/client/package.json b/client/package.json index cd10e7433..ec98f0b3c 100644 --- a/client/package.json +++ b/client/package.json @@ -44,7 +44,6 @@ "react-dom": "^17.0.2", "react-hook-form": "^7.43.1", "react-i18next": "^11.8.5", - "react-image-file-resizer": "^0.4.8", "react-markdown": "^8.0.7", "react-measure": "^2.5.2", "react-monaco-editor": "0.51.0", diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 11ee14af4..e8fb60568 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -341,7 +341,7 @@ export type SettingTypes = { "mvn.insecure.enabled": boolean; "review.assessment.required": boolean; "svn.insecure.enabled": boolean; - "ui.ruleset.order": number[]; + "ui.target.order": number[]; }; export type Setting = { @@ -404,7 +404,6 @@ export interface TaskData { tags: { excluded: string[]; }; - rulesets: Ref[]; repository?: Repository; identity?: Ref; labels: { @@ -463,18 +462,29 @@ export enum RulesetKind { } export interface Ruleset { - createTime?: string; - createUser?: string; - description?: string; id?: number; - image?: RulesetImage; kind?: RulesetKind; - name: string; + name?: string; + description?: string; rules: Rule[]; - custom?: boolean; repository?: Repository; identity?: Ref; } +export interface TargetLabel { + name: string; + label: string; +} +export interface Target { + id?: number; + name: string; + description?: string; + choice?: boolean; + custom?: boolean; + labels?: TargetLabel[]; + image?: RulesetImage; + ruleset: Ruleset; +} + export interface Metadata { target: string; source?: string; @@ -688,3 +698,9 @@ export type Fact = { //TODO: Address this when moving to structured facts api data: any; }; + +export type HubFile = { + id: number; + name: string; + path: string; +}; diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index ede6c4420..5f0bd531d 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -29,7 +29,6 @@ import { JobFunction, Proxy, Review, - Ruleset, Setting, SettingTypes, Stakeholder, @@ -49,6 +48,8 @@ import { AnalysisAppDependency, AnalysisAppReport, Rule, + Target, + HubFile, } from "./models"; import { QueryKey } from "@tanstack/react-query"; import { serializeRequestParamsForHub } from "@app/shared/hooks/table-controls"; @@ -84,7 +85,7 @@ export const TRACKER_PROJECT_ISSUETYPES = "issuetypes"; export const TICKETS = HUB + "/tickets"; export const FACTS = HUB + "/facts"; -export const RULESETS = HUB + "/rulesets"; +export const TARGETS = HUB + "/targets"; export const FILES = HUB + "/files"; export const CACHE = HUB + "/cache/m2"; @@ -440,20 +441,17 @@ export const deleteAllMigrationWaves = ( .catch((error) => error); }; -export const updateRuleset = (obj: Ruleset): Promise => - axios.put(`${RULESETS}/${obj.id}`, obj); +export const updateTarget = (obj: Target): Promise => + axios.put(`${TARGETS}/${obj.id}`, obj); -export const createRuleset = (obj: Ruleset): Promise => - axios.post(RULESETS, obj); +export const createTarget = (obj: Target): Promise => + axios.post(TARGETS, obj); -export const deleteRuleset = (id: number): Promise => - axios.delete(`${RULESETS}/${id}`); +export const deleteTarget = (id: number): Promise => + axios.delete(`${TARGETS}/${id}`); -export const getRulesets = (): Promise => - axios.get(RULESETS).then((response) => response.data); - -export const getFileByID = (id: number): Promise => - axios.get(FILES).then((response) => response.data); +export const getTargets = (): Promise => + axios.get(TARGETS).then((response) => response.data); export const createFile = ({ formData, @@ -463,7 +461,7 @@ export const createFile = ({ file: IReadFile; }) => axios - .post(`${FILES}/${file.fileName}`, formData, fileHeaders) + .post(`${FILES}/${file.fileName}`, formData, fileHeaders) .then((response) => { return response.data; }); diff --git a/client/src/app/common/CustomRules/rules-utils.tsx b/client/src/app/common/CustomRules/rules-utils.tsx index 9c5858a5e..a04dc8858 100644 --- a/client/src/app/common/CustomRules/rules-utils.tsx +++ b/client/src/app/common/CustomRules/rules-utils.tsx @@ -1,4 +1,4 @@ -import { IReadFile, ParsedRule, Rule, Ruleset } from "@app/api/models"; +import { IReadFile, ParsedRule } from "@app/api/models"; import yaml from "js-yaml"; type RuleFileType = "YAML" | "XML" | null; @@ -136,9 +136,3 @@ export const getLabels = (labels: string[]) => }, { sourceLabel: "", targetLabel: "", otherLabels: [], allLabels: [] } ); - -export const getRulesetTargetList = (ruleset: Ruleset) => { - return ruleset.rules.reduce((acc: string[], rule) => { - return [...acc, rule?.metadata?.target || ""]; - }, []); -}; diff --git a/client/src/app/common/CustomRules/useRuleFiles.tsx b/client/src/app/common/CustomRules/useRuleFiles.tsx index 0a1b67663..0f61cab9f 100644 --- a/client/src/app/common/CustomRules/useRuleFiles.tsx +++ b/client/src/app/common/CustomRules/useRuleFiles.tsx @@ -4,7 +4,7 @@ import { NotificationsContext } from "@app/shared/notifications-context"; import { AxiosError } from "axios"; import { useUploadFileMutation } from "@app/queries/taskgroups"; import { getAxiosErrorMessage } from "@app/utils/utils"; -import { useCreateFileMutation } from "@app/queries/rulesets"; +import { useCreateFileMutation } from "@app/queries/targets"; import { CustomTargetFormValues } from "@app/pages/migration-targets/components/custom-target-form"; import { UseFormReturn } from "react-hook-form"; import { XMLValidator } from "fast-xml-parser"; diff --git a/client/src/app/components/target-card.css b/client/src/app/components/target-card.css index 43648ea7e..6bc10c829 100644 --- a/client/src/app/components/target-card.css +++ b/client/src/app/components/target-card.css @@ -1,3 +1,7 @@ +.panel-style { + background: none; +} + .select-card__component__empty-state { padding: 0 !important; } diff --git a/client/src/app/components/target-card.tsx b/client/src/app/components/target-card.tsx index 443fba767..2a4bab1d4 100644 --- a/client/src/app/components/target-card.tsx +++ b/client/src/app/components/target-card.tsx @@ -13,6 +13,9 @@ import { ButtonVariant, Label, CardHeader, + PanelMain, + PanelMainBody, + Panel, } from "@patternfly/react-core"; import { DropdownItem } from "@patternfly/react-core/deprecated"; import { @@ -28,16 +31,19 @@ import { KebabDropdown } from "@app/shared/components"; import { useTranslation } from "react-i18next"; import "./target-card.css"; import DefaultRulesetIcon from "@app/images/Icon-Red_Hat-Virtual_server_stack-A-Black-RGB.svg"; -import { Ruleset } from "@app/api/models"; -import { getParsedLabel } from "@app/common/CustomRules/rules-utils"; +import { Target, TargetLabel } from "@app/api/models"; export interface TargetCardProps { - item: Ruleset; + item: Target; cardSelected?: boolean; isEditable?: boolean; - onCardClick?: (isSelecting: boolean, value: string) => void; + onCardClick?: ( + isSelecting: boolean, + targetLabelName: string, + target: Target + ) => void; onSelectedCardTargetChange?: (value: string) => void; - formTargets?: string[]; + formLabels?: TargetLabel[]; handleProps?: any; readOnly?: boolean; onEdit?: () => void; @@ -45,14 +51,14 @@ export interface TargetCardProps { } // Force display dropdown box even though there only one option available. -// This is a business rule to guarantee that option is alwyas present. +// This is a business rule to guarantee that option is always present. const forceSelect = ["Azure"]; export const TargetCard: React.FC = ({ - item, + item: target, readOnly, cardSelected, - formTargets, + formLabels, onCardClick, onSelectedCardTargetChange, handleProps, @@ -62,19 +68,18 @@ export const TargetCard: React.FC = ({ const { t } = useTranslation(); const [isCardSelected, setCardSelected] = React.useState(cardSelected); - const prevSelectedTarget = formTargets?.find( - (target) => - item.rules.map((ruleset) => ruleset?.metadata?.target).indexOf(target) !== - -1 - ); + const prevSelectedLabel = + formLabels?.find((formLabel) => { + const labelNames = target?.labels?.map((label) => label.name); + return labelNames?.includes(formLabel.name); + })?.name || ""; - const [isRuleTargetSelectOpen, setRuleTargetSelectOpen] = - React.useState(false); + const [isLabelSelectOpen, setLabelSelectOpen] = React.useState(false); - const [selectedRuleTarget, setSelectedRuleTarget] = React.useState( - prevSelectedTarget || - item.rules[0]?.metadata?.target || - `${item.name}-Empty` + const [selectedLabelName, setSelectedLabelName] = React.useState( + prevSelectedLabel || + target?.labels?.[0]?.name || + `${target?.name || "target"}-Empty` ); const handleCardClick = (event: React.MouseEvent) => { @@ -84,19 +89,17 @@ export const TargetCard: React.FC = ({ setCardSelected(!isCardSelected); onCardClick && - selectedRuleTarget && - onCardClick(!isCardSelected, selectedRuleTarget); + selectedLabelName && + onCardClick(!isCardSelected, selectedLabelName, target); }; - const handleRuleTargetSelection = ( + const handleLabelSelection = ( event: React.MouseEvent | React.ChangeEvent, selection: string | SelectOptionObject ) => { event.stopPropagation(); - setRuleTargetSelectOpen(false); - setSelectedRuleTarget(selection as string); - - //update the formTargets if this card is selected + setLabelSelectOpen(false); + setSelectedLabelName(selection as string); if (isCardSelected && onSelectedCardTargetChange) { onSelectedCardTargetChange(selection as string); } @@ -104,10 +107,10 @@ export const TargetCard: React.FC = ({ const getImage = (): React.ComponentType => { let result: React.ComponentType = CubesIcon; - const imagePath = item?.image?.id - ? `/hub/files/${item.image.id}` + const imagePath = target?.image?.id + ? `/hub/files/${target?.image.id}` : DefaultRulesetIcon; - if (item.image) { + if (target.image) { result = () => ( = ({ > @@ -151,10 +154,10 @@ export const TargetCard: React.FC = ({ )} - {readOnly && item.custom ? ( + {readOnly && target.custom ? ( ) : ( - item.custom && ( + target.custom && ( @@ -175,32 +178,40 @@ export const TargetCard: React.FC = ({ > - {item.name} + {target.name} - {item.kind === "category" && - (item.rules.length > 1 || forceSelect.includes(item.name)) ? ( + {target.choice && + ((!!target?.labels?.length && target?.labels?.length > 1) || + forceSelect.includes(target.name)) ? ( ) : null} - - {item.description} - + {target.description ? ( + + + + + {target.description} + + + + + ) : null} diff --git a/client/src/app/data/targets.ts b/client/src/app/data/targets.ts index 16ad5a5d2..e1b316052 100644 --- a/client/src/app/data/targets.ts +++ b/client/src/app/data/targets.ts @@ -1,33 +1,51 @@ import { APP_BRAND, BrandType } from "@app/Constants"; +import { TargetLabel } from "@app/api/models"; -const openTargets: string[] = [ - "konveyor.io/target=camel", - "konveyor.io/target=cloud-readiness", - "konveyor.io/target=drools", - "konveyor.io/target=eap", - "konveyor.io/target=eap6", - "konveyor.io/target=eap7", - "konveyor.io/target=eap8", - "konveyor.io/target=eapxp", - "konveyor.io/target=fsw", - "konveyor.io/target=fuse", - "konveyor.io/target=hibernate", - "konveyor.io/target=hibernate-search", - "konveyor.io/target=jakarta-ee", - "konveyor.io/target=java-ee", - "konveyor.io/target=jbpm", - "konveyor.io/target=linux", - "konveyor.io/target=openjdk", - "konveyor.io/target=openjdk11", - "konveyor.io/target=openjdk17", - "konveyor.io/target=openliberty", - "konveyor.io/target=quarkus", - "konveyor.io/target=resteasy", - "konveyor.io/target=rhr", - "konveyor.io/target=azure-appservice", +const openTargets: TargetLabel[] = [ + { + name: "konveyor.io/target=camel", + label: "konveyor.io/target=camel", + }, + { + name: "cloud-readiness", + label: "konveyor.io/target=cloud-readiness", + }, + { + name: "drools", + label: "konveyor.io/target=drools", + }, + { name: "eap", label: "konveyor.io/target=eap" }, + { name: "eap6", label: "konveyor.io/target=eap6" }, + { name: "eap7", label: "konveyor.io/target=eap7" }, + { name: "eap8", label: "konveyor.io/target=eap8" }, + { name: "eapxp", label: "konveyor.io/target=eapxp" }, + { name: "fsw", label: "konveyor.io/target=fsw" }, + { + name: "fuse", + label: "konveyor.io/target=fuse", + }, + { name: "hibernate", label: "konveyor.io/target=hibernate" }, + { name: "hibernate-search", label: "konveyor.io/target=hibernate-search" }, + { name: "jakarta-ee", label: "konveyor.io/target=jakarta-ee" }, + { name: "java-ee", label: "konveyor.io/target=java-ee" }, + { name: "jbpm", label: "konveyor.io/target=jbpm" }, + { name: "linux", label: "konveyor.io/target=linux" }, + { name: "openjdk", label: "konveyor.io/target=openjdk" }, + { name: "openjdk11", label: "konveyor.io/target=openjdk11" }, + { name: "openjdk17", label: "konveyor.io/target=openjdk17" }, + { name: "openliberty", label: "konveyor.io/target=openliberty" }, + { name: "quarkus", label: "konveyor.io/target=quarkus" }, + { name: "resteasy", label: "konveyor.io/target=resteasy" }, + { name: "rhr", label: "konveyor.io/target=rhr" }, + { name: "azure-appservice", label: "konveyor.io/target=azure-appservice" }, ]; -const proprietaryTargets = ["konveyor.io/target=azure-aks"]; +const proprietaryTargets = [ + { + name: "konveyor.io/target=azure-aks", + label: "konveyor.io/target=azure-aks", + }, +]; export const defaultTargets = APP_BRAND === BrandType.Konveyor diff --git a/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx b/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx index 69c34faae..875a493c5 100644 --- a/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx +++ b/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx @@ -169,11 +169,7 @@ export const AnalysisWizard: React.FC = ({ defaultValues: { artifact: null, mode: "binary", - formTargets: [], - formOtherLabels: [], - selectedFormSources: [], - formSources: defaultSources, - formRulesets: [], + formLabels: [], withKnown: "app", includedPackages: [], excludedPackages: [], @@ -259,9 +255,7 @@ export const AnalysisWizard: React.FC = ({ labels: { included: Array.from( new Set([ - ...fieldValues.formTargets, - ...fieldValues.selectedFormSources, - ...fieldValues.formOtherLabels, + ...fieldValues.formLabels.map((label) => label.label), ]) ), excluded: [], @@ -270,9 +264,6 @@ export const AnalysisWizard: React.FC = ({ tags: { excluded: fieldValues.excludedRulesTags, }, - rulesets: fieldValues.formRulesets.map((ruleset) => { - return { name: ruleset.name, id: ruleset.id || 0 }; - }), ...(fieldValues.rulesKind === "repository" && { repository: { kind: fieldValues?.repositoryType, diff --git a/client/src/app/pages/applications/analysis-wizard/custom-rules.tsx b/client/src/app/pages/applications/analysis-wizard/custom-rules.tsx index 97ae2bcef..caedaf60f 100644 --- a/client/src/app/pages/applications/analysis-wizard/custom-rules.tsx +++ b/client/src/app/pages/applications/analysis-wizard/custom-rules.tsx @@ -40,12 +40,15 @@ import { FilterType, } from "@app/shared/components/FilterToolbar"; import { useLegacyFilterState } from "@app/shared/hooks/useLegacyFilterState"; -import { IReadFile, Ref } from "@app/api/models"; +import { IReadFile, Ref, TargetLabel } from "@app/api/models"; import { NoDataEmptyState } from "@app/shared/components/no-data-empty-state"; import "./wizard.css"; import { AnalysisWizardFormValues } from "./schema"; -import { parseRules } from "@app/common/CustomRules/rules-utils"; +import { + getParsedLabel, + parseRules, +} from "@app/common/CustomRules/rules-utils"; import { HookFormPFGroupController, HookFormPFTextInput, @@ -64,13 +67,7 @@ export const CustomRules: React.FC = (props) => { useFormContext(); const values = getValues(); - const { - formSources, - formTargets, - formOtherLabels, - customRulesFiles, - rulesKind, - } = watch(); + const { formLabels, customRulesFiles, rulesKind } = watch(); const initialActiveTabKeyValue = (value: string): number => value === "manual" ? 0 : value === "repository" ? 1 : 0; @@ -194,19 +191,11 @@ export const CustomRules: React.FC = (props) => { variant="plain" onClick={() => { customRulesFiles.forEach((file) => { - const { source, target } = parseRules(file); - if (source && formSources.includes(source)) { - const updatedSources = formSources.filter( - (formSource) => formSource !== source - ); - setValue("formSources", [...updatedSources]); - } - if (target && formTargets.includes(target)) { - const updatedTargets = formTargets.filter( - (formTarget) => formTarget !== target - ); - setValue("formTargets", [...updatedTargets]); - } + const { allLabels } = parseRules(file); + const updatedFormLabels = formLabels.filter( + (label) => !allLabels?.includes(label.label) + ); + setValue("formLabels", [...updatedFormLabels]); }); // Remove rule file from list @@ -236,7 +225,7 @@ export const CustomRules: React.FC = (props) => { {t("wizard.label.customRules")} - {values.formRulesets.length === 0 && + {values.formLabels.length === 0 && values.customRulesFiles.length === 0 && !values.sourceRepository && ( = (props) => { shouldDirty: true, }); updatedCustomRulesFiles.forEach((file) => { - const { source, target, otherLabels } = parseRules(file); - if (source && !formSources.includes(source)) { - setValue("formSources", [...formSources, source]); - } - if (target && !formTargets.includes(target)) { - setValue("formTargets", [...formTargets, target]); - } - if ( - otherLabels?.length && - formOtherLabels.some( - (otherLabel) => !otherLabels.includes(otherLabel) - ) - ) { - const newOtherLabels = otherLabels.filter( - (otherLabel) => !formOtherLabels.includes(otherLabel) - ); + const { allLabels } = parseRules(file); - setValue("formOtherLabels", [ - ...formOtherLabels, - ...newOtherLabels, - ]); - } + const formattedAllLabels = + allLabels?.map((label): TargetLabel => { + return { + name: getParsedLabel(label).labelValue, + label: label, + }; + }) || []; + const newLabels = formLabels.filter((label) => { + const newLabelNames = formattedAllLabels.map( + (label) => label.name + ); + return !newLabelNames.includes(label.name); + }); + setValue("formLabels", [ + ...newLabels, + ...formattedAllLabels, + ]); }); + setRuleFiles([]); setUploadError(""); setCustomRulesModalOpen(false); diff --git a/client/src/app/pages/applications/analysis-wizard/review.tsx b/client/src/app/pages/applications/analysis-wizard/review.tsx index acda559fb..c4b40aca8 100644 --- a/client/src/app/pages/applications/analysis-wizard/review.tsx +++ b/client/src/app/pages/applications/analysis-wizard/review.tsx @@ -42,8 +42,7 @@ export const Review: React.FC = ({ applications, mode }) => { const { watch } = useFormContext(); const { - formTargets, - selectedFormSources, + formLabels, withKnown, includedPackages, hasExcludedPackages, @@ -83,33 +82,39 @@ export const Review: React.FC = ({ applications, mode }) => { - {formTargets.length > 1 + {formLabels.length > 1 ? t("wizard.terms.targets") : t("wizard.terms.target")} - {formTargets.map((target, index) => ( - - {getParsedLabel(target).labelValue} - - ))} + {formLabels.map((label, index) => { + const parsedLabel = getParsedLabel(label?.label); + if (parsedLabel.labelType === "target") { + return ( + {parsedLabel.labelValue} + ); + } + })} - {formTargets.length > 1 + {formLabels.length > 1 ? t("wizard.terms.sources") : t("wizard.terms.source")} - {selectedFormSources.map((source, index) => ( - - {getParsedLabel(source).labelValue} - - ))} + {formLabels.map((label, index) => { + const parsedLabel = getParsedLabel(label?.label); + if (parsedLabel.labelType === "source") { + return ( + {parsedLabel.labelValue} + ); + } + })} @@ -125,12 +130,9 @@ export const Review: React.FC = ({ applications, mode }) => { - { - // t("wizard.terms.packages") - t("wizard.composed.included", { - what: t("wizard.terms.packages").toLowerCase(), - }) - } + {t("wizard.composed.included", { + what: t("wizard.terms.packages").toLowerCase(), + })} diff --git a/client/src/app/pages/applications/analysis-wizard/schema.ts b/client/src/app/pages/applications/analysis-wizard/schema.ts index 465a5f332..5e90566df 100644 --- a/client/src/app/pages/applications/analysis-wizard/schema.ts +++ b/client/src/app/pages/applications/analysis-wizard/schema.ts @@ -2,12 +2,9 @@ import * as yup from "yup"; import { Application, IReadFile, - Ref, - Repository, - Ruleset, - RulesetImage, - RulesetKind, FileLoadError, + Target, + TargetLabel, } from "@app/api/models"; import { useTranslation } from "react-i18next"; import { useAnalyzableApplicationsByMode } from "./utils"; @@ -58,31 +55,12 @@ const useModeStepSchema = ({ }; export interface TargetsStepValues { - formTargets: string[]; - formOtherLabels: string[]; - formRulesets: Ruleset[]; + formLabels: TargetLabel[]; } -export const rulesetSchema: yup.SchemaOf = yup.object({ - createTime: yup.string(), - createUser: yup.string(), - description: yup.string(), - id: yup.number().required(), - name: yup.string().required(), - image: yup.mixed(), - kind: yup.mixed(), - rules: yup.array(), - custom: yup.boolean(), - repository: yup.mixed(), - identity: yup.mixed(), - updateUser: yup.string(), -}); const useTargetsStepSchema = (): yup.SchemaOf => { - const { t } = useTranslation(); return yup.object({ - formTargets: yup.array(), - formOtherLabels: yup.array(), - formRulesets: yup.array().of(rulesetSchema), + formLabels: yup.array(), }); }; @@ -114,8 +92,6 @@ const useScopeStepSchema = (): yup.SchemaOf => { }; export interface CustomRulesStepValues { - formSources: string[]; - selectedFormSources: string[]; customRulesFiles: IReadFile[]; rulesKind: string; repositoryType?: string; @@ -137,8 +113,6 @@ export const customRulesFilesSchema: yup.SchemaOf = yup.object({ const useCustomRulesStepSchema = (): yup.SchemaOf => { const { t } = useTranslation(); return yup.object({ - selectedFormSources: yup.array().of(yup.string().defined()), - formSources: yup.array().of(yup.string().defined()), rulesKind: yup.string().defined(), customRulesFiles: yup .array() @@ -148,9 +122,9 @@ const useCustomRulesStepSchema = (): yup.SchemaOf => { then: yup.array().of(customRulesFilesSchema), otherwise: (schema) => schema, }) - .when(["formRulesets", "rulesKind"], { - is: (rulesets: Ruleset[], rulesKind: string) => - rulesets.length === 0 && rulesKind === "manual", + .when(["formLabels", "rulesKind"], { + is: (labels: TargetLabel[], rulesKind: string) => + labels.length === 0 && rulesKind === "manual", then: (schema) => schema.min(1, "At least 1 Rule File is required"), // TODO translation here }), repositoryType: yup.mixed().when("rulesKind", { diff --git a/client/src/app/pages/applications/analysis-wizard/set-options.tsx b/client/src/app/pages/applications/analysis-wizard/set-options.tsx index d30a092af..4d9541f6c 100644 --- a/client/src/app/pages/applications/analysis-wizard/set-options.tsx +++ b/client/src/app/pages/applications/analysis-wizard/set-options.tsx @@ -21,12 +21,11 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { AnalysisWizardFormValues } from "./schema"; import { HookFormPFGroupController } from "@app/shared/components/hook-form-pf-fields"; import { StringListField } from "@app/shared/components/string-list-field"; -import { useFetchRulesets } from "@app/queries/rulesets"; -import { - getParsedLabel, - getRulesetTargetList, -} from "@app/common/CustomRules/rules-utils"; +import { getParsedLabel } from "@app/common/CustomRules/rules-utils"; import { DEFAULT_SELECT_MAX_HEIGHT } from "@app/Constants"; +import { useFetchTargets } from "@app/queries/targets"; +import defaultSources from "./sources"; +import { Target } from "@app/api/models"; export const SetOptions: React.FC = () => { const { t } = useTranslation(); @@ -34,26 +33,39 @@ export const SetOptions: React.FC = () => { const { watch, control, setValue, getValues } = useFormContext(); - const { - formSources, - selectedFormSources, - formTargets, - formRulesets, - diva, - excludedRulesTags, - autoTaggingEnabled, - } = watch(); + const { formLabels, diva, excludedRulesTags, autoTaggingEnabled } = watch(); const [isSelectTargetsOpen, setSelectTargetsOpen] = React.useState(false); const [isSelectSourcesOpen, setSelectSourcesOpen] = React.useState(false); - const { rulesets } = useFetchRulesets(); + const { targets } = useFetchTargets(); + + const allLabelsFromTargets = targets + .map((target) => target?.labels ?? []) + .filter(Boolean) + .flat() + // Remove duplicates from array of objects based on label value (label.label) + .filter((v, i, a) => a.findIndex((v2) => v2.label === v.label) === i); + + const allTargetLabelsFromTargets = allLabelsFromTargets.filter((label) => { + const parsedLabel = getParsedLabel(label?.label); + if (parsedLabel.labelType === "target") { + return parsedLabel.labelValue; + } + }); + + const allSourceLabelsFromTargets = allLabelsFromTargets.filter((label) => { + const parsedLabel = getParsedLabel(label?.label); + if (parsedLabel.labelType === "source") { + return parsedLabel.labelValue; + } + }); - const allRulesetTargets = rulesets - .map((Ruleset) => getRulesetTargetList(Ruleset)) - .flat(); + const defaultTargetsAndTargetsLabels = [ + ...new Set(defaultTargets.concat(allTargetLabelsFromTargets)), + ]; - const defaultTargetsAndRulesetTargets = [ - ...new Set(defaultTargets.concat(allRulesetTargets)), + const defaultSourcesAndSourcesLabels = [ + ...new Set(defaultSources.concat(allSourceLabelsFromTargets)), ]; return ( @@ -71,117 +83,146 @@ export const SetOptions: React.FC = () => { ( - { + const selectionWithLabelSelector = `konveyor.io/target=${selection}`; + const matchingLabel = + defaultTargetsAndTargetsLabels?.find( + (label) => label.label === selectionWithLabelSelector + ) || ""; + + const formLabelLabels = formLabels.map( + (formLabel) => formLabel.label + ); + if ( + matchingLabel && + !formLabelLabels.includes(matchingLabel.label) + ) { + onChange([...formLabels, matchingLabel]); + } else { + onChange( + formLabels.filter( + (formLabel) => + formLabel.label !== selectionWithLabelSelector ) ); - onChange( - selectedFormTargets.filter( - (formTarget) => formTarget !== selectionWithLabelSelector - ) - ); - } - onBlur(); - setSelectTargetsOpen(!isSelectTargetsOpen); - }} - onToggle={() => { - setSelectTargetsOpen(!isSelectTargetsOpen); - }} - onClear={() => { - onChange([]); - }} - validated={getValidatedFromErrors(error, isDirty)} - > - {defaultTargetsAndRulesetTargets.map((targetName, index) => ( - - ))} - - )} + } + onBlur(); + setSelectTargetsOpen(!isSelectTargetsOpen); + }} + onToggle={() => { + setSelectTargetsOpen(!isSelectTargetsOpen); + }} + onClear={() => { + onChange([]); + }} + validated={getValidatedFromErrors(error, isDirty)} + > + {defaultTargetsAndTargetsLabels.map((targetLabel, index) => ( + + ))} + + ); + }} /> ( - - )} + }) + .filter(Boolean); + + return ( + + ); + }} /> { const { t } = useTranslation(); - const { rulesets } = useFetchRulesets(); + const { targets } = useFetchTargets(); - const rulesetOrderSetting = useSetting("ui.ruleset.order"); + const targetOrderSetting = useSetting("ui.target.order"); const { watch, setValue, getValues } = useFormContext(); const values = getValues(); - const formTargets = watch("formTargets"); - const formRulesets = watch("formRulesets"); - const formSources = watch("formSources"); - const formOtherLabels = watch("formOtherLabels"); - - const handleOnSelectedCardTargetChange = ( - selectedRuleTarget: string, - selectedRuleset: Ruleset - ) => { - const otherSelectedRuleTargets = formTargets.filter( - (formTarget) => - !selectedRuleset.rules - .map((rule) => rule?.metadata?.target) - .includes(formTarget) - ); - const definedSelectedTargets: string[] = - selectedRuleset.kind === "category" - ? [selectedRuleTarget] - : selectedRuleset.rules - .map((rulesets) => rulesets?.metadata?.target || "") - .filter((target) => !!target); - - setValue("formTargets", [ - ...otherSelectedRuleTargets, - ...definedSelectedTargets, - ]); + const formLabels = watch("formLabels"); + + const handleOnSelectedCardTargetChange = (selectedLabelName: string) => { + const otherSelectedLabels = formLabels?.filter((formLabel) => { + return formLabel.name !== selectedLabelName; + }); + const matchingLabel = + targets + ?.find((target) => { + const labelNames = target?.labels?.map((label) => label.name); + return labelNames?.includes(selectedLabelName); + }) + ?.labels?.find((label) => label.name === selectedLabelName) || ""; + + const matchingOtherLabelNames = + targets + ?.find((target) => { + const labelNames = target?.labels?.map((label) => label.name); + return labelNames?.includes(selectedLabelName); + }) + ?.labels?.filter((label) => label.name !== selectedLabelName) + .map((label) => label.name) || ""; + + const isNewLabel = !formLabels + .map((label) => label.name) + .includes(selectedLabelName); + if (isNewLabel) { + const filterConflictingLabels = otherSelectedLabels.filter( + (label) => !matchingOtherLabelNames.includes(label.name) + ); + matchingLabel && + setValue("formLabels", [...filterConflictingLabels, matchingLabel]); + } }; const handleOnCardClick = ( isSelecting: boolean, - selectedRuleTarget: string, - selectedRuleset: Ruleset + selectedLabelName: string, + target: Target ) => { - const otherSelectedRuleSources = formSources.filter( - (formSource) => - !selectedRuleset.rules - .map((rule) => rule?.metadata?.source) - .includes(formSource) - ); - const otherSelectedRuleTargets = formTargets.filter( - (formTarget) => - !selectedRuleset.rules - .map((rule) => rule?.metadata?.target) - .includes(formTarget) - ); - - const otherSelectedRulesets = formRulesets.filter( - (formRuleset) => selectedRuleset.id !== formRuleset.id - ); - - const otherSelectedOtherLabels = formOtherLabels.filter( - (label) => - !selectedRuleset.rules - .flatMap((rule) => rule?.metadata?.otherLabels) - .includes(label) - ); - - if (isSelecting) { - const definedSelectedOtherLabels: string[] = Array.from( - new Set( - selectedRuleset.rules - .flatMap((rulesets) => rulesets?.metadata?.otherLabels || "") - .filter((otherLabel) => otherLabel!) - ) - ); - - setValue("formOtherLabels", [ - ...otherSelectedOtherLabels, - ...definedSelectedOtherLabels, - ]); - - const definedSelectedSources: string[] = Array.from( - new Set( - selectedRuleset.rules - .map((rulesets) => rulesets?.metadata?.source || "") - .filter((source) => !!source) - ) - ); - - setValue("formSources", [ - ...otherSelectedRuleSources, - ...definedSelectedSources, - ]); - - const definedSelectedTargets: string[] = Array.from( - new Set( - selectedRuleset.kind === "category" - ? [selectedRuleTarget] - : selectedRuleset.rules - .map((rulesets) => rulesets?.metadata?.target || "") - .filter((target) => !!target) - ) - ); - - setValue("formTargets", [ - ...otherSelectedRuleTargets, - ...definedSelectedTargets, - ]); - - setValue("formRulesets", [...otherSelectedRulesets, selectedRuleset]); + if (target.custom) { + const customTargetLabelNames = target.labels?.map((label) => label.name); + const otherSelectedLabels = formLabels?.filter((formLabel) => { + return !customTargetLabelNames?.includes(formLabel.name); + }); + if (isSelecting && target?.labels) { + setValue("formLabels", [...otherSelectedLabels, ...target.labels]); + } else { + setValue("formLabels", otherSelectedLabels); + } } else { - setValue("formSources", otherSelectedRuleSources); - setValue("formTargets", otherSelectedRuleTargets); - setValue("formRulesets", otherSelectedRulesets); - setValue("formOtherLabels", otherSelectedOtherLabels); + const otherSelectedLabels = formLabels?.filter((formLabel) => { + return formLabel.name !== selectedLabelName; + }); + if (isSelecting) { + const matchingLabel = + target.labels?.find((label) => label.name === selectedLabelName) || + ""; + + matchingLabel && + setValue("formLabels", [...otherSelectedLabels, matchingLabel]); + } else { + setValue("formLabels", otherSelectedLabels); + } } }; + return (
{ @@ -146,49 +105,34 @@ export const SetTargets: React.FC = () => { {t("wizard.label.setTargets")} - {values.formRulesets.length === 0 && - values.customRulesFiles.length === 0 && - !values.sourceRepository && ( - - )} - {rulesetOrderSetting.isSuccess - ? rulesetOrderSetting.data.map((id, index) => { - const matchingRuleset = rulesets.find( - (target) => target.id === id + {targetOrderSetting.isSuccess + ? targetOrderSetting.data.map((id, index) => { + const matchingTarget = targets.find((target) => target.id === id); + const matchingLabelNames = + matchingTarget?.labels?.map((label) => label.name) || []; + + const isSelected = formLabels?.some((label) => + matchingLabelNames.includes(label.name) ); - if (matchingRuleset) { + if (matchingTarget) { return ( formRuleset.name) - .includes(matchingRuleset.name)} - onSelectedCardTargetChange={( - selectedRuleTarget: string - ) => { - handleOnSelectedCardTargetChange( - selectedRuleTarget, - matchingRuleset - ); + item={matchingTarget} + cardSelected={isSelected} + onSelectedCardTargetChange={(selectedTarget) => { + handleOnSelectedCardTargetChange(selectedTarget); }} - onCardClick={( - isSelecting: boolean, - selectedRuleTarget: string - ) => { + onCardClick={(isSelecting, selectedLabelName, target) => { handleOnCardClick( isSelecting, - selectedRuleTarget, - matchingRuleset + selectedLabelName, + target ); }} - formTargets={formTargets} + formLabels={formLabels} /> ); diff --git a/client/src/app/pages/applications/analysis-wizard/sources.ts b/client/src/app/pages/applications/analysis-wizard/sources.ts index 3616a64b9..2cd4dd7c1 100644 --- a/client/src/app/pages/applications/analysis-wizard/sources.ts +++ b/client/src/app/pages/applications/analysis-wizard/sources.ts @@ -1,60 +1,60 @@ -const defaultSources = [ - "konveyor.io/source=agroal", - "konveyor.io/source=amazon", - "konveyor.io/source=apicurio", - "konveyor.io/source=artemis", - "konveyor.io/source=avro", - "konveyor.io/source=camel", - "konveyor.io/source=config", - "konveyor.io/source=drools", - "konveyor.io/source=eap", - "konveyor.io/source=eap6", - "konveyor.io/source=eap7", - "konveyor.io/source=eap8", - "konveyor.io/source=eapxp", - "konveyor.io/source=elytron", - "konveyor.io/source=flyway", - "konveyor.io/source=glassfish", - "konveyor.io/source=hibernate", - "konveyor.io/source=hibernate-search", - "konveyor.io/source=jakarta-ee", - "konveyor.io/source=java", - "konveyor.io/source=java-ee", - "konveyor.io/source=javaee", - "konveyor.io/source=jbpm", - "konveyor.io/source=jdbc", - "konveyor.io/source=jonas", - "konveyor.io/source=jrun", - "konveyor.io/source=jsonb", - "konveyor.io/source=jsonp", - "konveyor.io/source=kafka", - "konveyor.io/source=keycloak", - "konveyor.io/source=kubernetes", - "konveyor.io/source=liquibase", - "konveyor.io/source=log4j", - "konveyor.io/source=logging", - "konveyor.io/source=micrometer", - "konveyor.io/source=narayana", - "konveyor.io/source=openjdk", - "konveyor.io/source=openjdk11", - "konveyor.io/source=openshift", - "konveyor.io/source=opentelemetry", - "konveyor.io/source=oraclejdk", - "konveyor.io/source=orion", - "konveyor.io/source=picocli", - "konveyor.io/source=resin", - "konveyor.io/source=resteasy", - "konveyor.io/source=rmi", - "konveyor.io/source=rpc", - "konveyor.io/source=seam", - "konveyor.io/source=soa", - "konveyor.io/source=soa-p", - "konveyor.io/source=sonic", - "konveyor.io/source=sonicesb", - "konveyor.io/source=springboot", - "konveyor.io/source=thorntail", - "konveyor.io/source=weblogic", - "konveyor.io/source=websphere", +import { TargetLabel } from "@app/api/models"; + +const defaultSources: TargetLabel[] = [ + { name: "agroal", label: "konveyor.io/source=agroal" }, + { name: "amazon", label: "konveyor.io/source=amazon" }, + { name: "apicurio", label: "konveyor.io/source=apicurio" }, + { name: "artemis", label: "konveyor.io/source=artemis" }, + { name: "avro", label: "konveyor.io/source=avro" }, + { name: "camel", label: "konveyor.io/source=camel" }, + { name: "config", label: "konveyor.io/source=config" }, + { name: "drools", label: "konveyor.io/source=drools" }, + { name: "eap", label: "konveyor.io/source=eap" }, + { name: "eap6", label: "konveyor.io/source=eap6" }, + { name: "eap7", label: "konveyor.io/source=eap7" }, + { name: "eap8", label: "konveyor.io/source=eap8" }, + { name: "eapxp", label: "konveyor.io/source=eapxp" }, + { name: "elytron", label: "konveyor.io/source=elytron" }, + { name: "flyway", label: "konveyor.io/source=flyway" }, + { name: "glassfish", label: "konveyor.io/source=glassfish" }, + { name: "hibernate", label: "konveyor.io/source=hibernate" }, + { name: "hibernate-search", label: "konveyor.io/source=hibernate-search" }, + { name: "jakarta-ee", label: "konveyor.io/source=jakarta-ee" }, + { name: "java-ee", label: "konveyor.io/source=java-ee" }, + { name: "jbpm", label: "konveyor.io/source=jbpm" }, + { name: "jboss", label: "konveyor.io/source=jboss" }, + { name: "javaee", label: "konveyor.io/source=javaee" }, + { name: "jdbc", label: "konveyor.io/source=jdbc" }, + { name: "jonas", label: "konveyor.io/source=jonas" }, + { name: "jrun", label: "konveyor.io/source=jrun" }, + { name: "jsonb", label: "konveyor.io/source=jsonb" }, + { name: "jsonp", label: "konveyor.io/source=jsonp" }, + { name: "kafka", label: "konveyor.io/source=kafka" }, + { name: "keycloak", label: "konveyor.io/source=keycloak" }, + { name: "kubernetes", label: "konveyor.io/source=kubernetes" }, + { name: "liquibase", label: "konveyor.io/source=liquibase" }, + { name: "log4j", label: "konveyor.io/source=log4j" }, + { name: "logging", label: "konveyor.io/source=logging" }, + { name: "micrometer", label: "konveyor.io/source=micrometer" }, + { name: "narayana", label: "konveyor.io/source=narayana" }, + { name: "openjdk", label: "konveyor.io/source=openjdk" }, + { name: "openjdk11", label: "konveyor.io/source=openjdk11" }, + { name: "openshift", label: "konveyor.io/source=openshift" }, + { name: "opentelemetry", label: "konveyor.io/source=opentelemetry" }, + { name: "oraclejdk", label: "konveyor.io/source=oraclejdk" }, + { name: "orion", label: "konveyor.io/source=orion" }, + { name: "picocli", label: "konveyor.io/source=picocli" }, + { name: "resin", label: "konveyor.io/source=resin" }, + { name: "resteasy", label: "konveyor.io/source=resteasy" }, + { name: "rmi", label: "konveyor.io/source=rmi" }, + { name: "rpc", label: "konveyor.io/source=rpc" }, + { name: "seam", label: "konveyor.io/source=seam" }, + { name: "soa", label: "konveyor.io/source=soa" }, + { name: "spring", label: "konveyor.io/source=spring" }, + { name: "spring-boot", label: "konveyor.io/source=spring-boot" }, + { name: "thorntail", label: "konveyor.io/source=thorntail" }, + { name: "weblogic", label: "konveyor.io/source=weblogic" }, + { name: "websphere", label: "konveyor.io/source=websphere" }, ]; export default defaultSources; diff --git a/client/src/app/pages/migration-targets/components/custom-target-form.tsx b/client/src/app/pages/migration-targets/components/custom-target-form.tsx index f45b833d4..4351f76bc 100644 --- a/client/src/app/pages/migration-targets/components/custom-target-form.tsx +++ b/client/src/app/pages/migration-targets/components/custom-target-form.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { ActionGroup, Alert, @@ -18,32 +18,40 @@ import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; -import Resizer from "react-image-file-resizer"; import { AxiosError, AxiosResponse } from "axios"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import defaultImage from "./default.png"; import { HookFormPFGroupController, HookFormPFTextInput, } from "@app/shared/components/hook-form-pf-fields"; -import { IReadFile, Ruleset, Rule } from "@app/api/models"; -import { parseRules } from "@app/common/CustomRules/rules-utils"; +import { IReadFile, Rule, Target, TargetLabel } from "@app/api/models"; import { - useCreateFileMutation, - useCreateRulesetMutation, - useFetchRulesets, - useUpdateRulesetMutation, -} from "@app/queries/rulesets"; + getParsedLabel, + parseRules, +} from "@app/common/CustomRules/rules-utils"; +import { useCreateFileMutation } from "@app/queries/targets"; import { OptionWithValue, SimpleSelect } from "@app/shared/components"; import { toOptionLike } from "@app/utils/model-utils"; import { useFetchIdentities } from "@app/queries/identities"; import useRuleFiles from "@app/common/CustomRules/useRuleFiles"; -import { customURLValidation, duplicateNameCheck } from "@app/utils/utils"; +import { + customURLValidation, + duplicateNameCheck, + getAxiosErrorMessage, +} from "@app/utils/utils"; import { customRulesFilesSchema } from "../../applications/analysis-wizard/schema"; +import { + useCreateTargetMutation, + useFetchTargets, + useUpdateTargetMutation, +} from "@app/queries/targets"; +import { NotificationsContext } from "@app/shared/notifications-context"; export interface CustomTargetFormProps { - ruleset?: Ruleset | null; - onSaved: (response: AxiosResponse) => void; + target?: Target | null; + onSaved: (response: AxiosResponse) => void; onCancel: () => void; } @@ -62,34 +70,18 @@ export interface CustomTargetFormValues { } export const CustomTargetForm: React.FC = ({ - ruleset: initialRuleset, + target: initialTarget, onSaved, onCancel, }) => { + const { pushNotification } = useContext(NotificationsContext); const { t } = useTranslation(); - const [ruleset, setRuleset] = useState(initialRuleset); + const [target, setTarget] = useState(initialTarget); const [filename, setFilename] = React.useState("default.png"); const [isImageFileRejected, setIsImageFileRejected] = useState(false); - const resizeFile = (file: File) => - new Promise((resolve) => { - const extension = file?.name?.split(".")[1]; - Resizer.imageFileResizer( - file, - 80, - 80, - extension, - 100, - 0, - (uri) => { - resolve(uri as File); - }, - "file" - ); - }); - const repositoryTypeOptions: OptionWithValue[] = [ { value: "git", @@ -112,7 +104,7 @@ export const CustomTargetForm: React.FC = ({ }; }); - const { rulesets } = useFetchRulesets(); + const { targets } = useFetchTargets(); const validationSchema: yup.SchemaOf = yup .object() @@ -127,10 +119,10 @@ export const CustomTargetForm: React.FC = ({ .test( "Duplicate name", "A custom target with this name already exists. Use a different name.", - (value) => duplicateNameCheck(rulesets, ruleset || null, value || "") + (value) => duplicateNameCheck(targets, target || null, value || "") ), description: yup.string(), - imageID: yup.number().defined(), + imageID: yup.number().defined().nullable(), rulesKind: yup.string().defined(), customRulesFiles: yup .array() @@ -167,7 +159,7 @@ export const CustomTargetForm: React.FC = ({ }); const getInitialCustomRulesFilesData = () => - ruleset?.rules?.map((rule): IReadFile => { + target?.ruleset?.rules?.map((rule): IReadFile => { const emptyFile = new File(["empty"], rule.name, { type: "placeholder", }); @@ -181,21 +173,21 @@ export const CustomTargetForm: React.FC = ({ const methods = useForm({ defaultValues: { - id: ruleset?.id || 0, - name: ruleset?.name || "", - description: ruleset?.description || "", - imageID: ruleset?.image?.id || 1, + id: target?.id || 0, + name: target?.name || "", + description: target?.description || "", + imageID: target?.image?.id || null, customRulesFiles: getInitialCustomRulesFilesData(), - rulesKind: !ruleset + rulesKind: !target ? "manual" - : !!ruleset?.rules?.length + : !!target?.ruleset?.rules?.length ? "manual" : "repository", - associatedCredentials: ruleset?.identity?.name, - repositoryType: ruleset?.repository?.kind, - sourceRepository: ruleset?.repository?.url, - branch: ruleset?.repository?.branch, - rootPath: ruleset?.repository?.path, + associatedCredentials: target?.ruleset?.identity?.name, + repositoryType: target?.ruleset?.repository?.kind, + sourceRepository: target?.ruleset?.repository?.url, + branch: target?.ruleset?.repository?.branch, + rootPath: target?.ruleset?.repository?.path, }, resolver: yupResolver(validationSchema), mode: "onChange", @@ -215,14 +207,14 @@ export const CustomTargetForm: React.FC = ({ } = methods; useEffect(() => { - setRuleset(initialRuleset); - if (initialRuleset?.image?.id === 1) { + setTarget(initialTarget); + if (initialTarget?.image?.id === 1) { setFilename("default.png"); } else { - setFilename(initialRuleset?.image?.name || "default.png"); + setFilename(initialTarget?.image?.name || "default.png"); } return () => { - setRuleset(undefined); + setTarget(undefined); setFilename("default.png"); }; }, []); @@ -246,6 +238,7 @@ export const CustomTargetForm: React.FC = ({ const onSubmit = (formValues: CustomTargetFormValues) => { let rules: Rule[] = []; + let labels: TargetLabel[] = []; ruleFiles.forEach((file) => { if (file.data && file?.fullFile?.type !== "placeholder") { @@ -258,8 +251,17 @@ export const CustomTargetForm: React.FC = ({ }, }; rules = [...rules, newRule]; + labels = [ + ...labels, + ...(allLabels?.map((label): TargetLabel => { + return { + name: getParsedLabel(label).labelValue, + label: label, + }; + }) || []), + ]; } else { - const matchingExistingRule = ruleset?.rules.find( + const matchingExistingRule = target?.ruleset?.rules.find( (ruleset) => ruleset.name === file.fileName ); if (matchingExistingRule) { @@ -272,76 +274,125 @@ export const CustomTargetForm: React.FC = ({ (identity) => identity.name === formValues.associatedCredentials ); - const payload: Ruleset = { + const payload: Target = { id: formValues.id ? formValues.id : undefined, name: formValues.name.trim(), description: formValues?.description?.trim() || "", - image: { id: formValues.imageID ? formValues.imageID : 1 }, + ...(formValues.imageID && { image: { id: formValues.imageID } }), custom: true, - rules: rules, - ...(formValues.rulesKind === "repository" && { - repository: { - kind: formValues?.repositoryType, - url: formValues?.sourceRepository?.trim(), - branch: formValues?.branch?.trim(), - path: formValues?.rootPath?.trim(), - }, - }), - ...(formValues.associatedCredentials && - matchingSourceCredential && - formValues.rulesKind === "repository" && { - identity: { - id: matchingSourceCredential.id, - name: matchingSourceCredential.name, + labels: !!labels.length ? labels : [{ name: "custom", label: "custom" }], + ruleset: { + id: target && target.custom ? target.ruleset.id : undefined, + name: formValues.name.trim(), + rules: rules, + ...(formValues.rulesKind === "repository" && { + repository: { + kind: formValues?.repositoryType, + url: formValues?.sourceRepository?.trim(), + branch: formValues?.branch?.trim(), + path: formValues?.rootPath?.trim(), }, }), + ...(formValues.associatedCredentials && + matchingSourceCredential && + formValues.rulesKind === "repository" && { + identity: { + id: matchingSourceCredential.id, + name: matchingSourceCredential.name, + }, + }), + }, }; - if (ruleset) { - updateRuleset({ ...payload }); + + if (target) { + formValues.imageID + ? updateTarget({ ...payload }) + : fetch(defaultImage) + .then((res) => res.blob()) + .then((res) => { + const defaultImageFile = new File([res], "default.png"); + return handleFileUpload(defaultImageFile); + }) + .then((res) => { + console.log("res ", res); + updateTarget({ + ...payload, + image: { id: res.id }, + }); + }) + .catch((err) => { + console.error(err); + }); } else { - createRuleset(payload); + formValues.imageID + ? createTarget(payload) + : fetch(defaultImage) + .then((res) => res.blob()) + .then((res) => { + const defaultImageFile = new File([res], "default.png", { + type: res.type, + }); + return handleFileUpload(defaultImageFile); + }) + .then((res) => { + console.log("res ", res); + createTarget({ + ...payload, + image: { id: res.id }, + }); + }) + .catch((err) => { + console.error(err); + }); } }; - const onCreateImageFileSuccess = (response: any) => { - setValue("imageID", response?.id); - setFocus("imageID"); - clearErrors("imageID"); - trigger("imageID"); + const { mutateAsync: createImageFileAsync } = useCreateFileMutation(); + + const onCreateTargetSuccess = (response: any) => { + onSaved(response); + reset(); }; - const onCreateImageFileFailure = (error: AxiosError) => { - setValue("imageID", 1); + const onCreateTargetFailure = (error: AxiosError) => { + pushNotification({ + title: getAxiosErrorMessage(error), + variant: "danger", + }); }; - const { mutate: createImageFile } = useCreateFileMutation( - onCreateImageFileSuccess, - onCreateImageFileFailure + const { mutate: createTarget } = useCreateTargetMutation( + onCreateTargetSuccess, + onCreateTargetFailure ); - const onCreaterulesuccess = (response: any) => { + const onUpdateTargetSuccess = (response: any) => { onSaved(response); reset(); }; - const onCreateRulesetFailure = (error: AxiosError) => {}; + const onUpdateTargetFailure = (error: AxiosError) => {}; - const { mutate: createRuleset } = useCreateRulesetMutation( - onCreaterulesuccess, - onCreateRulesetFailure + const { mutate: updateTarget } = useUpdateTargetMutation( + onUpdateTargetSuccess, + onUpdateTargetFailure ); - const onUpdaterulesuccess = (response: any) => { - onSaved(response); - reset(); - }; + const handleFileUpload = async (file: File) => { + setFilename(file.name); + const formFile = new FormData(); + formFile.append("file", file); - const onUpdateRulesetFailure = (error: AxiosError) => {}; + const newImageFile: IReadFile = { + fileName: file.name, + fullFile: file, + }; - const { mutate: updateRuleset } = useUpdateRulesetMutation( - onUpdaterulesuccess, - onUpdateRulesetFailure - ); + return createImageFileAsync({ + formData: formFile, + file: newImageFile, + }); + }; return ( @@ -390,25 +441,21 @@ export const CustomTargetForm: React.FC = ({ }} validated={isImageFileRejected || error ? "error" : "default"} onFileInputChange={async (_, file) => { - const image = await resizeFile(file); - setFilename(image.name); - const formFile = new FormData(); - formFile.append("file", file); - - const newImageFile: IReadFile = { - fileName: file.name, - fullFile: file, - }; - - createImageFile({ - formData: formFile, - file: newImageFile, - }); + handleFileUpload(file) + .then((res) => { + setValue("imageID", res.id); + setFocus("imageID"); + clearErrors("imageID"); + trigger("imageID"); + }) + .catch((err) => { + setValue("imageID", null); + }); }} onClearClick={() => { onChange(0); setFilename("default.png"); - setValue("imageID", 1); + setValue("imageID", null); setIsImageFileRejected(false); }} browseButtonText="Upload" @@ -580,7 +627,7 @@ export const CustomTargetForm: React.FC = ({ variant={ButtonVariant.primary} isDisabled={!isValid || isSubmitting || isValidating || !isDirty} > - {!ruleset ? t("actions.create") : t("actions.save")} + {!target ? t("actions.create") : t("actions.save")} ), }); - // update ruleset order + // update target order if ( - rulesetOrderSetting.isSuccess && + targetOrderSetting.isSuccess && response.data.id && - rulesetOrderSetting.data + targetOrderSetting.data ) { - rulesetOrderSettingMutation.mutate([ - ...rulesetOrderSetting.data, + targetOrderSettingMutation.mutate([ + ...targetOrderSetting.data, response.data.id, ]); } } setCreateUpdateModalState(null); - refetchrulesets(); + refetchTargets(); }; const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)); function handleDragOver(event: any) { - if (rulesetOrderSetting.isSuccess) { + if (targetOrderSetting.isSuccess) { const { active, over } = event; if (active.id !== over.id) { - const reorderRuleset = (items: number[]) => { + const reorderTarget = (items: number[]) => { const oldIndex = items.indexOf(active.id); const newIndex = items.indexOf(over.id); return arrayMove(items, oldIndex, newIndex); }; - rulesetOrderSettingMutation.mutate( - reorderRuleset(rulesetOrderSetting.data) + targetOrderSettingMutation.mutate( + reorderTarget(targetOrderSetting.data) ); } } @@ -189,7 +183,7 @@ export const MigrationTargets: React.FC = () => { { onClose={() => setCreateUpdateModalState(null)} > setCreateUpdateModalState(null)} /> @@ -212,31 +206,31 @@ export const MigrationTargets: React.FC = () => { onDragOver={handleDragOver} > - {rulesetOrderSetting.isSuccess && - rulesetOrderSetting.data.map((id) => { - const matchingRuleset = rulesets.find( - (Ruleset) => Ruleset.id === id + {targetOrderSetting.isSuccess && + targetOrderSetting.data.map((id) => { + const matchingTarget = targets.find( + (target) => target.id === id ); - if (matchingRuleset) { + if (matchingTarget) { return ( { - if (matchingRuleset) { - setCreateUpdateModalState(matchingRuleset); + if (matchingTarget) { + setCreateUpdateModalState(matchingTarget); } }} onDelete={() => { - const matchingRuleset = rulesets.find( - (Ruleset) => Ruleset.id === id + const matchingTarget = targets.find( + (target) => target.id === id ); - if (matchingRuleset?.id) { - deleteRuleset(matchingRuleset.id); + if (matchingTarget?.id) { + deleteTarget(matchingTarget.id); } }} /> diff --git a/client/src/app/queries/rulesets.ts b/client/src/app/queries/rulesets.ts deleted file mode 100644 index 3ac031064..000000000 --- a/client/src/app/queries/rulesets.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { IReadFile, Metadata, Ruleset } from "@app/api/models"; -import { - createFile, - createRuleset, - deleteRuleset, - getRulesets, - updateRuleset, -} from "@app/api/rest"; -import { AxiosError } from "axios"; -import { getLabels } from "@app/common/CustomRules/rules-utils"; - -export const RulesetsQueryKey = "rulesets"; - -export const useFetchRulesets = () => { - const { data, isLoading, error, refetch } = useQuery( - [RulesetsQueryKey], - async () => await getRulesets(), - { - onError: (err) => console.log(err), - select: (data) => { - return data - .filter((ruleset) => !ruleset.name.startsWith(".")) - .map((ruleset) => { - const mappedRules = ruleset.rules.map((rule) => { - const labels = getLabels(rule.labels || []); - - const transformedMetadata: Metadata = { - source: labels.sourceLabel, - target: labels.targetLabel, - otherLabels: labels.otherLabels, - }; - - return { - ...rule, - metadata: transformedMetadata, - }; - }); - return { ...ruleset, rules: mappedRules }; - }); - }, - } - ); - return { - rulesets: data || [], - isFetching: isLoading, - fetchError: error, - refetch, - }; -}; - -export const useUpdateRulesetMutation = ( - onSuccess: (res: any) => void, - onError: (err: AxiosError) => void -) => { - const queryClient = useQueryClient(); - const { isLoading, mutate, error } = useMutation(updateRuleset, { - onSuccess: (res) => { - onSuccess(res); - queryClient.invalidateQueries([RulesetsQueryKey]); - }, - onError: (err: AxiosError) => { - onError(err); - }, - }); - return { - mutate, - isLoading, - error, - }; -}; - -export const useDeleteRulesetMutation = ( - onSuccess: (res: any, id: number) => void, - onError: (err: AxiosError) => void -) => { - const queryClient = useQueryClient(); - - const { isLoading, mutate, error } = useMutation(deleteRuleset, { - onSuccess: (res, id) => { - onSuccess(res, id); - queryClient.invalidateQueries([RulesetsQueryKey]); - }, - onError: (err: AxiosError) => { - onError(err); - queryClient.invalidateQueries([RulesetsQueryKey]); - }, - }); - return { - mutate, - isLoading, - error, - }; -}; - -export const useCreateRulesetMutation = ( - onSuccess: (res: any) => void, - onError: (err: AxiosError) => void -) => { - const queryClient = useQueryClient(); - const { isLoading, mutate, error } = useMutation(createRuleset, { - onSuccess: (res) => { - onSuccess(res); - queryClient.invalidateQueries([RulesetsQueryKey]); - }, - onError: (err: AxiosError) => { - onError(err); - }, - }); - return { - mutate, - isLoading, - error, - }; -}; - -export const useCreateFileMutation = ( - onSuccess: (res: any, formData: FormData, file: IReadFile) => void, - onError: (err: AxiosError) => void -) => { - const queryClient = useQueryClient(); - const { isLoading, mutate, error } = useMutation(createFile, { - onSuccess: (res, { formData, file }) => { - onSuccess(res, formData, file); - queryClient.invalidateQueries([]); - }, - onError: (err: AxiosError) => { - onError(err); - }, - }); - return { - mutate, - isLoading, - error, - }; -}; diff --git a/client/src/app/queries/targets.ts b/client/src/app/queries/targets.ts new file mode 100644 index 000000000..0f93d6983 --- /dev/null +++ b/client/src/app/queries/targets.ts @@ -0,0 +1,116 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { IReadFile, Target } from "@app/api/models"; +import { + createFile, + createTarget, + deleteTarget, + getTargets, + updateTarget, +} from "@app/api/rest"; +import { AxiosError } from "axios"; + +export const TargetsQueryKey = "targets"; + +export const useFetchTargets = () => { + const { data, isLoading, error, refetch } = useQuery( + [TargetsQueryKey], + async () => await getTargets(), + { + onError: (err) => console.log(err), + } + ); + + return { + targets: data || [], + isFetching: isLoading, + fetchError: error, + refetch, + }; +}; + +export const useUpdateTargetMutation = ( + onSuccess: (res: Target) => void, + onError: (err: AxiosError) => void +) => { + const queryClient = useQueryClient(); + const { isLoading, mutate, error } = useMutation(updateTarget, { + onSuccess: (res) => { + onSuccess(res); + queryClient.invalidateQueries([TargetsQueryKey]); + }, + onError: (err: AxiosError) => { + onError(err); + }, + }); + return { + mutate, + isLoading, + error, + }; +}; + +export const useDeleteTargetMutation = ( + onSuccess: (res: Target, id: number) => void, + onError: (err: AxiosError) => void +) => { + const queryClient = useQueryClient(); + + const { isLoading, mutate, error } = useMutation(deleteTarget, { + onSuccess: (res, id) => { + onSuccess(res, id); + queryClient.invalidateQueries([TargetsQueryKey]); + }, + onError: (err: AxiosError) => { + onError(err); + queryClient.invalidateQueries([TargetsQueryKey]); + }, + }); + return { + mutate, + isLoading, + error, + }; +}; + +export const useCreateTargetMutation = ( + onSuccess: (res: Target) => void, + onError: (err: AxiosError) => void +) => { + const queryClient = useQueryClient(); + const { isLoading, mutate, error } = useMutation(createTarget, { + onSuccess: (res) => { + onSuccess(res); + queryClient.invalidateQueries([TargetsQueryKey]); + }, + onError: (err: AxiosError) => { + onError(err); + }, + }); + return { + mutate, + isLoading, + error, + }; +}; + +export const useCreateFileMutation = ( + onSuccess?: (res: any, formData: FormData, file: IReadFile) => void, + onError?: (err: AxiosError) => void +) => { + const queryClient = useQueryClient(); + const { isLoading, mutate, mutateAsync, error } = useMutation(createFile, { + onSuccess: (res, { formData, file }) => { + onSuccess && onSuccess(res, formData, file); + queryClient.invalidateQueries([]); + }, + onError: (err: AxiosError) => { + onError && onError(err); + }, + }); + return { + mutate, + mutateAsync, + isLoading, + error, + }; +}; diff --git a/package-lock.json b/package-lock.json index d2900c015..21c481f24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "react-dom": "^17.0.2", "react-hook-form": "^7.43.1", "react-i18next": "^11.8.5", - "react-image-file-resizer": "^0.4.8", "react-markdown": "^8.0.7", "react-measure": "^2.5.2", "react-monaco-editor": "0.51.0", @@ -13648,11 +13647,6 @@ } } }, - "node_modules/react-image-file-resizer": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz", - "integrity": "sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ==" - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",