From 23f8cab0afa659505c2d05f9de337d542ebd9de9 Mon Sep 17 00:00:00 2001 From: Felix Edel Date: Fri, 19 Apr 2024 09:37:54 +0200 Subject: [PATCH] Visualize branches in ChangeQueues The old status page visualized failing queue items (e.g. merge conflicts) by creating a new "branch" in the change queue. This change implements a similar functionality in the PipelineDetailsView by nesting ProgressSteppers in a tree-like view to visualize multiple branches of QueueItems within a ChangeQueue. By utilizing a ProgressStepper for this purpose we can get rid of most of the custom CSS code that was necessary to draw and connect the "branching lines" in the old approach. We only need a little bit of CSS to connect an inner ProgressStepper to the outer one. In case a queue has multiple heads, each head will be rendered in a separate tree (ProgressStepper). Change-Id: Iee80b8d472cfbd8436301dff83edde5924bb7d08 --- web/src/containers/status/ChangeQueue.jsx | 175 ++++++++++++++++++---- web/src/index.css | 22 +++ 2 files changed, 165 insertions(+), 32 deletions(-) diff --git a/web/src/containers/status/ChangeQueue.jsx b/web/src/containers/status/ChangeQueue.jsx index 770ec8b3d3..4c1420bdb0 100644 --- a/web/src/containers/status/ChangeQueue.jsx +++ b/web/src/containers/status/ChangeQueue.jsx @@ -28,8 +28,146 @@ import { import QueueItem from './QueueItem' import { getQueueItemIconConfig } from './Misc' +// This function will create a "tree-like" data structure to visualize +// multiple branches in a single ChangeQueue. +// The data structure is basically a linked-list that contains +// additional branches for items that are no longer relevant for the +// main branch (e.g. merge conflicts in a dependent pipeline). The base +// for the data structure is the "items_behind" relation of queue items. +// As a result, each queue item will have a single item_behind in the +// the main branch, while all other items_behind are moved to different +// branches. +// +// In the example below the items A, B, D and F build the "main" branch, +// while the items C and E create new branches (e.g. due to merge +// conflicts). G is the item behind E, so it will also be part of the +// branch starting with E. +// +// A +// | +// B +// |-C +// D +// |-E +// | | +// F G +const createTree = (head) => { + // Root of the tree/linked list + let tree = null + + // Map for easier lookup of items by their id + const itemsById = {} + // Create a copy of the original queue, so we can remove the current + // node while iterating over the list. Once the list is empty, we + // know that we have seen all items in the queue. + //let head = JSON.parse(JSON.stringify(_head)) + + // First iteration: Create map for "lookup by id" + head.forEach(item => { + itemsById[item.id] = item + }) + + // Second iteration: Move each item to the correct position within + // the tree + head.forEach(node => { + // node._next builds a linked-list to visualize the "main" branch + // of the queue. + node._next = null + // Branches will contain all items_behind which are not part of + // main branch. + node._branches = [] + + // Create the root of the tree + if (tree === null) { + tree = node + } + + if (node.items_behind.length === 0) { + // Basically a continue for the forEach loop + return + } + + // Copy the items_behind, so we pop() already assigned items without + // affecting the original array. + const items_behind = node.items_behind.slice() + if (items_behind.length > 1) { + // The last element in the list is the item_behind on the "main" + // branch + const item_behind = itemsById[items_behind.pop()] + node._next = item_behind + // All other items_behind are failing ones, so they should be + // added to separate branches + items_behind.forEach(item => { + const item_behind = itemsById[item] + node._branches.push(item_behind) + }) + } else { + // We have only one element, so add it to the main branch + const item_behind = itemsById[items_behind.pop()] + node._next = item_behind + } + }) + + return tree +} + +const Branch = ({ item, newBranch = false }) => { + // Recursively render QueueItems to visualize a ChangeQueue. + const iconConfig = getQueueItemIconConfig(item) + const Icon = iconConfig.icon + + const step = ( + <> + } + style={{ marginBottom: '16px' }} + key={`ps-${item.id}`} + > + + {/* To visualize a new branch, we put a ProgressStepper within + the current ProgressStep. */} + {item._branches.map(branch => ( + + ))} + + {/* Items in the same branch must come after the current + ProgressStep. We don't want them to be nested. */} + {item._next !== null ? : ''} + + ) + + const wrappedStep = ( +
+ + {step} + +
+ ) + + // If we want to start a new branch, we have to wrap the current + // branch into a new ProgressStepper. + if (newBranch) { + return wrappedStep + } + + // Otherwise, just return the current step + return step +} + +Branch.propTypes = { + item: PropTypes.object.isRequired, + newBranch: PropTypes.bool, +} function ChangeQueue({ queue }) { + // TODO (felix): Use useMemo hook to cache the rendered tree across re-renders + const trees = [] + queue.heads.forEach(head => ( + trees.push(createTree(head)) + )) return ( <> @@ -43,38 +181,11 @@ function ChangeQueue({ queue }) { : ''} - {/* TODO (felix): Add the following CSS rules to the ::before - element of a nested ProgressStep connector: - transform: skew(45deg); - left: 30px; - top: 20px; - height: 20px; - That will draw a diagonal line between the inner - ProgressStep and the outer ProgressStep. Maybe something - like this would work to implement branching within a - ProgressStepper. - */} - - {queue.heads.map(head => ( - head.map(item => { - const iconConfig = getQueueItemIconConfig(item) - const Icon = iconConfig.icon - - return ( - } - style={{ marginBottom: '16px' }} - key={item.id} - > - - - ) - }) - ))} - + {trees.map(tree => ( + + + + ))} diff --git a/web/src/index.css b/web/src/index.css index 8bf53eeb8c..f6dfd04af5 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -342,6 +342,28 @@ a.refresh { animation: progress-bar-stripes 1s linear infinite; } +.pf-c-progress-stepper.zuul-queue-branch { + margin-top: 20px; +} + +.zuul-branch-wrapper::before { + content: ""; + position: absolute; + border-bottom: var(--pf-c-progress-stepper__step-connector--before--BorderRightWidth) solid var(--pf-c-progress-stepper__step-connector--before--BorderRightColor); + left: 15px; + height: 14px; + width: 40px; +} +/*.pf-c-progress-stepper.zuul-queue-branch > .pf-c-progress-stepper__step:first-of-type > .pf-c-progress-stepper__step-connector::before { + content: ""; + position: absolute; + border-bottom: var(--pf-c-progress-stepper__step-connector--before--BorderRightWidth) solid var(--pf-c-progress-stepper__step-connector--before--BorderRightColor); + left: -18px; + top: 13px; + height: 0; + width: 40px; +}*/ + .zuul-pipeline-header h3 { font-weight: var(--pf-global--FontWeight--bold); }