-
Notifications
You must be signed in to change notification settings - Fork 43
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
base: main
Are you sure you want to change the base?
Changes from all commits
4c0e299
472a52a
0a32ae3
654aabf
00008f9
60290cc
0f6ab82
1bdf29a
b0a806a
c61a69f
8919b1e
37c8d47
3797695
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ import { | |
DropdownItem, | ||
Modal, | ||
Tooltip, | ||
FormSelect, | ||
FormSelectOption, | ||
TextContent, | ||
} from "@patternfly/react-core"; | ||
import { | ||
PencilAltIcon, | ||
|
@@ -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, | ||
|
@@ -109,6 +116,7 @@ import { | |
DecoratedApplication, | ||
useDecoratedApplications, | ||
} from "./useDecoratedApplications"; | ||
import yaml from "js-yaml"; | ||
|
||
export const ApplicationsTable: React.FC = () => { | ||
const { t } = useTranslation(); | ||
|
@@ -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 | ||
); | ||
|
@@ -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, | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user should be alerted to an error with at least a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
@@ -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 | ||
|
@@ -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> | ||
); | ||
}; |
There was a problem hiding this comment.
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:
Filter construction is done a lot in the dynamic analysis reporting parts of the ui.
There was a problem hiding this comment.
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!