diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index d1c6edccbc..dc525e6bb6 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -322,21 +322,28 @@ export const getApplicationImports = ( .get(`${APP_IMPORT}?importSummary.id=${importSummaryID}&isValid=${isValid}`) .then((response) => response.data); -export function getTaskById(id: number, format: "json"): Promise; -export function getTaskById(id: number, format: "yaml"): Promise; export function getTaskById( id: number, - format: "json" | "yaml" + format: string, + merged: boolean = false ): Promise { - if (format === "yaml") { - return axios - .get(`${TASKS}/${id}`, yamlHeaders) - .then((response) => response.data); - } else { - return axios - .get(`${TASKS}/${id}`, jsonHeaders) - .then((response) => response.data); + const headers = + format === "yaml" ? { ...yamlHeaders.headers } : { ...jsonHeaders.headers }; + const responseType = format === "yaml" ? "text" : "json"; + + let url = `${TASKS}/${id}`; + if (merged) { + url += "?merged=1"; } + + return axios + .get(url, { + headers: headers, + responseType: responseType, + }) + .then((response) => { + return response.data; + }); } export const getTasks = () => diff --git a/client/src/app/components/SimpleDocumentViewer.css b/client/src/app/components/SimpleDocumentViewer.css index 5e19d7bcc3..a566ed5be9 100644 --- a/client/src/app/components/SimpleDocumentViewer.css +++ b/client/src/app/components/SimpleDocumentViewer.css @@ -74,3 +74,7 @@ .simple-task-viewer .language-toggle-group { --pf-v5-c-toggle-group__button--FontSize: var(--pf-v5-global--FontSize--md); } + +.merged-checkbox { + margin: auto 0.5rem; +} diff --git a/client/src/app/components/SimpleDocumentViewer.tsx b/client/src/app/components/SimpleDocumentViewer.tsx index 8583221587..91dc6c4a32 100644 --- a/client/src/app/components/SimpleDocumentViewer.tsx +++ b/client/src/app/components/SimpleDocumentViewer.tsx @@ -6,6 +6,7 @@ import { } from "@patternfly/react-code-editor"; import { Button, + Checkbox, EmptyState, EmptyStateIcon, EmptyStateVariant, @@ -22,24 +23,17 @@ import CodeIcon from "@patternfly/react-icons/dist/esm/icons/code-icon"; import UndoIcon from "@patternfly/react-icons/dist/esm/icons/undo-icon"; import "./SimpleDocumentViewer.css"; +import { useFetchTaskByID } from "@app/queries/tasks"; export { Language } from "@patternfly/react-code-editor"; -interface FetchFunction { - /** Fetch a yaml document for the given document */ - (documentId: number, format: Language.yaml): Promise; - - /** Fetch a JSON document as a `FetchType` object for the given document */ - (documentId: number, format: Language.json): Promise; -} - /** The subset of MonacoEditor component functions we want to use. */ type ControlledEditor = { focus: () => void; setPosition: (position: object) => void; }; -export interface ISimpleDocumentViewerProps { +export interface ISimpleDocumentViewerProps { /** The id of the document to display, or `undefined` to display the empty state. */ documentId: number | undefined; @@ -57,45 +51,45 @@ export interface ISimpleDocumentViewerProps { * vertical space. Defaults to "450px". */ height?: string | "full"; - - /** Function that will fetch the document to display. */ - fetch: FetchFunction; } /** * Fetch and then use the `@patternfly/react-code-editor` to display a document in * read-only mode with language highlighting applied. */ -export const SimpleDocumentViewer = ({ +export const SimpleDocumentViewer = ({ documentId, downloadFilename, language = Language.yaml, height = "450px", - fetch, -}: ISimpleDocumentViewerProps) => { +}: ISimpleDocumentViewerProps) => { const editorRef = React.useRef(); - - const [code, setCode] = React.useState(undefined); const [currentLanguage, setCurrentLanguage] = React.useState(language); + const [code, setCode] = React.useState(); + const [merged, setMerged] = React.useState(false); + + const { task, isFetching, fetchError, refetch } = useFetchTaskByID( + documentId, + currentLanguage === Language.yaml ? "yaml" : "json", + merged + ); + + const onMergedChange = (checked: boolean) => { + setMerged(checked); + refetch(); + }; React.useEffect(() => { - setCode(undefined); - documentId && fetchDocument(documentId); - }, [documentId, currentLanguage]); + if (task) { + const formattedCode = + currentLanguage === Language.yaml + ? task.toString() + : JSON.stringify(task, undefined, 2); - const fetchDocument = (documentId: number) => { - if (currentLanguage === Language.yaml) { - fetch(documentId, currentLanguage).then((yaml) => { - setCode(yaml.toString()); - focusAndHomePosition(); - }); - } else { - fetch(documentId, currentLanguage).then((json) => { - setCode(JSON.stringify(json, undefined, 2)); - focusAndHomePosition(); - }); + setCode(formattedCode); + focusAndHomePosition(); } - }; + }, [task, currentLanguage]); const focusAndHomePosition = () => { if (editorRef.current) { @@ -103,13 +97,14 @@ export const SimpleDocumentViewer = ({ editorRef.current.setPosition({ column: 0, lineNumber: 1 }); } }; + const refreshControl = ( } aria-label="refresh-task" tooltipProps={{ content: "Refresh" }} onClick={() => { - documentId && fetchDocument(documentId); + refetch(); }} isVisible={code !== ""} /> @@ -147,6 +142,15 @@ export const SimpleDocumentViewer = ({ } customControls={[ refreshControl, + onMergedChange(checked)} + aria-label="Merged Checkbox" + />,
({ ); }; -export interface ISimpleDocumentViewerModalProps - extends ISimpleDocumentViewerProps { +export interface ISimpleDocumentViewerModalProps + extends ISimpleDocumentViewerProps { /** Simple text content of the modal header. */ title?: string; @@ -220,14 +224,14 @@ export interface ISimpleDocumentViewerModalProps * displayed if the `documentId` is set. If `documentId` is `undefined`, the modal is * closed. */ -export const SimpleDocumentViewerModal = ({ +export const SimpleDocumentViewerModal = ({ title, documentId, onClose, position = "top", isFullHeight = true, ...rest -}: ISimpleDocumentViewerModalProps) => { +}: ISimpleDocumentViewerModalProps) => { const isOpen = documentId !== undefined; return ( @@ -248,7 +252,7 @@ export const SimpleDocumentViewerModal = ({ , ]} > - + { if (candidateTasks.length === selectedRows.length) return true; return false; }; + const hasExistingAnalysis = selectedRows.some((app) => tasks.some((task) => task.application?.id === app.id) ); @@ -872,6 +869,10 @@ export const ApplicationsTable: React.FC = () => { > {currentPageItems?.map((application, rowIndex) => { + const hasExistingAnalysis = tasks.some( + (task) => task.application?.id === application.id + ); + return ( { }, ] : []), - ...(analysisReadAccess + ...(analysisReadAccess && hasExistingAnalysis ? [ { title: t("actions.analysisDetails"), @@ -1111,9 +1112,8 @@ export const ApplicationsTable: React.FC = () => { onClose={() => setSaveApplicationModalState(null)} /> - + setTaskToView(undefined)} /> diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx index e53940ab5e..15144d45d8 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx @@ -41,7 +41,6 @@ import { } from "@app/pages/issues/helpers"; import { ApplicationTags } from "../application-tags"; import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants"; -import { getTaskById } from "@app/api/rest"; import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer"; import { useFetchFacts } from "@app/queries/facts"; @@ -425,9 +424,8 @@ export const ApplicationDetailDrawer: React.FC< )} )} - + { setTaskIdToView(undefined); diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index b0e43699f2..23d107a911 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery } from "@tanstack/react-query"; -import { cancelTask, deleteTask, getTasks } from "@app/api/rest"; +import { cancelTask, deleteTask, getTaskById, getTasks } from "@app/api/rest"; interface FetchTasksFilters { addon?: string; @@ -80,3 +80,25 @@ export const useCancelTaskMutation = ( }, }); }; + +export const TaskByIDQueryKey = "taskByID"; + +export const useFetchTaskByID = ( + taskId?: number, + format = "json", + merged = false +) => { + console.log("useFetchTaskByID", taskId, format, merged); + const { isLoading, error, data, refetch } = useQuery({ + queryKey: [TaskByIDQueryKey, taskId, format, merged], + queryFn: () => (taskId ? getTaskById(taskId, format, merged) : null), + enabled: !!taskId, + }); + + return { + task: data, + isFetching: isLoading, + fetchError: error, + refetch, + }; +};