diff --git a/frontend/src/components/DownloadB64Button/index.tsx b/frontend/src/components/DownloadB64Button/index.tsx new file mode 100644 index 000000000..9a9fc6590 --- /dev/null +++ b/frontend/src/components/DownloadB64Button/index.tsx @@ -0,0 +1,73 @@ +import { Tooltip, type ButtonProps, Button } from "@mui/material"; +import React, { useCallback } from "react"; + +interface Props extends ButtonProps { + base64_content: string; + file_type: string; +} + +export const DownloadB64Button: React.FC = ({ + base64_content, + file_type, + ...props +}) => { + const downloadContent = useCallback(() => { + let href = ""; + switch (file_type) { + case "txt": + href = `data:text/plain;base64,${base64_content}`; + break; + case "plotly_json": + case "json": + href = `data:application/json;base64,${base64_content}`; + break; + case "jpeg": + case "jpg": + case "png": + case "bmp": + case "gif": + case "tiff": + href = `data:image/${file_type};base64,${base64_content}`; + break; + case "svg": + href = `data:image/svg+xml;base64,${base64_content}`; + break; + case "md": + href = `data:text/markdown;base64,${base64_content}`; + break; + case "pdf": + href = `data:application/pdf;base64,${base64_content}`; + break; + case "html": + href = `data:text/html;base64,${base64_content}`; + break; + default: + href = `data:text/plain;base64,${base64_content}`; + break; + } + + const a = document.createElement("a"); // Create + a.href = href; // Image Base64 Goes here + a.download = `download.${file_type}`; // File name Here + a.click(); // Downloaded file + }, [base64_content, file_type]); + + return ( + + + + ); +}; diff --git a/frontend/src/components/RenderB64/index.tsx b/frontend/src/components/RenderB64/index.tsx new file mode 100644 index 000000000..96a23be59 --- /dev/null +++ b/frontend/src/components/RenderB64/index.tsx @@ -0,0 +1,93 @@ +import { Typography } from "@mui/material"; +import { RenderPDF } from "components/RenderPDF"; +import DOMPurify from "dompurify"; +import React, { type CSSProperties } from "react"; +import ReactMarkdown from "react-markdown"; +import Plot from "react-plotly.js"; +import remarkGfm from "remark-gfm"; + +interface Props { + base64_content: string; + file_type: string; + style?: CSSProperties; +} + +export const RenderB64: React.FC = ({ + base64_content, + file_type, + style, +}) => { + if (!base64_content || !file_type) { + return No content; + } + switch (file_type) { + case "txt": + return
{window.atob(base64_content)}
; + case "json": + return ( +
+          {JSON.stringify(JSON.parse(window.atob(base64_content)), null, 2)}
+        
+ ); + case "jpeg": + case "jpg": + case "png": + case "bmp": + case "gif": + case "tiff": + return ( + Content + ); + case "svg": + return ( + + Your browser does not support SVG + + ); + case "md": + return ( +
+ + {window.atob(base64_content)} + + ; +
+ ); + + case "pdf": + return ; + case "html": { + const decodedHTML = atob(base64_content); + const sanitizedHTML = DOMPurify.sanitize(decodedHTML); + + return
; + } + case "plotly_json": { + const utf8String = atob(base64_content); + const decodedJSON = JSON.parse(utf8String); + return ( + + ); + } + default: + return
Unsupported file type
; + } +}; diff --git a/frontend/src/features/myWorkflows/api/runs/getWorkflowRunReport.ts b/frontend/src/features/myWorkflows/api/runs/getWorkflowRunReport.ts new file mode 100644 index 000000000..47d87a827 --- /dev/null +++ b/frontend/src/features/myWorkflows/api/runs/getWorkflowRunReport.ts @@ -0,0 +1,75 @@ +import { type AxiosResponse } from "axios"; +import { useWorkspaces } from "context/workspaces"; +import { dominoApiClient } from "services/clients/domino.client"; +import useSWR from "swr"; + +export interface IGetWorkflowRunResultReportParams { + workflowId: string; + runId: string; +} + +const getWorkflowRunResultReportUrl = ({ + workspace, + workflowId, + runId, +}: Partial) => { + if (workspace && workflowId && runId) { + return `/workspaces/${workspace}/workflows/${workflowId}/runs/${runId}/tasks/report`; + } else { + return null; + } +}; + +/** + * Get workflows using GET /workflows + * @returns workflow + */ +const getWorkflowRunResultReport: ({ + workspace, + workflowId, + runId, +}: Partial< + IGetWorkflowRunResultReportParams & { workspace: string } +>) => Promise< + | AxiosResponse<{ + data: Array<{ base64_content: string; file_type: string }>; + }> + | undefined +> = async ({ workspace, workflowId, runId }) => { + if (workspace && workflowId && runId) { + const url = getWorkflowRunResultReportUrl({ + workspace, + workflowId, + runId, + }); + if (url) return await dominoApiClient.get(url); + } +}; + +/** + * Get workflow runs + * @returns runs as swr response + */ +export const useAuthenticatedGetWorkflowRunResultReport = ( + params: Partial, +) => { + const { workspace } = useWorkspaces(); + if (!workspace) + throw new Error( + "Impossible to fetch workflows without specifying a workspace", + ); + + const url = getWorkflowRunResultReportUrl({ + workspace: workspace.id, + ...params, + }); + + return useSWR( + url, + async () => + await getWorkflowRunResultReport({ + workspace: workspace.id, + ...params, + }).then((data) => data?.data), + ); +}; diff --git a/frontend/src/features/myWorkflows/api/runs/index.ts b/frontend/src/features/myWorkflows/api/runs/index.ts index bc215ffa3..5e4270878 100644 --- a/frontend/src/features/myWorkflows/api/runs/index.ts +++ b/frontend/src/features/myWorkflows/api/runs/index.ts @@ -2,3 +2,4 @@ export * from "./getWorkflowRuns"; export * from "./getWorkflowRunTasks"; export * from "./getWorkflowRunTaskLogs"; export * from "./getWorkflowRunTaskResult"; +export * from "./getWorkflowRunReport"; diff --git a/frontend/src/features/myWorkflows/components/ResultsReport/index.tsx b/frontend/src/features/myWorkflows/components/ResultsReport/index.tsx new file mode 100644 index 000000000..8ae4af979 --- /dev/null +++ b/frontend/src/features/myWorkflows/components/ResultsReport/index.tsx @@ -0,0 +1,105 @@ +import { Grid, Paper } from "@mui/material"; +import { RenderPDF } from "components/RenderPDF"; +import DOMPurify from "dompurify"; +import { useAuthenticatedGetWorkflowRunResultReport } from "features/myWorkflows/api"; +import React from "react"; +import ReactMarkdown from "react-markdown"; +import Plot from "react-plotly.js"; +import { useParams } from "react-router-dom"; +import remarkGfm from "remark-gfm"; + +export const ResultsReport: React.FC = () => { + const { id, runId } = useParams<{ id: string; runId: string }>(); + const { data } = useAuthenticatedGetWorkflowRunResultReport({ + workflowId: id, + runId, + }); + return ( + + + {data?.data?.map((d) => { + switch (d.file_type) { + case "txt": + return
{window.atob(d.base64_content)}
; + case "json": + return ( +
+                  {JSON.stringify(
+                    JSON.parse(window.atob(d.base64_content)),
+                    null,
+                    2,
+                  )}
+                
+ ); + case "jpeg": + case "jpg": + case "png": + case "bmp": + case "gif": + case "tiff": + return ( + Content + ); + case "svg": + return ( + + Your browser does not support SVG + + ); + case "md": + return ( +
+ + {window.atob(d.base64_content)} + + ; +
+ ); + + case "pdf": + return ; + case "html": { + const decodedHTML = atob(d.base64_content); + const sanitizedHTML = DOMPurify.sanitize(decodedHTML); + + return ( +
+ ); + } + case "plotly_json": { + const utf8String = atob(d.base64_content); + const decodedJSON = JSON.parse(utf8String); + return ( + + ); + } + default: + return
Unsupported file type: {d.file_type}
; + } + })} + + + ); +}; diff --git a/frontend/src/features/myWorkflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx b/frontend/src/features/myWorkflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx index 9e8396090..fb1521101 100644 --- a/frontend/src/features/myWorkflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx +++ b/frontend/src/features/myWorkflows/components/WorkflowDetail/CustomTabMenu/TaskResult.tsx @@ -1,16 +1,7 @@ -import { - Button, - CircularProgress, - Container, - Tooltip, - Typography, -} from "@mui/material"; -import { RenderPDF } from "components/RenderPDF"; -import DOMPurify from "dompurify"; -import { useCallback, type CSSProperties } from "react"; -import ReactMarkdown from "react-markdown"; -import Plot from "react-plotly.js"; -import remarkGfm from "remark-gfm"; +import { CircularProgress, Container, Typography } from "@mui/material"; +import { DownloadB64Button } from "components/DownloadB64Button"; +import { RenderB64 } from "components/RenderB64"; +import { type CSSProperties } from "react"; import "./styles.css"; interface ITaskResultProps { @@ -19,9 +10,11 @@ interface ITaskResultProps { file_type?: string; } -export const TaskResult = (props: ITaskResultProps) => { - const { base64_content, file_type } = props; - +export const TaskResult = ({ + base64_content, + file_type, + isLoading, +}: ITaskResultProps) => { const style: CSSProperties = { height: "100%", width: "100%", @@ -31,124 +24,21 @@ export const TaskResult = (props: ITaskResultProps) => { whiteSpace: "pre-wrap", }; - const renderContent = () => { - if (props.isLoading) { - return ; - } - - if (!base64_content || !file_type) { - return No content; - } - switch (file_type) { - case "txt": - return
{window.atob(base64_content)}
; - case "json": - return ( -
-            {JSON.stringify(JSON.parse(window.atob(base64_content)), null, 2)}
-          
- ); - case "jpeg": - case "png": - case "bmp": - case "gif": - case "tiff": - return ( - Content - ); - case "svg": - return ( - - Your browser does not support SVG - - ); - case "md": - return ( -
- - {window.atob(base64_content)} - - ; -
- ); - - case "pdf": - return ; - case "html": { - const decodedHTML = atob(base64_content); - const sanitizedHTML = DOMPurify.sanitize(decodedHTML); - - return
; - } - case "plotly_json": { - const utf8String = atob(base64_content); - const decodedJSON = JSON.parse(utf8String); - return ( - - ); - } - default: - return
Unsupported file type
; - } - }; - - const downloadContent = useCallback(() => { - let href = ""; - switch (file_type) { - case "txt": - href = `data:text/plain;base64,${base64_content}`; - break; - case "plotly_json": - case "json": - href = `data:application/json;base64,${base64_content}`; - break; - case "jpeg": - case "png": - case "bmp": - case "gif": - case "tiff": - href = `data:image/${file_type};base64,${base64_content}`; - break; - case "svg": - href = `data:image/svg+xml;base64,${base64_content}`; - break; - case "md": - href = `data:text/markdown;base64,${base64_content}`; - break; - case "pdf": - href = `data:application/pdf;base64,${base64_content}`; - break; - case "html": - href = `data:text/html;base64,${base64_content}`; - break; - default: - href = `data:text/plain;base64,${base64_content}`; - break; - } - - const a = document.createElement("a"); // Create
- a.href = href; // Image Base64 Goes here - a.download = `download.${file_type}`; // File name Here - a.click(); // Downloaded file - }, [base64_content, file_type]); + if (isLoading) { + return ( + + + + ); + } return ( { overflowX: "hidden", }} > - {renderContent()} + {!!base64_content && !!file_type ? ( + + ) : ( + No content + )} {!!base64_content && !!file_type && ( - - - + )} ); diff --git a/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx b/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx index 902515374..b859cd6d9 100644 --- a/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx +++ b/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx @@ -1,4 +1,5 @@ -import { Card, Grid, Skeleton } from "@mui/material"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; +import { Card, Grid, IconButton, Skeleton, Tooltip } from "@mui/material"; import { DataGrid, type GridColDef } from "@mui/x-data-grid"; import { NoDataOverlay } from "components/NoDataOverlay"; import { useAuthenticatedGetWorkflowRuns } from "features/myWorkflows/api"; @@ -10,6 +11,7 @@ import React, { useMemo, useState, } from "react"; +import { useNavigate } from "react-router-dom"; import { States } from "./States"; import { WorkflowRunTableFooter } from "./WorkflowRunTableFooter"; @@ -37,6 +39,7 @@ export const WorkflowRunsTable = forwardRef( }, ref, ) => { + const navigation = useNavigate(); const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0, @@ -95,6 +98,28 @@ export const WorkflowRunsTable = forwardRef( return ; }, }, + { + field: "actions", + headerName: "", + maxWidth: 10, + renderCell: ({ row }) => ( + + { + navigation( + `/my-workflows/${workflowId}/report/${row.workflow_run_id}`, + ); + }} + disabled={row.state !== "success" && row.state !== "failed"} + > + + + + ), + headerAlign: "center", + align: "center", + sortable: false, + }, ], [], ); diff --git a/frontend/src/features/myWorkflows/pages/ResultsReportPage.tsx b/frontend/src/features/myWorkflows/pages/ResultsReportPage.tsx new file mode 100644 index 000000000..39d9e6a95 --- /dev/null +++ b/frontend/src/features/myWorkflows/pages/ResultsReportPage.tsx @@ -0,0 +1,17 @@ +import { Grid } from "@mui/material"; +import PrivateLayout from "components/PrivateLayout"; +import React from "react"; + +import { ResultsReport } from "../components/ResultsReport"; + +export const ResultsReportPage: React.FC = () => { + return ( + + + + + + + + ); +}; diff --git a/frontend/src/features/myWorkflows/pages/index.ts b/frontend/src/features/myWorkflows/pages/index.ts index aad9c83da..8efce1d3f 100644 --- a/frontend/src/features/myWorkflows/pages/index.ts +++ b/frontend/src/features/myWorkflows/pages/index.ts @@ -1,2 +1,3 @@ export * from "./WorkflowsPage"; export * from "./WorkflowDetailPage"; +export * from "./ResultsReportPage"; diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index c52d1ba87..16c8d7ca1 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -30,6 +30,10 @@ const { WorkflowDetailPage } = lazyImport( async () => await import("features/myWorkflows/pages"), "WorkflowDetailPage", ); +const { ResultsReportPage } = lazyImport( + async () => await import("features/myWorkflows/pages"), + "ResultsReportPage", +); /** * Application router @@ -90,6 +94,13 @@ export const ApplicationRoutes: FC = () => ( } /> + + } + /> +