From 61dffa2d8b5afb4eddbb7650079eb1adaf0fb4b5 Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Wed, 22 Feb 2023 12:10:22 -0500 Subject: [PATCH] :sparkles: Custom target api integration (#634) * Upload file initial work Resize image for icon Load real target data Persist target reorder Reflect target reorder in wizard Signed-off-by: ibolton336 * Skip tests that rely on mock targets Signed-off-by: ibolton336 * Add bundle refs to task data Signed-off-by: ibolton336 * Extract rule parsing logic for reuse in custom targets Extract rule parsing logic for reuse Signed-off-by: ibolton336 * Consume shared rule upload component Signed-off-by: ibolton336 * Prepare rule files for custom targets form submit Signed-off-by: ibolton336 * Submit rule bundle Signed-off-by: ibolton336 * Update bundle order on create rule bundle Signed-off-by: ibolton336 * Fix ref id type Signed-off-by: ibolton336 * Load initial value for editing custom targets Signed-off-by: ibolton336 * Add delete functionality Signed-off-by: ibolton336 * Add appropriate modal arch for edit/add Signed-off-by: ibolton336 * Fix saved notification Signed-off-by: ibolton336 * ss Signed-off-by: ibolton336 * Update modal names Signed-off-by: ibolton336 * Get validation working and fix default values Signed-off-by: ibolton336 * Fix wizard custom rules flow Signed-off-by: ibolton336 * Address PR comments Signed-off-by: ibolton336 * remove useEffect Signed-off-by: ibolton336 * Cleanup and set initial image filename Signed-off-by: ibolton336 * Cleanup Signed-off-by: ibolton336 --------- Signed-off-by: ibolton336 --- client/package.json | 3 +- client/src/app/api/models.tsx | 63 ++- client/src/app/api/rest.tsx | 52 ++- .../CustomRules}/add-custom-rules.tsx | 133 ++++-- .../app/common/CustomRules/rules-utils.tsx | 53 +++ .../CustomRules}/windup-jboss-ruleset.xsd | 0 .../src/app/components/DefaultTargetImage.svg | 12 + client/src/app/components/target-card.tsx | 60 +-- client/src/app/data/targets.ts | 112 ----- .../__snapshots__/AppAboutModal.test.tsx.snap | 301 +++++++++++++ .../__tests__/analysis-wizard.test.tsx | 3 +- .../analysis-wizard/analysis-wizard.tsx | 56 +-- .../analysis-wizard/custom-rules.tsx | 116 ++--- .../applications/analysis-wizard/review.tsx | 12 +- .../applications/analysis-wizard/schema.ts | 15 +- .../analysis-wizard/set-options.tsx | 16 +- .../analysis-wizard/set-targets.tsx | 104 +++-- .../migration-targets/components/dnd/item.tsx | 22 +- .../components/dnd/sortable-item.tsx | 8 +- .../new-custom-target-modal/index.ts | 1 + .../new-custom-target-modal.tsx | 29 ++ .../update-custom-target-modal/index.ts | 1 + .../update-custom-target-modal.tsx | 31 ++ .../migration-targets/custom-target-form.tsx | 405 ++++++++++++------ .../migration-targets/migration-targets.tsx | 188 ++++++-- client/src/app/queries/rulebundles.ts | 135 ++++++ client/src/app/queries/rulesets.ts | 68 --- client/src/app/queries/taskgroups.ts | 4 +- package-lock.json | 13 +- 29 files changed, 1435 insertions(+), 581 deletions(-) rename client/src/app/{pages/applications/analysis-wizard/components => common/CustomRules}/add-custom-rules.tsx (64%) create mode 100644 client/src/app/common/CustomRules/rules-utils.tsx rename client/src/app/{pages/applications/analysis-wizard/components => common/CustomRules}/windup-jboss-ruleset.xsd (100%) create mode 100644 client/src/app/components/DefaultTargetImage.svg create mode 100644 client/src/app/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap create mode 100644 client/src/app/pages/migration-targets/components/new-custom-target-modal/index.ts create mode 100644 client/src/app/pages/migration-targets/components/new-custom-target-modal/new-custom-target-modal.tsx create mode 100644 client/src/app/pages/migration-targets/components/update-custom-target-modal/index.ts create mode 100644 client/src/app/pages/migration-targets/components/update-custom-target-modal/update-custom-target-modal.tsx create mode 100644 client/src/app/queries/rulebundles.ts delete mode 100644 client/src/app/queries/rulesets.ts diff --git a/client/package.json b/client/package.json index 1ad6d0d966..0cac3fa5eb 100644 --- a/client/package.json +++ b/client/package.json @@ -44,9 +44,10 @@ "keycloak-js": "^18.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-hook-form": "^7.27.1", + "react-hook-form": "^7.43.1", "react-hot-loader": "^4.13.1", "react-i18next": "^11.8.5", + "react-image-file-resizer": "^0.4.8", "react-measure": "^2.5.2", "react-router-dom": "^5.2.0", "typesafe-actions": "^5.1.0", diff --git a/client/src/app/api/models.tsx b/client/src/app/api/models.tsx index 6edada981e..a539f44670 100644 --- a/client/src/app/api/models.tsx +++ b/client/src/app/api/models.tsx @@ -318,6 +318,11 @@ export interface Setting { value: boolean | undefined; } +export interface BundleOrderSetting { + key: string; + value: number[]; +} + // Analysis Task export type TaskState = @@ -370,6 +375,7 @@ export interface TaskData { tags: { excluded: string[]; }; + bundles: Ref[]; }; } @@ -386,13 +392,6 @@ interface TaskReport { updateUser: string; } -export interface Rule { - name: string; - source: string | null; - target: string | null; - total: number; -} - export interface TaskgroupTask { name: string; data: any; @@ -419,19 +418,51 @@ export interface ITypeOptions { value: string; } -export interface MigrationTargetRule { - name: string; - content: string; +export interface RuleBundleImage { + id: number; + name?: string; } -export interface MigrationTarget { - id?: number; +export enum RuleBundleKind { + CATEGORY = "category", +} + +export interface RuleBundle { + id: number; name: string; description: string; - image: string; - rules?: MigrationTargetRule[]; + image: RuleBundleImage; + kind?: RuleBundleKind; + rulesets: Ruleset[]; custom: boolean; - order?: number; repository?: Repository; - options?: string[][]; +} +export interface Metadata { + target: string; + source?: string; +} +export interface Ruleset { + name: string; + metadata: Metadata; + file?: { + id: number; + }; +} + +export interface TableRule { + name: string; + source: string | null; + target: string | null; + total: number; + fileID?: number; +} + +export interface IReadFile { + fileName: string; + fullFile: File; + loadError?: DOMException; + loadPercentage?: number; + loadResult?: "danger" | "success"; + data?: string; + responseID?: number; } diff --git a/client/src/app/api/rest.tsx b/client/src/app/api/rest.tsx index 7d46b6bb4d..6f25a66e64 100644 --- a/client/src/app/api/rest.tsx +++ b/client/src/app/api/rest.tsx @@ -13,12 +13,14 @@ import { AssessmentRisk, BulkCopyAssessment, BulkCopyReview, + BundleOrderSetting, BusinessService, Identity, + IReadFile, JobFunction, - MigrationTarget, Proxy, Review, + RuleBundle, Setting, Stakeholder, StakeholderGroup, @@ -54,7 +56,8 @@ export const SETTINGS = HUB + "/settings"; export const TASKS = HUB + "/tasks"; export const TASKGROUPS = HUB + "/taskgroups"; -export const RULESETS = HUB + "/rulesets"; +export const RULEBUNDLES = HUB + "/rulebundles"; +export const FILES = HUB + "/files"; // PATHFINDER export const PATHFINDER = "/hub/pathfinder"; @@ -62,6 +65,7 @@ export const ASSESSMENTS = PATHFINDER + "/assessments"; const jsonHeaders = { headers: { Accept: "application/json" } }; const formHeaders = { headers: { Accept: "multipart/form-data" } }; +const fileHeaders = { headers: { Accept: "application/octet-stream" } }; type Direction = "asc" | "desc"; @@ -449,10 +453,15 @@ export const deleteIdentity = (id: number): AxiosPromise => { return APIClient.delete(`${IDENTITIES}/${id}`); }; -export const getSettingById = (id: number | string): AxiosPromise => { - return APIClient.get(`${SETTINGS}/${id}`, jsonHeaders); +export const getSettingById = (id: number | string) => { + return axios.get(`${SETTINGS}/${id}`, jsonHeaders).then((res) => res.data); }; +export const updateBundleOrderSetting = ( + obj: BundleOrderSetting +): AxiosPromise => { + return APIClient.put(`${SETTINGS}/${obj.key}`, obj.value, jsonHeaders); +}; export const updateSetting = (obj: Setting): AxiosPromise => { return APIClient.put( `${SETTINGS}/${obj.key}`, @@ -461,10 +470,6 @@ export const updateSetting = (obj: Setting): AxiosPromise => { ); }; -export const createSetting = (obj: Setting): AxiosPromise => { - return APIClient.post(`${SETTINGS}`, obj); -}; - export const getProxies = (): AxiosPromise> => { return APIClient.get(`${PROXIES}`, jsonHeaders); }; @@ -560,11 +565,30 @@ export const removeFileTaskgroup = ({ ); }; -export const updateMigrationTarget = (obj: MigrationTarget) => - axios.put(`${RULESETS}/${obj.name}`, obj); +export const updateRuleBundle = (obj: RuleBundle) => + axios.put(`${RULEBUNDLES}/${obj.id}`, obj); + +export const createRuleBundle = (obj: RuleBundle) => + axios.post(RULEBUNDLES, obj); + +export const deleteRuleBundle = (id: number) => + axios.delete(`${RULEBUNDLES}/${id}`); + +export const getRuleBundles = () => + axios.get(RULEBUNDLES).then((response) => response.data); -export const createMigrationTarget = (obj: MigrationTarget) => - axios.post(RULESETS, obj).then((response) => response.data); +export const getFileByID = (id: number) => + axios.get(FILES).then((response) => response.data); -export const deleteMigrationTarget = (id: number): AxiosPromise => - axios.delete(`${RULESETS}/${id}`); +export const createFile = ({ + formData, + file, +}: { + formData: FormData; + file: IReadFile; +}) => + axios + .post(`${FILES}/${file.fileName}`, formData, fileHeaders) + .then((response) => { + return response.data; + }); diff --git a/client/src/app/pages/applications/analysis-wizard/components/add-custom-rules.tsx b/client/src/app/common/CustomRules/add-custom-rules.tsx similarity index 64% rename from client/src/app/pages/applications/analysis-wizard/components/add-custom-rules.tsx rename to client/src/app/common/CustomRules/add-custom-rules.tsx index 40f814a686..315f5b7d6c 100644 --- a/client/src/app/pages/applications/analysis-wizard/components/add-custom-rules.tsx +++ b/client/src/app/common/CustomRules/add-custom-rules.tsx @@ -12,13 +12,20 @@ import { XMLValidator } from "fast-xml-parser"; import XSDSchema from "./windup-jboss-ruleset.xsd"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { IReadFile } from "../analysis-wizard"; +import { IReadFile } from "@app/api/models"; +import { useUploadFileMutation } from "@app/queries/taskgroups"; +import { AxiosError } from "axios"; +import { useCreateFileMutation } from "@app/queries/rulebundles"; +import { NotificationsContext } from "@app/shared/notifications-context"; +import { getAxiosErrorMessage } from "@app/utils/utils"; const xmllint = require("xmllint"); interface IAddCustomRules { customRulesFiles: IReadFile[]; readFileData: IReadFile[]; setReadFileData: (setReadFile: React.SetStateAction) => void; + taskgroupID?: number | null; + handleCustomTargetFileChange?: (readFiles: IReadFile[]) => void; } interface IParsedXMLFileStatus { state: "valid" | "error"; @@ -29,10 +36,42 @@ export const AddCustomRules: React.FC = ({ customRulesFiles, readFileData, setReadFileData, + taskgroupID, + handleCustomTargetFileChange, }: IAddCustomRules) => { + const { pushNotification } = React.useContext(NotificationsContext); const [error, setError] = React.useState(""); - const [currentFiles, setCurrentFiles] = React.useState([]); - const [showStatus, setShowStatus] = React.useState(false); + const [currentFiles, setCurrentFiles] = React.useState( + !taskgroupID ? customRulesFiles.map((crf) => crf.fullFile) : [] + ); + const [showStatus, setShowStatus] = React.useState(true); + + const onUploadError = (error: AxiosError) => + console.log("File upload failed: ", error); + + const { mutate: uploadFile } = useUploadFileMutation(() => {}, onUploadError); + + const onCreateRuleFileSuccess = ( + response: any, + formData: FormData, + file: IReadFile + ) => { + //Set file ID for use in form submit + const fileWithID: IReadFile = { ...file, ...{ responseID: response?.id } }; + setReadFileData((prevReadFiles) => [...prevReadFiles, fileWithID]); + }; + + const onCreateRuleFileFailure = (error: AxiosError) => { + pushNotification({ + title: getAxiosErrorMessage(error), + variant: "danger", + }); + }; + + const { mutate: createRuleFile } = useCreateFileMutation( + onCreateRuleFileSuccess, + onCreateRuleFileFailure + ); // only show the status component once a file has been uploaded, but keep the status list component itself even if all files are removed if (!showStatus && currentFiles.length > 0) { @@ -116,21 +155,26 @@ export const AddCustomRules: React.FC = ({ const handleFile = (file: File) => { readFile(file) .then((data) => { - if (isFileIncluded(file.name)) { - const error = new DOMException( - `File "${file.name}" is already uploaded` - ); - handleReadFail(error, 100, file); + if (isFileIncluded(file.name) && !taskgroupID) { + //If existing file loaded in edit mode, add placeholder file for custom target form + handleReadSuccess(data || "", file); } else { - if (data) { - const validatedXMLResult = validateXMLFile(data); - if (validatedXMLResult.state === "valid") - handleReadSuccess(data, file); - else { - const error = new DOMException( - `File "${file.name}" is not a valid XML: ${validatedXMLResult.message}` - ); - handleReadFail(error, 100, file); + if (isFileIncluded(file.name)) { + const error = new DOMException( + `File "${file.name}" is already uploaded` + ); + handleReadFail(error, 100, file); + } else { + if (data) { + const validatedXMLResult = validateXMLFile(data); + if (validatedXMLResult.state === "valid") + handleReadSuccess(data, file); + else { + const error = new DOMException( + `File "${file.name}" is not a valid XML: ${validatedXMLResult.message}` + ); + handleReadFail(error, 100, file); + } } } } @@ -155,17 +199,45 @@ export const AddCustomRules: React.FC = ({ ); setReadFileData(newReadFiles); + handleCustomTargetFileChange && handleCustomTargetFileChange(newReadFiles); }; const handleReadSuccess = (data: string, file: File) => { - const newFile: IReadFile = { - data, - fileName: file.name, - loadResult: "success", - loadPercentage: 100, - fullFile: file, - }; - setReadFileData((prevReadFiles) => [...prevReadFiles, newFile]); + if (taskgroupID) { + // Upload file to bucket if bucket exists / in analysis wizard mode + const newFile: IReadFile = { + data, + fileName: file.name, + loadResult: "success", + loadPercentage: 100, + fullFile: file, + }; + setReadFileData((prevReadFiles) => [...prevReadFiles, newFile]); + const formFile = new FormData(); + formFile.append("file", newFile.fullFile); + uploadFile({ + id: taskgroupID as number, + path: `rules/${newFile.fileName}`, + file: formFile, + }); + } else { + // Use new file api to add file when in custom target edit mode + const newFile: IReadFile = { + data, + fileName: file.name, + loadResult: "success", + loadPercentage: 100, + fullFile: file, + }; + handleCustomTargetFileChange && + handleCustomTargetFileChange([...readFileData, newFile]); + const formFile = new FormData(); + formFile.append("file", newFile.fullFile); + createRuleFile({ + formData: formFile, + file: newFile, + }); + } }; const handleReadFail = ( @@ -184,6 +256,17 @@ export const AddCustomRules: React.FC = ({ fullFile: file, }, ]); + handleCustomTargetFileChange && + handleCustomTargetFileChange([ + ...readFileData, + { + loadError: error, + fileName: file.name, + loadResult: "danger", + loadPercentage: percentage, + fullFile: file, + }, + ]); }; const successfullyReadFileCount = readFileData.filter( diff --git a/client/src/app/common/CustomRules/rules-utils.tsx b/client/src/app/common/CustomRules/rules-utils.tsx new file mode 100644 index 0000000000..6230408c17 --- /dev/null +++ b/client/src/app/common/CustomRules/rules-utils.tsx @@ -0,0 +1,53 @@ +import { IReadFile, TableRule } from "@app/api/models"; + +export const parseRules = (file: IReadFile) => { + if (file.data) { + let source: string | null = null; + let target: string | null = null; + let rulesCount = 0; + + const payload = atob(file.data.substring(21)); + const parser = new DOMParser(); + const xml = parser.parseFromString(payload, "text/xml"); + + const ruleSets = xml.getElementsByTagName("ruleset"); + + if (ruleSets && ruleSets.length > 0) { + const metadata = ruleSets[0].getElementsByTagName("metadata"); + + if (metadata && metadata.length > 0) { + const sources = metadata[0].getElementsByTagName("sourceTechnology"); + if (sources && sources.length > 0) source = sources[0].id; + + const targets = metadata[0].getElementsByTagName("targetTechnology"); + if (targets && targets.length > 0) target = targets[0].id; + } + + const rulesGroup = ruleSets[0].getElementsByTagName("rules"); + if (rulesGroup && rulesGroup.length > 0) + rulesCount = rulesGroup[0].getElementsByTagName("rule").length; + } + + const ruleset: TableRule = { + name: file.fileName, + source: source, + target: target, + total: rulesCount, + ...(file.responseID && { + fileID: file.responseID, + }), + }; + + return { + parsedRuleset: ruleset, + parsedSource: source, + parsedTarget: target, + }; + } else { + return { + parsedRuleset: null, + parsedSource: null, + parsedTarget: null, + }; + } +}; diff --git a/client/src/app/pages/applications/analysis-wizard/components/windup-jboss-ruleset.xsd b/client/src/app/common/CustomRules/windup-jboss-ruleset.xsd similarity index 100% rename from client/src/app/pages/applications/analysis-wizard/components/windup-jboss-ruleset.xsd rename to client/src/app/common/CustomRules/windup-jboss-ruleset.xsd diff --git a/client/src/app/components/DefaultTargetImage.svg b/client/src/app/components/DefaultTargetImage.svg new file mode 100644 index 0000000000..9217cf7e59 --- /dev/null +++ b/client/src/app/components/DefaultTargetImage.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/client/src/app/components/target-card.tsx b/client/src/app/components/target-card.tsx index a8c1a72590..0fecba9134 100644 --- a/client/src/app/components/target-card.tsx +++ b/client/src/app/components/target-card.tsx @@ -22,16 +22,19 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { KebabDropdown } from "@app/shared/components"; import { useTranslation } from "react-i18next"; -import { MigrationTarget } from "@app/api/models"; import "./target-card.css"; +import DefaultRuleBundleIcon from "@app/images/Icon-Red_Hat-Virtual_server_stack-A-Black-RGB.svg"; +import { RuleBundle } from "@app/api/models"; export interface TargetCardProps { - item: MigrationTarget; + item: RuleBundle; cardSelected?: boolean; isEditable?: boolean; - onChange?: (isNewCard: boolean, value: string) => void; + onCardClick?: (isSelecting: boolean, value: string) => void; handleProps?: any; readOnly?: boolean; + onEdit?: () => void; + onDelete?: () => void; } // Force display dropdown box even though there only one option available. @@ -42,14 +45,17 @@ export const TargetCard: React.FC = ({ item, readOnly, cardSelected, - onChange = () => {}, + onCardClick, handleProps, + onEdit, + onDelete, }) => { const { t } = useTranslation(); const [isCardSelected, setCardSelected] = React.useState(cardSelected); - const [isSelectOpen, setSelectOpen] = React.useState(false); - const [selectedRelease, setSelectedRelease] = React.useState( - item.options ? item.options[0][0] : "" + const [isRuleTargetSelectOpen, setRuleTargetSelectOpen] = + React.useState(false); + const [selectedRuleTarget, setSelectedRuleTarget] = React.useState( + item.rulesets[0]?.metadata?.target ); const handleCardClick = (event: React.MouseEvent) => { @@ -58,24 +64,27 @@ export const TargetCard: React.FC = ({ if (eventTarget.type === "button") return; setCardSelected(!isCardSelected); - onChange(!isCardSelected, selectedRelease); + onCardClick && onCardClick(!isCardSelected, selectedRuleTarget); }; - const handleSelectSelection = ( + const handleRuleTargetSelection = ( event: React.MouseEvent | React.ChangeEvent, selection: string | SelectOptionObject ) => { event.stopPropagation(); - setSelectOpen(false); - setSelectedRelease(selection as string); + setRuleTargetSelectOpen(false); + setSelectedRuleTarget(selection as string); }; - const getImage = (): React.ComponentType => { + const getImage = (): React.ComponentType => { let result: React.ComponentType = CubesIcon; + const imagePath = item.image.id + ? `/hub/files/${item.image.id}` + : DefaultRuleBundleIcon; if (item.image) { result = () => ( Card logo @@ -113,10 +122,10 @@ export const TargetCard: React.FC = ({ {!readOnly && item.custom ? ( {}}> + {t("actions.edit")} , - {}}> + {t("actions.delete")} , ]} @@ -132,20 +141,23 @@ export const TargetCard: React.FC = ({ {item.name} - {item.options && - (item.options.length > 1 || forceSelect.includes(item.name)) ? ( + {item.kind === "category" && + (item.rulesets.length > 1 || forceSelect.includes(item.name)) ? ( diff --git a/client/src/app/data/targets.ts b/client/src/app/data/targets.ts index c0a54ff29e..59d02590a3 100644 --- a/client/src/app/data/targets.ts +++ b/client/src/app/data/targets.ts @@ -1,12 +1,4 @@ -import migrationIcon from "@app/images/Icon-Red_Hat-Migration-A-Black-RGB.svg"; -import appOnServerIcon from "@app/images/Icon-Red_Hat-App_on_server-A-Black-RGB.svg"; -import cloudIcon from "@app/images/Icon-Red_Hat-Cloud-A-Black-RGB.svg"; -import serverIcon from "@app/images/Icon-Red_Hat-Server-A-Black-RGB.svg"; -import mugIcon from "@app/images/Icon-Red_Hat-Mug-A-Black-RGB.svg"; -import multiplyIcon from "@app/images/Icon-Red_Hat-Multiply-A-Black-RGB.svg"; -import virtualServerStackIcon from "@app/images/Icon-Red_Hat-Virtual_server_stack-A-Black-RGB.svg"; import { APP_BRAND, BrandType } from "@app/Constants"; -import { MigrationTarget } from "@app/api/models"; const openTargets: string[] = [ "camel", @@ -40,107 +32,3 @@ export const defaultTargets = APP_BRAND === BrandType.Konveyor ? [...openTargets, ...proprietaryTargets] : [...openTargets]; - -export const transformationTargets: MigrationTarget[] = [ - { - name: "Application server migration to", - description: - "Upgrade to the latest Release of JBoss EAP or migrate your applications to JBoss EAP from other Enterprise Application Server (e.g. Oracle WebLogic Server).", - options: [ - ["eap7", "JBoss EAP 7"], - ["eap6", "JBoss EAP 6"], - ], - image: appOnServerIcon, - custom: false, - }, - { - name: "Containerization", - description: - "A comprehensive set of cloud and container readiness rules to assess applications for suitability for deployment on Kubernetes.", - options: [["cloud-readiness"]], - image: cloudIcon, - custom: false, - }, - { - name: "Quarkus", - description: - "Rules to support the migration of Spring Boot applications to Quarkus.", - options: [["quarkus"]], - image: migrationIcon, - custom: false, - }, - { - name: "OracleJDK to OpenJDK", - description: "Rules to support the migration to OpenJDK from OracleJDK.", - options: [["openjdk"]], - image: mugIcon, - custom: false, - }, - { - name: "OpenJDK", - description: - "Rules to support upgrading the version of OpenJDK. Migrate to OpenJDK 11 or OpenJDK 17.", - options: [ - ["openjdk11", "OpenJDK 11"], - ["openjdk17", "OpenJDK 17"], - ], - image: mugIcon, - custom: false, - }, - { - name: "Linux", - description: - "Ensure there are no Microsoft Windows paths hard coded into your applications.", - options: [["linux"]], - image: serverIcon, - custom: false, - }, - { - name: "Jakarta EE 9", - description: - "A collection of rules to support migrating applications from Java EE 8 to Jakarta EE 9. The rules cover project dependencies, package renaming, updating XML Schema namespaces, the renaming of application configuration properties and bootstraping files.", - options: [["jakarta-ee"]], - image: migrationIcon, - custom: false, - }, - { - name: "Spring Boot on Red Hat Runtimes", - description: - "A set of rules for assessing the compatibility of applications against the versions of Spring Boot libraries supported by Red Hat Runtimes.", - options: [["rhr"]], - image: migrationIcon, - custom: false, - }, - { - name: "Open Liberty", - description: - "A comprehensive set of rulesfor migrating traditional WebSphere applications to Open Liberty.", - options: [["openliberty"]], - image: migrationIcon, - custom: false, - }, - { - name: "Camel", - description: - "A comprehensive set of rules for migration from Apache Camel 2 to Apache Camel 3.", - options: [["camel"]], - image: multiplyIcon, - custom: false, - }, - { - name: "Azure", - description: - "Upgrade your Java application so it can be deployed in different flavors of Azure.", - options: - APP_BRAND === BrandType.Konveyor - ? [ - ["azure-appservice", "Azure App Service"], - ["azure-aks", "Azure Kubernetes Service"], - ] - : [["azure-appservice", "Azure App Service"]], - image: virtualServerStackIcon, - // TODO for test purpose only, remove - // custom: false, - custom: true, - }, -]; diff --git a/client/src/app/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap b/client/src/app/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap new file mode 100644 index 0000000000..37d600a7b0 --- /dev/null +++ b/client/src/app/layout/AppAboutModal/tests/__snapshots__/AppAboutModal.test.tsx.snap @@ -0,0 +1,301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppAboutModal 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +