Skip to content

Commit 427ed02

Browse files
authored
✨ Add component for downloading the static report (#1312)
- Adds a download button to handle downloading the analysis report with custom headers driven by props passed in - Uses the Fetch API to allow setting custom headers. Converts the received data to a blob & then creates an object URL from that blob. Then we can use that URL with a temporary anchor `<a>` element to trigger the download with a localized loading state. - This approach allows us to bin any custom headers we were adding in the proxy and just use the fetch api to set the custom headers. ![image](https://github.com/konveyor/tackle2-ui/assets/11218376/3b30ee73-d5bc-4a1e-be32-4ca079321826) Signed-off-by: ibolton336 <[email protected]>
1 parent ba82364 commit 427ed02

File tree

5 files changed

+170
-31
lines changed

5 files changed

+170
-31
lines changed

client/src/app/api/rest.ts

-10
Original file line numberDiff line numberDiff line change
@@ -348,16 +348,6 @@ export const getApplicationImports = (
348348
.get(`${APP_IMPORT}?importSummary.id=${importSummaryID}&isValid=${isValid}`)
349349
.then((response) => response.data);
350350

351-
export const getApplicationAnalysis = (
352-
applicationId: number,
353-
format: "json" | "yaml"
354-
): Promise<string> => {
355-
const headers = format === "yaml" ? yamlHeaders : jsonHeaders;
356-
return axios
357-
.get<string>(`${APPLICATIONS}/${applicationId}/analysis`, headers)
358-
.then((response) => response.data);
359-
};
360-
361351
export function getTaskById(id: number, format: "json"): Promise<Task>;
362352
export function getTaskById(id: number, format: "yaml"): Promise<string>;
363353
export function getTaskById(

client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-analysis.tsx

+59-18
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import {
66
Title,
77
Tooltip,
88
Button,
9+
Divider,
10+
DescriptionList,
11+
DescriptionListGroup,
12+
DescriptionListTerm,
13+
DescriptionListDescription,
914
} from "@patternfly/react-core";
1015
import {
1116
CheckCircleIcon,
@@ -23,8 +28,10 @@ import { EmptyTextMessage } from "@app/components/EmptyTextMessage";
2328
import { useFetchFacts } from "@app/queries/facts";
2429
import { ApplicationFacts } from "./application-facts";
2530
import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer";
26-
import { getTaskById } from "@app/api/rest";
31+
import { APPLICATIONS, getTaskById } from "@app/api/rest";
2732
import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants";
33+
import { Link } from "react-router-dom";
34+
import DownloadButton, { MimeType } from "./components/download-button";
2835

2936
export interface IApplicationDetailDrawerAnalysisProps
3037
extends Pick<
@@ -100,23 +107,57 @@ export const ApplicationDetailDrawerAnalysis: React.FC<
100107
</Title>
101108
{task?.state === "Succeeded" && application ? (
102109
<>
103-
<Tooltip content="View Report">
104-
<Button
105-
icon={
106-
<span className={spacing.mrXs}>
107-
<ExclamationCircleIcon
108-
color={COLOR_HEX_VALUES_BY_NAME.blue}
109-
></ExclamationCircleIcon>
110-
</span>
111-
}
112-
type="button"
113-
variant="link"
114-
isInline
115-
onClick={() => setAppAnalysisToView(application.id)}
116-
>
117-
View analysis
118-
</Button>
119-
</Tooltip>
110+
<DescriptionList
111+
isHorizontal
112+
columnModifier={{ default: "2Col" }}
113+
>
114+
<DescriptionListGroup>
115+
<DescriptionListTerm>Details</DescriptionListTerm>
116+
<DescriptionListDescription>
117+
<Tooltip content="View the analysis task details">
118+
<Button
119+
icon={
120+
<span className={spacing.mrXs}>
121+
<ExclamationCircleIcon
122+
color={COLOR_HEX_VALUES_BY_NAME.blue}
123+
></ExclamationCircleIcon>
124+
</span>
125+
}
126+
type="button"
127+
variant="link"
128+
onClick={() => setAppAnalysisToView(application.id)}
129+
className={spacing.ml_0}
130+
style={{ margin: "0", padding: "0" }}
131+
>
132+
View analysis details
133+
</Button>
134+
</Tooltip>
135+
</DescriptionListDescription>
136+
<DescriptionListTerm>Download</DescriptionListTerm>
137+
<DescriptionListDescription>
138+
<Tooltip
139+
content="Click to download Analysis report"
140+
position="top"
141+
>
142+
<DownloadButton
143+
application={application}
144+
mimeType={MimeType.TAR}
145+
/>
146+
</Tooltip>
147+
{" | "}
148+
<Tooltip
149+
content="Click to download Analysis report"
150+
position="top"
151+
>
152+
<DownloadButton
153+
application={application}
154+
mimeType={MimeType.YAML}
155+
/>
156+
</Tooltip>
157+
</DescriptionListDescription>
158+
</DescriptionListGroup>
159+
</DescriptionList>
160+
<Divider className={spacing.mtMd}></Divider>
120161
</>
121162
) : task?.state === "Failed" ? (
122163
task ? (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useState } from "react";
2+
import { Alert, Button } from "@patternfly/react-core";
3+
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
4+
import { Application } from "@app/api/models";
5+
import { Spinner } from "@patternfly/react-core";
6+
import { useDownloadStaticReport } from "@app/queries/download";
7+
8+
export enum MimeType {
9+
TAR = "tar",
10+
YAML = "yaml",
11+
}
12+
function DownloadButton({
13+
application,
14+
mimeType,
15+
}: {
16+
application: Application;
17+
mimeType: MimeType;
18+
}) {
19+
const {
20+
mutate: downloadFile,
21+
isLoading,
22+
isError,
23+
} = useDownloadStaticReport();
24+
25+
const handleDownload = () => {
26+
downloadFile({
27+
applicationId: application.id,
28+
mimeType: mimeType,
29+
});
30+
};
31+
32+
return (
33+
<>
34+
{isLoading ? (
35+
<Spinner size="sm" />
36+
) : isError ? (
37+
<Alert variant="warning" isInline title={"Error downloading report"}>
38+
<p>{"An error has occurred. Try to download again."}</p>
39+
</Alert>
40+
) : (
41+
<>
42+
<Button
43+
onClick={handleDownload}
44+
id={`download-${mimeType}-button`}
45+
variant="link"
46+
className={spacing.pXs}
47+
>
48+
{mimeType === MimeType.YAML ? "YAML" : "Report"}
49+
</Button>
50+
</>
51+
)}
52+
</>
53+
);
54+
}
55+
56+
export default DownloadButton;

client/src/app/queries/download.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import axios from "axios";
2+
import { saveAs } from "file-saver";
3+
import { APPLICATIONS } from "@app/api/rest";
4+
import { useMutation } from "@tanstack/react-query";
5+
import { MimeType } from "@app/pages/applications/components/application-detail-drawer/components/download-button";
6+
7+
interface DownloadOptions {
8+
applicationId: number;
9+
mimeType: MimeType;
10+
}
11+
12+
export const downloadStaticReport = async ({
13+
applicationId,
14+
mimeType,
15+
}: DownloadOptions): Promise<void> => {
16+
let acceptHeader = "application/x-tar";
17+
18+
switch (mimeType) {
19+
case MimeType.YAML:
20+
acceptHeader = "application/x-yaml";
21+
break;
22+
case MimeType.TAR:
23+
default:
24+
acceptHeader = "application/x-tar";
25+
}
26+
27+
try {
28+
const response = await axios.get(
29+
`${APPLICATIONS}/${applicationId}/analysis/report`,
30+
{
31+
responseType: "blob",
32+
headers: {
33+
Accept: acceptHeader,
34+
},
35+
}
36+
);
37+
38+
if (response.status !== 200) {
39+
throw new Error("Network response was not ok when downloading file.");
40+
}
41+
42+
const blob = new Blob([response.data]);
43+
saveAs(
44+
blob,
45+
`analysis-report-app-${applicationId}.${acceptHeader.split("-")[1]}`
46+
);
47+
} catch (error) {
48+
console.error("There was an error downloading the file:", error);
49+
throw error;
50+
}
51+
};
52+
53+
export const useDownloadStaticReport = () => {
54+
return useMutation(downloadStaticReport);
55+
};

common/src/proxies.ts

-3
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ export const proxyMap: Record<string, Options> = {
3939
},
4040

4141
onProxyReq: (proxyReq, req, res) => {
42-
if (req.originalUrl.includes("windup/report/?filter")) {
43-
proxyReq.setHeader("Accept", "");
44-
}
4542
if (req.cookies?.keycloak_cookie && !req.headers["authorization"]) {
4643
proxyReq.setHeader(
4744
"Authorization",

0 commit comments

Comments
 (0)