Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add upload YAML questionnaire form #1290

Merged
merged 2 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@
"discardAssessment": "The assessment for <1>{{applicationName}}</1> will be discarded, as well as the review result. Do you wish to continue?",
"leavePage": "Are you sure you want to leave this page? Be sure to save your changes, or they will be lost.",
"pageError": "Oops! Something went wrong.",
"refreshPage": "Try to refresh your page or contact your admin."
"refreshPage": "Try to refresh your page or contact your admin.",
"maxfileSize": "Max file size of 1MB exceeded. Upload a smaller file.",
"dragAndDropFile": "Drag and drop your file here or upload one.",
"uploadYamlFile": "Upload your YAML file"
},
"title": {
"applicationAnalysis": "Application analysis",
Expand Down Expand Up @@ -362,6 +365,7 @@
"unknown": "Unknown",
"unsuitableForContainers": "Unsuitable for containers",
"uploadApplicationFile": "Upload your application file",
"uploadYamlFile": "Upload your YAML file",
"url": "URL",
"user": "User",
"version": "Version",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { getAxiosErrorMessage } from "@app/utils/utils";
import { Questionnaire } from "@app/api/models";
import { useHistory } from "react-router-dom";
import { Paths } from "@app/Paths";
import { ImportQuestionnaireForm } from "@app/pages/assessment/import-questionnaire-form/import-questionnaire-form";

const AssessmentSettings: React.FC = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -382,7 +383,7 @@ const AssessmentSettings: React.FC = () => {
isOpen={isImportModal}
onClose={() => setIsImportModal(false)}
>
<Text>TODO Import questionnaire component</Text>
<ImportQuestionnaireForm onSaved={() => setIsImportModal(false)} />
</Modal>
<Modal
id="download.template.modal"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState } from "react";
import { AxiosResponse } from "axios";
import { useTranslation } from "react-i18next";
import * as yup from "yup";

import {
ActionGroup,
Button,
ButtonVariant,
FileUpload,
Form,
FormHelperText,
HelperText,
HelperTextItem,
} from "@patternfly/react-core";

import { HookFormPFGroupController } from "@app/components/HookFormPFFields";
import { useForm } from "react-hook-form";
import { FileLoadError, IReadFile } from "@app/api/models";
import { yupResolver } from "@hookform/resolvers/yup";
import { useCreateFileMutation } from "@app/queries/targets";

export interface ImportQuestionnaireFormProps {
onSaved: (response?: AxiosResponse) => void;
}
export interface ImportQuestionnaireFormValues {
yamlFile: IReadFile;
}

export const yamlFileSchema: yup.SchemaOf<IReadFile> = yup.object({
fileName: yup.string().required(),
fullFile: yup.mixed<File>(),
loadError: yup.mixed<FileLoadError>(),
loadPercentage: yup.number(),
loadResult: yup.mixed<"danger" | "success" | undefined>(),
data: yup.string(),
responseID: yup.number(),
});

export const ImportQuestionnaireForm: React.FC<
ImportQuestionnaireFormProps
> = ({ onSaved }) => {
const { t } = useTranslation();

const [filename, setFilename] = useState<string>();
const [isFileRejected, setIsFileRejected] = useState(false);
const validationSchema: yup.SchemaOf<ImportQuestionnaireFormValues> = yup
.object()
.shape({
yamlFile: yamlFileSchema,
});
const methods = useForm<ImportQuestionnaireFormValues>({
resolver: yupResolver(validationSchema),
mode: "onChange",
});

const {
handleSubmit,
formState: { isSubmitting, isValidating, isValid, isDirty },
getValues,
setValue,
control,
watch,
setFocus,
clearErrors,
trigger,
reset,
} = methods;

const { mutateAsync: createYamlFileAsync } = useCreateFileMutation();

const handleFileUpload = async (file: File) => {
setFilename(file.name);
const formFile = new FormData();
formFile.append("file", file);

const newYamlFile: IReadFile = {
fileName: file.name,
fullFile: file,
};

return createYamlFileAsync({
formData: formFile,
file: newYamlFile,
});
};

const onSubmit = (values: ImportQuestionnaireFormValues) => {
console.log("values", values);
onSaved();
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<HookFormPFGroupController
control={control}
name="yamlFile"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see many "yamlFile" occurences here, maybe extract it to a const?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved to using the "name" provided by the controller component on subsequent occurrences. TS helps us here by inferring the types from the react hook form form values initialization.

Screenshot 2023-08-23 at 11 20 21 AM

Copy link
Collaborator

@avivtur avivtur Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant maybe to add at the top of the file adding something like: const yamlID = "yamlFile" or similar const name so if the type changes in the future, you can change it in only one place and not multiple places. If it's not possible because of the type inferring than please disregard this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form values are defined in one place so I think that accomplishes what you are suggesting here. When we define the formValues, the internal TS support for react-hook-form allows us to infer the types from usage so that we will get a TS error if there is a spelling error or another issue.

Screenshot 2023-08-24 at 9 13 41 AM Screenshot 2023-08-24 at 9 13 33 AM

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2023-08-24 at 9 17 40 AM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha, thanks for the explanation 😄

label={t("terms.uploadYamlFile")}
fieldId="yamlFile"
helperText={t("dialog.uploadYamlFile")}
renderInput={({ field: { onChange, name }, fieldState: { error } }) => (
<FileUpload
id={`${name}-file-upload`}
name={name}
value={filename}
filename={filename}
filenamePlaceholder={t("dialog.dragAndDropFile")}
dropzoneProps={{
accept: {
"text/yaml": [".yml", ".yaml"],
},
maxSize: 1000000,
onDropRejected: (event) => {
const currentFile = event[0];
if (currentFile.file.size > 1000000) {
methods.setError(name, {
type: "custom",
message: t("dialog.maxFileSize"),
});
}
setIsFileRejected(true);
},
}}
validated={isFileRejected || error ? "error" : "default"}
onFileInputChange={async (_, file) => {
console.log("uploading file", file);
//TODO: handle new api here. This is just a placeholder.
try {
await handleFileUpload(file);
setFocus(name);
clearErrors(name);
trigger(name);
} catch (err) {
//Handle new api error here
}
}}
onClearClick={() => {
//TODO
console.log("clearing file");
avivtur marked this conversation as resolved.
Show resolved Hide resolved
}}
browseButtonText="Upload"
/>
)}
/>

{isFileRejected && (
<FormHelperText>
<HelperText>
<HelperTextItem variant="error">
You should select a YAML file.
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
<ActionGroup>
<Button
type="submit"
aria-label="submit"
id="import-questionnaire-submit-button"
variant={ButtonVariant.primary}
isDisabled={!isValid || isSubmitting || isValidating || !isDirty}
>
{t("actions.import")}
</Button>
</ActionGroup>
</Form>
);
};
Loading