diff --git a/frontend/src/app/App.scss b/frontend/src/app/App.scss index 60c55f9db1..3e4dd89d26 100644 --- a/frontend/src/app/App.scss +++ b/frontend/src/app/App.scss @@ -41,6 +41,12 @@ body, inset-inline-start: 12px; } } + .pf-topology-container { + overflow-y: hidden; + .pf-v5-c-drawer__panel.pf-m-resizable { + min-width: 350px; + } + } } // specificity targeting form elements to override --pf-v5-global--FontSize--md diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index 915fcdec68..e863ba7942 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx @@ -3,11 +3,9 @@ import { useNavigate, useParams } from 'react-router-dom'; import { Breadcrumb, BreadcrumbItem, - Drawer, - DrawerContent, - DrawerContentBody, Flex, FlexItem, + PageSection, Tab, TabContent, TabContentBody, @@ -82,92 +80,96 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = ); } + const panelContent = selectedNode ? ( + setSelectedId(null)} + /> + ) : null; + return ( <> - - setSelectedId(null)} - /> - } - > - - - {breadcrumbPath()} - - {/* TODO: Remove the custom className after upgrading to PFv6 */} - - - - {/* TODO: Remove the custom className after upgrading to PFv6 */} - - - - } - title={ - + {breadcrumbPath()} + + {/* TODO: Remove the custom className after upgrading to PFv6 */} + + + + {/* TODO: Remove the custom className after upgrading to PFv6 */} + + + + } + title={ + + } + {...(pipelineVersion && { + description: ( + + ), + })} + empty={false} + loaded={isLoaded} + headerAction={ + isPipelineVersionLoaded && ( + + + + navigate( + routePipelineDetailsNamespace( + namespace, + version.pipeline_id, + version.pipeline_version_id, + ), + ) + } /> - } - {...(pipelineVersion && { - description: ( - + + {isLoaded && ( + setDeletionOpen(true)} + pipeline={pipeline} + pipelineVersion={pipelineVersion} /> - ), - })} - empty={false} - loaded={isLoaded} - headerAction={ - isPipelineVersionLoaded && ( - - - - navigate( - routePipelineDetailsNamespace( - namespace, - version.pipeline_id, - version.pipeline_version_id, - ), - ) - } - /> - - - {isLoaded && ( - setDeletionOpen(true)} - pipeline={pipeline} - pipelineVersion={pipelineVersion} - /> - )} - - - ) - } - > + )} + + + ) + } + > + + + { setActiveTabKey(tabIndex); @@ -182,7 +184,6 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = aria-label="Pipeline Graph Tab" tabContentId={`tabContent-${PipelineDetailsTab.GRAPH}`} /> - Summary} @@ -192,7 +193,6 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = - Pipeline spec} @@ -201,54 +201,50 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = tabContentId={`tabContent-${PipelineDetailsTab.YAML}`} /> -
- - -
-
-
-
-
+ + + + + + + + {pipeline && ( = ({ t } return ( - + {task.name} {task.type === 'artifact' ? 'Artifact details' : ''} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index ebd7bb8098..6cb2b98be4 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import { Breadcrumb, BreadcrumbItem, - Drawer, - DrawerContent, EmptyState, EmptyStateIcon, EmptyStateVariant, @@ -96,99 +94,87 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, const runType = run?.storage_state === StorageStateKF.ARCHIVED ? PipelineRunType.ARCHIVED : undefined; + const panelContent = selectedNode ? ( + <PipelineRunDrawerRightContent + task={selectedNode.data.pipelineTask} + upstreamTaskName={selectedNode.runAfterTasks?.[0]} + onClose={() => setSelectedId(null)} + executions={executions} + /> + ) : null; + return ( <> - <Drawer isExpanded={!!selectedNode}> - <DrawerContent - panelContent={ - <PipelineRunDrawerRightContent - task={selectedNode?.data.pipelineTask} - upstreamTaskName={selectedNode?.runAfterTasks?.[0]} - onClose={() => setSelectedId(null)} - executions={executions} + <ApplicationsPage + title={ + run ? <PipelineDetailsTitle run={run} statusIcon pipelineRunLabel /> : 'Error loading run' + } + subtext={ + run && ( + <PipelineJobReferenceName + runName={run.display_name} + recurringRunId={run.recurring_run_id} /> - } - > - <ApplicationsPage - title={ - run ? ( - <PipelineDetailsTitle run={run} statusIcon pipelineRunLabel /> - ) : ( - 'Error loading run' - ) - } - subtext={ - run && ( - <PipelineJobReferenceName - runName={run.display_name} - recurringRunId={run.recurring_run_id} - /> - ) - } - description={ - run?.description ? <MarkdownView conciseDisplay markdown={run.description} /> : '' - } - loaded={loaded} - loadError={error} - breadcrumb={ - <Breadcrumb> - {breadcrumbPath(runType)} - <BreadcrumbItem isActive style={{ maxWidth: 300 }}> - {version ? ( - <Link - to={routePipelineVersionRunsNamespace( - namespace, - version.pipeline_id, - version.pipeline_version_id, - runType, - )} - > - {/* TODO: Remove the custom className after upgrading to PFv6 */} - <Truncate content={version.display_name} className="truncate-no-min-width" /> - </Link> - ) : ( - 'Loading...' + ) + } + description={ + run?.description ? <MarkdownView conciseDisplay markdown={run.description} /> : '' + } + loaded={loaded} + loadError={error} + breadcrumb={ + <Breadcrumb> + {breadcrumbPath(runType)} + <BreadcrumbItem isActive style={{ maxWidth: 300 }}> + {version ? ( + <Link + to={routePipelineVersionRunsNamespace( + namespace, + version.pipeline_id, + version.pipeline_version_id, + runType, )} - </BreadcrumbItem> - <BreadcrumbItem isActive style={{ maxWidth: 300 }}> + > {/* TODO: Remove the custom className after upgrading to PFv6 */} - <Truncate - content={run?.display_name ?? 'Loading...'} - className="truncate-no-min-width" - /> - </BreadcrumbItem> - </Breadcrumb> - } - headerAction={ - <PipelineRunDetailsActions - run={run} - onDelete={() => setDeleting(true)} - onArchive={() => setArchiving(true)} + <Truncate content={version.display_name} className="truncate-no-min-width" /> + </Link> + ) : ( + 'Loading...' + )} + </BreadcrumbItem> + <BreadcrumbItem isActive style={{ maxWidth: 300 }}> + {/* TODO: Remove the custom className after upgrading to PFv6 */} + <Truncate + content={run?.display_name ?? 'Loading...'} + className="truncate-no-min-width" /> - } - empty={false} - > - <PipelineRunDetailsTabs - run={run} - pipelineSpec={version?.pipeline_spec} - graphContent={ - <PipelineTopology - nodes={nodes} - selectedIds={selectedId ? [selectedId] : []} - onSelectionChange={(ids) => { - const firstId = ids[0]; - if (ids.length === 0) { - setSelectedId(null); - } else if (nodes.find((node) => node.id === firstId)) { - setSelectedId(firstId); - } - }} - /> - } + </BreadcrumbItem> + </Breadcrumb> + } + headerAction={ + <PipelineRunDetailsActions + run={run} + onDelete={() => setDeleting(true)} + onArchive={() => setArchiving(true)} + /> + } + empty={false} + > + <PipelineRunDetailsTabs + run={run} + pipelineSpec={version?.pipeline_spec} + graphContent={ + <PipelineTopology + nodes={nodes} + selectedIds={selectedId ? [selectedId] : []} + onSelectionChange={(ids) => { + setSelectedId(ids.length ? ids[0] : null); + }} + sidePanel={panelContent} /> - </ApplicationsPage> - </DrawerContent> - </Drawer> + } + /> + </ApplicationsPage> <DeletePipelineRunsModal type={PipelineRunType.ARCHIVED} toDeleteResources={deleting && run ? [run] : []} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx index 908cd4525a..fa076175e7 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx @@ -1,6 +1,15 @@ import React from 'react'; -import { Tabs, Tab, TabTitleText, TabContentBody, TabContent } from '@patternfly/react-core'; +import { + Tabs, + Tab, + TabTitleText, + TabContentBody, + TabContent, + PageSection, + FlexItem, + Flex, +} from '@patternfly/react-core'; import PipelineDetailsYAML from '~/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML'; import { @@ -32,62 +41,72 @@ export const PipelineRunDetailsTabs: React.FC<PipelineRunDetailsTabsProps> = ({ const isJob = run && isPipelineRunJob(run); return ( - <> - <Tabs - activeKey={activeKey} - onSelect={(_, eventKey) => setActiveKey(eventKey)} - aria-label="Pipeline run details tabs" + <PageSection + isFilled + padding={{ default: 'noPadding' }} + style={{ flexBasis: 0, overflowY: 'hidden' }} + variant="light" + > + <Flex + direction={{ default: 'column' }} + style={{ height: '100%' }} + spaceItems={{ default: 'spaceItemsNone' }} > - <Tab - eventKey={DetailsTabKey.Graph} - tabContentId={DetailsTabKey.Graph} - title={<TabTitleText>Graph</TabTitleText>} - aria-label="Run graph tab" - data-testid="pipeline-run-tab-graph" - /> - - <Tab - eventKey={DetailsTabKey.Details} - title={<TabTitleText>Details</TabTitleText>} - aria-label="Run details tab" - data-testid="pipeline-run-tab-details" - > - <TabContentBody hasPadding> - <PipelineRunTabDetails workflowName={run?.display_name} run={run} /> - </TabContentBody> - </Tab> - - {!isJob && pipelineSpec && ( - <Tab + <FlexItem> + <Tabs + activeKey={activeKey} + onSelect={(_, eventKey) => setActiveKey(eventKey)} + aria-label="Pipeline run details tabs" + > + <Tab + eventKey={DetailsTabKey.Graph} + tabContentId={DetailsTabKey.Graph} + title={<TabTitleText>Graph</TabTitleText>} + aria-label="Run graph tab" + data-testid="pipeline-run-tab-graph" + /> + <Tab + eventKey={DetailsTabKey.Details} + title={<TabTitleText>Details</TabTitleText>} + aria-label="Run details tab" + data-testid="pipeline-run-tab-details" + > + <TabContentBody hasPadding> + <PipelineRunTabDetails workflowName={run?.display_name} run={run} /> + </TabContentBody> + </Tab> + {!isJob && pipelineSpec && ( + <Tab + eventKey={DetailsTabKey.Spec} + tabContentId={DetailsTabKey.Spec} + title={<TabTitleText>Pipeline spec</TabTitleText>} + aria-label="Run spec tab" + data-testid="pipeline-run-tab-spec" + /> + )} + </Tabs> + </FlexItem> + <FlexItem flex={{ default: 'flex_1' }} style={{ overflowY: 'hidden' }}> + <TabContent + id={DetailsTabKey.Graph} + eventKey={DetailsTabKey.Graph} + className="pf-v5-u-h-100" + hidden={activeKey !== DetailsTabKey.Graph} + > + <TabContentBody className="pf-v5-u-h-100">{graphContent}</TabContentBody> + </TabContent> + <TabContent + id={DetailsTabKey.Spec} eventKey={DetailsTabKey.Spec} - tabContentId={DetailsTabKey.Spec} - title={<TabTitleText>Pipeline spec</TabTitleText>} - aria-label="Run spec tab" - data-testid="pipeline-run-tab-spec" - /> - )} - </Tabs> - - <div style={{ flex: 1 }} hidden={activeKey !== DetailsTabKey.Graph}> - <TabContent - id={DetailsTabKey.Graph} - eventKey={DetailsTabKey.Graph} - className="pf-v5-u-h-100" - > - <TabContentBody className="pf-v5-u-h-100">{graphContent}</TabContentBody> - </TabContent> - </div> - - <TabContent - id={DetailsTabKey.Spec} - eventKey={DetailsTabKey.Spec} - hidden={activeKey !== DetailsTabKey.Spec} - style={{ flex: 1 }} - > - <TabContentBody className="pf-v5-u-h-100" hasPadding> - <PipelineDetailsYAML filename={run?.display_name} content={pipelineSpec} /> - </TabContentBody> - </TabContent> - </> + hidden={activeKey !== DetailsTabKey.Spec} + style={{ flex: 1 }} + > + <TabContentBody className="pf-v5-u-h-100" hasPadding> + <PipelineDetailsYAML filename={run?.display_name} content={pipelineSpec} /> + </TabContentBody> + </TabContent> + </FlexItem> + </Flex> + </PageSection> ); }; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx index cc9b916457..87fb500b60 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx @@ -33,10 +33,8 @@ const PipelineRunDrawerRightContent: React.FC<PipelineRunDrawerRightContentProps return ( <DrawerPanelContent - isResizable - widths={{ default: 'width_33', lg: 'width_50' }} - minSize="500px" data-testid="pipeline-run-drawer-right-content" + style={{ height: '100%', overflowY: 'auto' }} > {task.type === 'artifact' ? ( <ArtifactNodeDrawerContent diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx index abb5335da2..ee0a22b007 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import { Breadcrumb, BreadcrumbItem, - Drawer, - DrawerContent, EmptyState, EmptyStateIcon, EmptyStateVariant, @@ -51,8 +49,6 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ [selectedId, nodes], ); - const getFirstNode = (firstId: string) => nodes.find((n) => n.id === firstId)?.data?.pipelineTask; - const loaded = versionLoaded && jobLoaded; const error = versionError || jobError; @@ -76,76 +72,67 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ ); } + const panelContent = selectedNode ? ( + <SelectedTaskDrawerContent + task={selectedNode.data.pipelineTask} + onClose={() => setSelectedId(null)} + /> + ) : null; + return ( <> - <Drawer isExpanded={!!selectedNode}> - <DrawerContent - panelContent={ - <SelectedTaskDrawerContent - task={selectedNode?.data.pipelineTask} - onClose={() => setSelectedId(null)} - /> - } - > - <ApplicationsPage - title={job?.display_name} - description={job ? <MarkdownView conciseDisplay markdown={job.description} /> : ''} - loaded={loaded} - loadError={error} - breadcrumb={ - <Breadcrumb> - {breadcrumbPath(PipelineRunType.SCHEDULED)} - <BreadcrumbItem isActive style={{ maxWidth: 300 }}> - {version ? ( - <Link - to={routePipelineVersionRunsNamespace( - namespace, - version.pipeline_id, - version.pipeline_version_id, - PipelineRunType.SCHEDULED, - )} - > - {version.display_name} - </Link> - ) : ( - 'Loading...' + <ApplicationsPage + title={job?.display_name} + description={job ? <MarkdownView conciseDisplay markdown={job.description} /> : ''} + loaded={loaded} + loadError={error} + breadcrumb={ + <Breadcrumb> + {breadcrumbPath(PipelineRunType.SCHEDULED)} + <BreadcrumbItem isActive style={{ maxWidth: 300 }}> + {version ? ( + <Link + to={routePipelineVersionRunsNamespace( + namespace, + version.pipeline_id, + version.pipeline_version_id, + PipelineRunType.SCHEDULED, )} - </BreadcrumbItem> - <BreadcrumbItem isActive>{job?.display_name ?? 'Loading...'}</BreadcrumbItem> - </Breadcrumb> - } - headerAction={ - loaded && ( - <PipelineRunJobDetailsActions - job={job ?? undefined} - onDelete={() => setDeleting(true)} - /> - ) - } - empty={false} - > - <PipelineRunDetailsTabs - run={job} - pipelineSpec={version?.pipeline_spec} - graphContent={ - <PipelineTopology - nodes={nodes} - selectedIds={selectedId ? [selectedId] : []} - onSelectionChange={(ids) => { - const firstId = ids[0]; - if (ids.length === 0) { - setSelectedId(null); - } else if (getFirstNode(firstId)) { - setSelectedId(firstId); - } - }} - /> - } + > + {version.display_name} + </Link> + ) : ( + 'Loading...' + )} + </BreadcrumbItem> + <BreadcrumbItem isActive>{job?.display_name ?? 'Loading...'}</BreadcrumbItem> + </Breadcrumb> + } + headerAction={ + loaded && ( + <PipelineRunJobDetailsActions + job={job ?? undefined} + onDelete={() => setDeleting(true)} /> - </ApplicationsPage> - </DrawerContent> - </Drawer> - + ) + } + empty={false} + > + <PipelineRunDetailsTabs + run={job} + pipelineSpec={version?.pipeline_spec} + graphContent={ + <PipelineTopology + nodes={nodes} + selectedIds={selectedId ? [selectedId] : []} + onSelectionChange={(ids) => { + setSelectedId(ids.length ? ids[0] : null); + }} + sidePanel={panelContent} + /> + } + /> + </ApplicationsPage> <DeletePipelineRunsModal type={PipelineRunType.SCHEDULED} toDeleteResources={deleting && job ? [job] : []} diff --git a/frontend/src/concepts/topology/PipelineTopology.tsx b/frontend/src/concepts/topology/PipelineTopology.tsx index aba5f9e56f..2cd2807b97 100644 --- a/frontend/src/concepts/topology/PipelineTopology.tsx +++ b/frontend/src/concepts/topology/PipelineTopology.tsx @@ -13,12 +13,14 @@ type PipelineTopologyProps = { selectedIds?: string[]; onSelectionChange?: (selectionIds: string[]) => void; nodes: PipelineNodeModel[]; + sidePanel?: React.ReactElement | null; }; const PipelineTopology: React.FC<PipelineTopologyProps> = ({ nodes, selectedIds, onSelectionChange, + sidePanel, }) => { const controller = useTopologyController('g1'); @@ -51,7 +53,7 @@ const PipelineTopology: React.FC<PipelineTopologyProps> = ({ return ( <VisualizationProvider controller={controller}> - <PipelineVisualizationSurface nodes={nodes} selectedIds={selectedIds} /> + <PipelineVisualizationSurface nodes={nodes} selectedIds={selectedIds} sidePanel={sidePanel} /> </VisualizationProvider> ); }; diff --git a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx index f05528ecbc..488b579ba6 100644 --- a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx +++ b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx @@ -12,6 +12,7 @@ import { addSpacerNodes, DEFAULT_SPACER_NODE_TYPE, DEFAULT_EDGE_TYPE, + TopologySideBar, } from '@patternfly/react-topology'; import { EmptyState, @@ -25,14 +26,37 @@ import { NODE_HEIGHT, NODE_WIDTH } from './const'; type PipelineVisualizationSurfaceProps = { nodes: PipelineNodeModel[]; selectedIds?: string[]; + sidePanel?: React.ReactElement | null; }; const PipelineVisualizationSurface: React.FC<PipelineVisualizationSurfaceProps> = ({ nodes, selectedIds, + sidePanel, }) => { const controller = useVisualizationController(); const [error, setError] = React.useState<Error | null>(); + + React.useEffect(() => { + let resizeTimeout: NodeJS.Timeout | null; + + if (selectedIds?.[0]) { + const selectedNode = controller.getNodeById(selectedIds[0]); + if (selectedNode) { + // Use a timeout in order to allow the side panel to be shown and window size recomputed + resizeTimeout = setTimeout(() => { + controller.getGraph().panIntoView(selectedNode, { offset: 20, minimumVisible: 100 }); + resizeTimeout = null; + }, 500); + } + } + return () => { + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + }; + }, [selectedIds, controller]); + React.useEffect(() => { const currentModel = controller.toModel(); const updateNodes = nodes.map((node) => { @@ -158,6 +182,9 @@ const PipelineVisualizationSurface: React.FC<PipelineVisualizationSurfaceProps> })} /> } + sideBarOpen={!!sidePanel} + sideBarResizable + sideBar={<TopologySideBar resizable>{sidePanel}</TopologySideBar>} > <VisualizationSurface state={{ selectedIds }} /> </TopologyView>