Skip to content

Commit

Permalink
✨ Custom target api integration (#634)
Browse files Browse the repository at this point in the history
* Upload file initial work

Resize image for icon

Load real target data

Persist target reorder

Reflect target reorder in wizard

Signed-off-by: ibolton336 <[email protected]>

* Skip tests that rely on mock targets

Signed-off-by: ibolton336 <[email protected]>

* Add bundle refs to task data

Signed-off-by: ibolton336 <[email protected]>

* Extract rule parsing logic for reuse in custom targets

Extract rule parsing logic for reuse

Signed-off-by: ibolton336 <[email protected]>

* Consume shared rule upload component

Signed-off-by: ibolton336 <[email protected]>

* Prepare rule files for custom targets form submit

Signed-off-by: ibolton336 <[email protected]>

* Submit rule bundle

Signed-off-by: ibolton336 <[email protected]>

* Update bundle order on create rule bundle

Signed-off-by: ibolton336 <[email protected]>

* Fix ref id type

Signed-off-by: ibolton336 <[email protected]>

* Load initial value for editing custom targets

Signed-off-by: ibolton336 <[email protected]>

* Add delete functionality

Signed-off-by: ibolton336 <[email protected]>

* Add appropriate modal arch for edit/add

Signed-off-by: ibolton336 <[email protected]>

* Fix  saved notification

Signed-off-by: ibolton336 <[email protected]>

* ss

Signed-off-by: ibolton336 <[email protected]>

* Update modal names

Signed-off-by: ibolton336 <[email protected]>

* Get validation working and fix default values

Signed-off-by: ibolton336 <[email protected]>

* Fix wizard custom rules flow

Signed-off-by: ibolton336 <[email protected]>

* Address PR comments

Signed-off-by: ibolton336 <[email protected]>

* remove useEffect

Signed-off-by: ibolton336 <[email protected]>

* Cleanup and set initial image filename

Signed-off-by: ibolton336 <[email protected]>

* Cleanup

Signed-off-by: ibolton336 <[email protected]>

---------

Signed-off-by: ibolton336 <[email protected]>
  • Loading branch information
ibolton336 authored Feb 22, 2023
1 parent f005ce6 commit 61dffa2
Show file tree
Hide file tree
Showing 29 changed files with 1,435 additions and 581 deletions.
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 47 additions & 16 deletions client/src/app/api/models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ export interface Setting {
value: boolean | undefined;
}

export interface BundleOrderSetting {
key: string;
value: number[];
}

// Analysis Task

export type TaskState =
Expand Down Expand Up @@ -370,6 +375,7 @@ export interface TaskData {
tags: {
excluded: string[];
};
bundles: Ref[];
};
}

Expand All @@ -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;
Expand All @@ -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;
}
52 changes: 38 additions & 14 deletions client/src/app/api/rest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
AssessmentRisk,
BulkCopyAssessment,
BulkCopyReview,
BundleOrderSetting,
BusinessService,
Identity,
IReadFile,
JobFunction,
MigrationTarget,
Proxy,
Review,
RuleBundle,
Setting,
Stakeholder,
StakeholderGroup,
Expand Down Expand Up @@ -54,14 +56,16 @@ 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";
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";

Expand Down Expand Up @@ -449,10 +453,15 @@ export const deleteIdentity = (id: number): AxiosPromise => {
return APIClient.delete(`${IDENTITIES}/${id}`);
};

export const getSettingById = (id: number | string): AxiosPromise<boolean> => {
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<Setting> => {
return APIClient.put(`${SETTINGS}/${obj.key}`, obj.value, jsonHeaders);
};
export const updateSetting = (obj: Setting): AxiosPromise<Setting> => {
return APIClient.put(
`${SETTINGS}/${obj.key}`,
Expand All @@ -461,10 +470,6 @@ export const updateSetting = (obj: Setting): AxiosPromise<Setting> => {
);
};

export const createSetting = (obj: Setting): AxiosPromise<Setting> => {
return APIClient.post(`${SETTINGS}`, obj);
};

export const getProxies = (): AxiosPromise<Array<Proxy>> => {
return APIClient.get(`${PROXIES}`, jsonHeaders);
};
Expand Down Expand Up @@ -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<RuleBundle>(RULEBUNDLES, obj);

export const deleteRuleBundle = (id: number) =>
axios.delete(`${RULEBUNDLES}/${id}`);

export const getRuleBundles = () =>
axios.get<RuleBundle[]>(RULEBUNDLES).then((response) => response.data);

export const createMigrationTarget = (obj: MigrationTarget) =>
axios.post<MigrationTarget>(RULESETS, obj).then((response) => response.data);
export const getFileByID = (id: number) =>
axios.get<RuleBundle[]>(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<RuleBundle>(`${FILES}/${file.fileName}`, formData, fileHeaders)
.then((response) => {
return response.data;
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<IReadFile[]>) => void;
taskgroupID?: number | null;
handleCustomTargetFileChange?: (readFiles: IReadFile[]) => void;
}
interface IParsedXMLFileStatus {
state: "valid" | "error";
Expand All @@ -29,10 +36,42 @@ export const AddCustomRules: React.FC<IAddCustomRules> = ({
customRulesFiles,
readFileData,
setReadFileData,
taskgroupID,
handleCustomTargetFileChange,
}: IAddCustomRules) => {
const { pushNotification } = React.useContext(NotificationsContext);
const [error, setError] = React.useState("");
const [currentFiles, setCurrentFiles] = React.useState<File[]>([]);
const [showStatus, setShowStatus] = React.useState(false);
const [currentFiles, setCurrentFiles] = React.useState<File[]>(
!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) {
Expand Down Expand Up @@ -116,21 +155,26 @@ export const AddCustomRules: React.FC<IAddCustomRules> = ({
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);
}
}
}
}
Expand All @@ -155,17 +199,45 @@ export const AddCustomRules: React.FC<IAddCustomRules> = ({
);

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 = (
Expand All @@ -184,6 +256,17 @@ export const AddCustomRules: React.FC<IAddCustomRules> = ({
fullFile: file,
},
]);
handleCustomTargetFileChange &&
handleCustomTargetFileChange([
...readFileData,
{
loadError: error,
fileName: file.name,
loadResult: "danger",
loadPercentage: percentage,
fullFile: file,
},
]);
};

const successfullyReadFileCount = readFileData.filter(
Expand Down
Loading

0 comments on commit 61dffa2

Please sign in to comment.