diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index 4092ff3161..2825405bd0 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( + pipelineVersionDetailsRoute( + namespace, + version.pipeline_id, + version.pipeline_version_id, + ), + ) + } /> - } - {...(pipelineVersion && { - description: ( - + + {isLoaded && ( + setDeletionOpen(true)} + pipeline={pipeline} + pipelineVersion={pipelineVersion} /> - ), - })} - empty={false} - loaded={isLoaded} - headerAction={ - isPipelineVersionLoaded && ( - - - - navigate( - pipelineVersionDetailsRoute( - 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,55 @@ 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/pipelineRecurringRun/PipelineRecurringRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetails.tsx index 2d23bbc458..dd30f483fc 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetails.tsx @@ -2,8 +2,6 @@ import * as React from 'react'; import { Breadcrumb, BreadcrumbItem, - Drawer, - DrawerContent, EmptyState, EmptyStateIcon, EmptyStateVariant, @@ -74,68 +72,58 @@ const PipelineRecurringRunDetails: 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={recurringRun?.display_name} + description={ + recurringRun ? <MarkdownView conciseDisplay markdown={recurringRun.description} /> : '' + } + loaded={loaded} + loadError={error} + breadcrumb={ + <Breadcrumb> + {breadcrumbPath} + <BreadcrumbItem isActive>{recurringRun?.display_name ?? 'Loading...'}</BreadcrumbItem> + </Breadcrumb> + } + headerAction={ + loaded && ( + <PipelineRecurringRunDetailsActions + recurringRun={recurringRun ?? undefined} + onDelete={() => setDeleting(true)} /> - } - > - <ApplicationsPage - title={recurringRun?.display_name} - description={ - recurringRun ? ( - <MarkdownView conciseDisplay markdown={recurringRun.description} /> - ) : ( - '' - ) - } - loaded={loaded} - loadError={error} - breadcrumb={ - <Breadcrumb> - {breadcrumbPath} - <BreadcrumbItem isActive> - {recurringRun?.display_name ?? 'Loading...'} - </BreadcrumbItem> - </Breadcrumb> - } - headerAction={ - loaded && ( - <PipelineRecurringRunDetailsActions - recurringRun={recurringRun ?? undefined} - onDelete={() => setDeleting(true)} - /> - ) - } - empty={false} - > - <PipelineRunDetailsTabs - run={recurringRun} - 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); - } - }} - /> - } + ) + } + empty={false} + > + <PipelineRunDetailsTabs + run={recurringRun} + 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); + } + }} + sidePanel={panelContent} /> - </ApplicationsPage> - </DrawerContent> - </Drawer> - + } + /> + </ApplicationsPage> <DeletePipelineRunsModal type={PipelineRunType.SCHEDULED} toDeleteResources={deleting && recurringRun ? [recurringRun] : []} diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index d1eaa68e26..8462ba8d60 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, @@ -93,84 +91,77 @@ const PipelineRunDetails: React.FC< ); } + 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 && ( + <PipelineRecurringRunReferenceName + runName={run.display_name} + recurringRunId={run.recurring_run_id} /> - } - > - <ApplicationsPage - title={ - run ? ( - <PipelineDetailsTitle run={run} statusIcon pipelineRunLabel /> - ) : ( - 'Error loading run' - ) - } - subtext={ - run && ( - <PipelineRecurringRunReferenceName - runName={run.display_name} - recurringRunId={run.recurring_run_id} - /> - ) - } - description={ - run?.description ? <MarkdownView conciseDisplay markdown={run.description} /> : '' - } - loaded={loaded} - loadError={error} - breadcrumb={ - <Breadcrumb> - {breadcrumbPath} - <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)} + ) + } + description={ + run?.description ? <MarkdownView conciseDisplay markdown={run.description} /> : '' + } + loaded={loaded} + loadError={error} + breadcrumb={ + <Breadcrumb> + {breadcrumbPath} + <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} + </BreadcrumbItem> + </Breadcrumb> + } + headerAction={ + <PipelineRunDetailsActions + run={run} + onDelete={() => setDeleting(true)} + onArchive={() => setArchiving(true)} + /> + } + empty={false} + > + <PipelineRunDetailsTabs + run={run} + versionError={versionError} + pipelineSpec={version?.pipeline_spec} + graphContent={ + <PipelineTopology + nodes={nodes} versionError={versionError} - pipelineSpec={version?.pipeline_spec} - graphContent={ - <PipelineTopology - nodes={nodes} - versionError={versionError} - 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); - } - }} - /> - } + 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); + } + }} + 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 1112361f88..6b67045a17 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 { @@ -34,68 +43,80 @@ export const PipelineRunDetailsTabs: React.FC<PipelineRunDetailsTabsProps> = ({ const isRecurringRun = run && isPipelineRecurringRun(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" - /> + <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> - <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> + {!isRecurringRun && ( + <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" + data-testid="pipeline-graph-tab" + hidden={activeKey !== DetailsTabKey.Graph} + > + <TabContentBody className="pf-v5-u-h-100">{graphContent}</TabContentBody> + </TabContent> - {!isRecurringRun && ( - <Tab + <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" - data-testid="pipeline-graph-tab" - > - <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 }} - data-testid="pipeline-spec-tab" - > - <TabContentBody className="pf-v5-u-h-100"> - <PipelineDetailsYAML - filename={run?.display_name} - content={pipelineSpec} - versionError={versionError} - /> - </TabContentBody> - </TabContent> - </> + hidden={activeKey !== DetailsTabKey.Spec} + style={{ flex: 1 }} + data-testid="pipeline-spec-tab" + > + <TabContentBody className="pf-v5-u-h-100" hasPadding> + <PipelineDetailsYAML + filename={run?.display_name} + content={pipelineSpec} + versionError={versionError} + /> + </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/topology/PipelineTopology.tsx b/frontend/src/concepts/topology/PipelineTopology.tsx index aa8b8b1f6b..e6b7449aef 100644 --- a/frontend/src/concepts/topology/PipelineTopology.tsx +++ b/frontend/src/concepts/topology/PipelineTopology.tsx @@ -15,6 +15,7 @@ type PipelineTopologyProps = { onSelectionChange?: (selectionIds: string[]) => void; nodes: PipelineNodeModel[]; versionError?: Error; + sidePanel?: React.ReactElement | null; }; const PipelineTopology: React.FC<PipelineTopologyProps> = ({ @@ -22,6 +23,7 @@ const PipelineTopology: React.FC<PipelineTopologyProps> = ({ selectedIds, onSelectionChange, versionError, + sidePanel, }) => { const controller = useTopologyController('g1'); @@ -64,7 +66,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.scss b/frontend/src/concepts/topology/PipelineVisualizationSurface.scss new file mode 100644 index 0000000000..8a30f6e6a0 --- /dev/null +++ b/frontend/src/concepts/topology/PipelineVisualizationSurface.scss @@ -0,0 +1,8 @@ +.pipeline-visualization.m-is-open { + .pf-topology-container { + overflow-y: hidden; + .pf-v5-c-drawer__panel.pf-m-resizable { + min-width: 500px; + } + } +} diff --git a/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx b/frontend/src/concepts/topology/PipelineVisualizationSurface.tsx index f05528ecbc..a7f728c562 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, @@ -20,19 +21,50 @@ import { EmptyStateHeader, } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { css } from '@patternfly/react-styles'; import { NODE_HEIGHT, NODE_WIDTH } from './const'; +import './PipelineVisualizationSurface.scss'; 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>(); + + const selectedNode = React.useMemo(() => { + if (selectedIds?.[0]) { + const node = controller.getNodeById(selectedIds[0]); + if (node) { + return node; + } + } + return null; + }, [selectedIds, controller]); + + React.useEffect(() => { + let resizeTimeout: NodeJS.Timeout | null; + 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, selectedNode]); + React.useEffect(() => { const currentModel = controller.toModel(); const updateNodes = nodes.map((node) => { @@ -129,6 +161,7 @@ const PipelineVisualizationSurface: React.FC<PipelineVisualizationSurfaceProps> return ( <TopologyView + className={css('pipeline-visualization', !!selectedNode && 'm-is-open')} controlBar={ <TopologyControlBar controlButtons={createTopologyControlButtons({ @@ -158,6 +191,9 @@ const PipelineVisualizationSurface: React.FC<PipelineVisualizationSurfaceProps> })} /> } + sideBarOpen={!!selectedNode} + sideBarResizable + sideBar={<TopologySideBar resizable>{sidePanel}</TopologySideBar>} > <VisualizationSurface state={{ selectedIds }} /> </TopologyView>