From a8c5d142ea5b35a637da5cb7403b6275831d33d6 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:47:57 -0400 Subject: [PATCH] [RHOAIENG-7942]: Update Side Panel Behavior to Pan Graph on Node Selection Fix side panel sizing, scrolling, and contents (#8) apply light variants to page section, fix linting issue Gage PR feedback fix tests add flexItems and minWidth Fix to hide side panel completely on close update minWidth, fix pipelineRunDetails fix pipeline details view update PipelineRunDetailsTabs try fixing test error --- .../pipeline/PipelineDetails.tsx | 269 +++++++++--------- .../pipeline/SelectedTaskDrawerContent.tsx | 7 +- .../PipelineRecurringRunDetails.tsx | 108 ++++--- .../pipelineRun/PipelineRunDetails.tsx | 139 +++++---- .../pipelineRun/PipelineRunDetailsTabs.tsx | 141 +++++---- .../PipelineRunDrawerRightContent.tsx | 4 +- .../concepts/topology/PipelineTopology.tsx | 4 +- .../PipelineVisualizationSurface.scss | 8 + .../topology/PipelineVisualizationSurface.tsx | 36 +++ 9 files changed, 378 insertions(+), 338 deletions(-) create mode 100644 frontend/src/concepts/topology/PipelineVisualizationSurface.scss 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>