Skip to content

Commit

Permalink
✨ Add component for downloading the static report (#1312)
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
ibolton336 authored Aug 29, 2023
1 parent ba82364 commit 427ed02
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 31 deletions.
10 changes: 0 additions & 10 deletions client/src/app/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,16 +348,6 @@ export const getApplicationImports = (
.get(`${APP_IMPORT}?importSummary.id=${importSummaryID}&isValid=${isValid}`)
.then((response) => response.data);

export const getApplicationAnalysis = (
applicationId: number,
format: "json" | "yaml"
): Promise<string> => {
const headers = format === "yaml" ? yamlHeaders : jsonHeaders;
return axios
.get<string>(`${APPLICATIONS}/${applicationId}/analysis`, headers)
.then((response) => response.data);
};

export function getTaskById(id: number, format: "json"): Promise<Task>;
export function getTaskById(id: number, format: "yaml"): Promise<string>;
export function getTaskById(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
Title,
Tooltip,
Button,
Divider,
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
} from "@patternfly/react-core";
import {
CheckCircleIcon,
Expand All @@ -23,8 +28,10 @@ import { EmptyTextMessage } from "@app/components/EmptyTextMessage";
import { useFetchFacts } from "@app/queries/facts";
import { ApplicationFacts } from "./application-facts";
import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer";
import { getTaskById } from "@app/api/rest";
import { APPLICATIONS, getTaskById } from "@app/api/rest";
import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants";
import { Link } from "react-router-dom";
import DownloadButton, { MimeType } from "./components/download-button";

export interface IApplicationDetailDrawerAnalysisProps
extends Pick<
Expand Down Expand Up @@ -100,23 +107,57 @@ export const ApplicationDetailDrawerAnalysis: React.FC<
</Title>
{task?.state === "Succeeded" && application ? (
<>
<Tooltip content="View Report">
<Button
icon={
<span className={spacing.mrXs}>
<ExclamationCircleIcon
color={COLOR_HEX_VALUES_BY_NAME.blue}
></ExclamationCircleIcon>
</span>
}
type="button"
variant="link"
isInline
onClick={() => setAppAnalysisToView(application.id)}
>
View analysis
</Button>
</Tooltip>
<DescriptionList
isHorizontal
columnModifier={{ default: "2Col" }}
>
<DescriptionListGroup>
<DescriptionListTerm>Details</DescriptionListTerm>
<DescriptionListDescription>
<Tooltip content="View the analysis task details">
<Button
icon={
<span className={spacing.mrXs}>
<ExclamationCircleIcon
color={COLOR_HEX_VALUES_BY_NAME.blue}
></ExclamationCircleIcon>
</span>
}
type="button"
variant="link"
onClick={() => setAppAnalysisToView(application.id)}
className={spacing.ml_0}
style={{ margin: "0", padding: "0" }}
>
View analysis details
</Button>
</Tooltip>
</DescriptionListDescription>
<DescriptionListTerm>Download</DescriptionListTerm>
<DescriptionListDescription>
<Tooltip
content="Click to download Analysis report"
position="top"
>
<DownloadButton
application={application}
mimeType={MimeType.TAR}
/>
</Tooltip>
{" | "}
<Tooltip
content="Click to download Analysis report"
position="top"
>
<DownloadButton
application={application}
mimeType={MimeType.YAML}
/>
</Tooltip>
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<Divider className={spacing.mtMd}></Divider>
</>
) : task?.state === "Failed" ? (
task ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState } from "react";
import { Alert, Button } from "@patternfly/react-core";
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { Application } from "@app/api/models";
import { Spinner } from "@patternfly/react-core";
import { useDownloadStaticReport } from "@app/queries/download";

export enum MimeType {
TAR = "tar",
YAML = "yaml",
}
function DownloadButton({
application,
mimeType,
}: {
application: Application;
mimeType: MimeType;
}) {
const {
mutate: downloadFile,
isLoading,
isError,
} = useDownloadStaticReport();

const handleDownload = () => {
downloadFile({
applicationId: application.id,
mimeType: mimeType,
});
};

return (
<>
{isLoading ? (
<Spinner size="sm" />
) : isError ? (
<Alert variant="warning" isInline title={"Error downloading report"}>
<p>{"An error has occurred. Try to download again."}</p>
</Alert>
) : (
<>
<Button
onClick={handleDownload}
id={`download-${mimeType}-button`}
variant="link"
className={spacing.pXs}
>
{mimeType === MimeType.YAML ? "YAML" : "Report"}
</Button>
</>
)}
</>
);
}

export default DownloadButton;
55 changes: 55 additions & 0 deletions client/src/app/queries/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import axios from "axios";
import { saveAs } from "file-saver";
import { APPLICATIONS } from "@app/api/rest";
import { useMutation } from "@tanstack/react-query";
import { MimeType } from "@app/pages/applications/components/application-detail-drawer/components/download-button";

interface DownloadOptions {
applicationId: number;
mimeType: MimeType;
}

export const downloadStaticReport = async ({
applicationId,
mimeType,
}: DownloadOptions): Promise<void> => {
let acceptHeader = "application/x-tar";

switch (mimeType) {
case MimeType.YAML:
acceptHeader = "application/x-yaml";
break;
case MimeType.TAR:
default:
acceptHeader = "application/x-tar";
}

try {
const response = await axios.get(
`${APPLICATIONS}/${applicationId}/analysis/report`,
{
responseType: "blob",
headers: {
Accept: acceptHeader,
},
}
);

if (response.status !== 200) {
throw new Error("Network response was not ok when downloading file.");
}

const blob = new Blob([response.data]);
saveAs(
blob,
`analysis-report-app-${applicationId}.${acceptHeader.split("-")[1]}`
);
} catch (error) {
console.error("There was an error downloading the file:", error);
throw error;
}
};

export const useDownloadStaticReport = () => {
return useMutation(downloadStaticReport);
};
3 changes: 0 additions & 3 deletions common/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ export const proxyMap: Record<string, Options> = {
},

onProxyReq: (proxyReq, req, res) => {
if (req.originalUrl.includes("windup/report/?filter")) {
proxyReq.setHeader("Accept", "");
}
if (req.cookies?.keycloak_cookie && !req.headers["authorization"]) {
proxyReq.setHeader(
"Authorization",
Expand Down

0 comments on commit 427ed02

Please sign in to comment.