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

✨ Bulk download analysis details #2142

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
14 changes: 14 additions & 0 deletions client/src/app/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,20 @@ export function getTaskByIdAndFormat(
});
}

export function getTasksByIds(ids: number[]): Promise<Task[]> {
const filterParam = `id:(${ids.join("|")})`;

return axios
.get<Task[]>(`${TASKS}`, {
params: {
filter: filterParam,
},
})
Copy link
Contributor

@jortel jortel Oct 28, 2024

Choose a reason for hiding this comment

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

Is there a reasons this does not use the existing List endpoint with a filter?
Example:

GET /tasks?filter=id:(1|2|3)

Filter construction is done a lot in the dynamic analysis reporting parts of the ui.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In light of @jortel feedback, I have updated the code to utilize the existing List endpoint with a filter, as suggested. Thank you for your input!

.then((response) => {
return response.data;
});
}

export const getTasksDashboard = () =>
axios
.get<TaskDashboard[]>(`${TASKS}/report/dashboard`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
DropdownItem,
Modal,
Tooltip,
FormSelect,
FormSelectOption,
TextContent,
} from "@patternfly/react-core";
import {
PencilAltIcon,
Expand Down Expand Up @@ -71,7 +74,11 @@ import { checkAccess } from "@app/utils/rbac-utils";
import { useLocalTableControls } from "@app/hooks/table-controls";

// Queries
import { getArchetypeById, getAssessmentsByItemId } from "@app/api/rest";
import {
getArchetypeById,
getAssessmentsByItemId,
getTasksByIds,
} from "@app/api/rest";
import { Assessment, Ref } from "@app/api/models";
import {
useBulkDeleteApplicationMutation,
Expand Down Expand Up @@ -109,6 +116,7 @@ import {
DecoratedApplication,
useDecoratedApplications,
} from "./useDecoratedApplications";
import yaml from "js-yaml";

export const ApplicationsTable: React.FC = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -145,8 +153,13 @@ export const ApplicationsTable: React.FC = () => {

const [applicationDependenciesToManage, setApplicationDependenciesToManage] =
useState<DecoratedApplication | null>(null);

const isDependenciesModalOpen = applicationDependenciesToManage !== null;

const [isDownloadModalOpen, setIsDownloadModalOpen] = useState(false);

const [selectedFormat, setSelectedFormat] = useState<string>("json");

const [assessmentToEdit, setAssessmentToEdit] = useState<Assessment | null>(
null
);
Expand All @@ -167,6 +180,18 @@ export const ApplicationsTable: React.FC = () => {
dayjs()
);

const onChange = (
_event: React.FormEvent<HTMLSelectElement>,
value: string
) => {
setSelectedFormat(value);
};
const formats = [
{ value: "select one", label: "Select one", disabled: true },
{ value: "json", label: "JSON", disabled: false },
{ value: "yaml", label: "YAML", disabled: false },
];

const [
saveApplicationsCredentialsModalState,
setSaveApplicationsCredentialsModalState,
Expand Down Expand Up @@ -194,6 +219,42 @@ export const ApplicationsTable: React.FC = () => {
});
};

const handleDownload = async () => {
const ids = selectedRows
.map((row) => row.tasks.currentAnalyzer?.id)
.filter((id): id is number => typeof id === "number");

try {
const tasks = await getTasksByIds(ids);
const data =
selectedFormat === "yaml"
? yaml.dump(tasks, { indent: 2 })
: JSON.stringify(tasks, null, 2);
Comment on lines +229 to +232
Copy link
Member

Choose a reason for hiding this comment

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

If the data is coming back in the requested "selectedFormat" why is it being formatted again?

Copy link
Contributor Author

@Shevijacobson Shevijacobson Oct 31, 2024

Choose a reason for hiding this comment

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

I realized that I was unnecessarily sending the selectedFormat parameter in the server request. Since the server function does not currently support returning data in that format, I’ve adjusted the implementation to handle JSON formatting on the UI side instead


const blob = new Blob([data], {
type:
selectedFormat === "json" ? "application/json" : "application/x-yaml",
});
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = `logs - ${ids}.${selectedFormat}`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(url);

setIsDownloadModalOpen(false);
} catch (error) {
setIsDownloadModalOpen(false);
console.error("Error fetching tasks:", error);
Copy link
Member

Choose a reason for hiding this comment

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

The user should be alerted to an error with at least a pushNotification()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your valuable feedback. I have implemented the pushNotification() as you suggested to enhance user error alerts.

pushNotification({
title: "download failed",
variant: "danger",
});
}
};

const failedCancelTask = () => {
pushNotification({
title: "Task",
Expand Down Expand Up @@ -575,6 +636,20 @@ export const ApplicationsTable: React.FC = () => {
>
{t("actions.delete")}
</DropdownItem>,
<DropdownItem
key="analysis-bulk-download"
isDisabled={
!selectedRows.some(
(application: DecoratedApplication) =>
application.tasks.currentAnalyzer?.id !== undefined
)
}
onClick={() => {
setIsDownloadModalOpen(true);
}}
>
{t("actions.download", { what: "analysis details" })}
</DropdownItem>,
...(credentialsReadAccess
? [
<DropdownItem
Expand Down Expand Up @@ -1302,6 +1377,41 @@ export const ApplicationsTable: React.FC = () => {
}}
/>
</div>
<Modal
variant="small"
title={t("actions.download", { what: "analysis details" })}
isOpen={isDownloadModalOpen}
onClose={() => setIsDownloadModalOpen(false)}
actions={[
<Button key="confirm" variant="primary" onClick={handleDownload}>
Download
</Button>,
<Button
key="cancel"
variant="link"
onClick={() => setIsDownloadModalOpen(false)}
>
Cancel
</Button>,
]}
>
<TextContent>{"Select format"}</TextContent>
<FormSelect
value={selectedFormat}
onChange={onChange}
aria-label="FormSelect Input"
ouiaId="BasicFormSelect"
>
{formats.map((option, index) => (
<FormSelectOption
isDisabled={option.disabled}
key={index}
value={option.value}
label={option.label}
/>
))}
</FormSelect>
</Modal>
</ConditionalRender>
);
};
Loading