diff --git a/src/elm/Main.elm b/src/elm/Main.elm index 80ce6e4eb..9aefcb1ae 100644 --- a/src/elm/Main.elm +++ b/src/elm/Main.elm @@ -2178,7 +2178,7 @@ update msg model = ( { model | time = time, favicon = favicon } , Cmd.batch [ updateFavicon - , renderBuildGraph model + , refreshRenderBuildGraph model ] ) @@ -2584,7 +2584,7 @@ refreshBuildServices model org repo buildNumber focusFragment = Cmd.none -{-| refreshBuildGraph : takes model org repo and build number and refreshes the build status +{-| refreshBuildGraph : takes model org repo and build number and refreshes the build graph if necessary -} refreshBuildGraph : Model -> Org -> Repo -> BuildNumber -> Cmd Msg refreshBuildGraph model org repo buildNumber = @@ -2595,6 +2595,18 @@ refreshBuildGraph model org repo buildNumber = Cmd.none +{-| refreshRenderBuildGraph : takes model org repo and build number and refreshes the build graph render if necessary +-} +refreshRenderBuildGraph : Model -> Cmd Msg +refreshRenderBuildGraph model = + case model.page of + Pages.BuildGraph _ _ _ -> + renderBuildGraph model + + _ -> + Cmd.none + + {-| shouldRefresh : takes build and returns true if a refresh is required -} shouldRefresh : BuildModel -> Bool @@ -4301,6 +4313,7 @@ loadBuildGraphPage model org repo buildNumber = Cmd.batch [ getBuilds m org repo Nothing Nothing Nothing , getBuild m org repo buildNumber + , getAllBuildSteps m org repo buildNumber Nothing False , getBuildGraph m org repo buildNumber False , renderBuildGraph model ] diff --git a/src/elm/Pages/Build/Graph.elm b/src/elm/Pages/Build/Graph.elm index 09e239cc8..d7c2a660e 100644 --- a/src/elm/Pages/Build/Graph.elm +++ b/src/elm/Pages/Build/Graph.elm @@ -28,25 +28,8 @@ import Visualization.DOT as DOT ) -{-| renderBuildGraphDOT : constant for organizing the layout of build graph nodes --} -builtInClusterID : Int -builtInClusterID = - 2 - - -{-| pipelineClusterID : constant for organizing the layout of build graph nodes --} -pipelineClusterID : Int -pipelineClusterID = - 1 - -{-| serviceClusterID : constant for organizing the layout of build graph nodes --} -serviceClusterID : Int -serviceClusterID = - 0 +-- renderBuildGraph : { a | repo : Vela.RepoModel, velaScheduleAllowlist : List ( Vela.Org, Vela.Repo ), navigationKey : Key, user : RemoteData.WebData Vela.CurrentUser, sourceRepos : RemoteData.WebData (SourceRepositories), page : Page, time : Posix, zone : Zone, shift : Bool, buildMenuOpen : List Int, pipeline : Vela.PipelineModel } -> Cmd msg renderBuildGraph model = @@ -76,6 +59,27 @@ renderBuildGraph model = Cmd.none +{-| renderBuildGraphDOT : constant for organizing the layout of build graph nodes +-} +builtInClusterID : Int +builtInClusterID = + 2 + + +{-| pipelineClusterID : constant for organizing the layout of build graph nodes +-} +pipelineClusterID : Int +pipelineClusterID = + 1 + + +{-| serviceClusterID : constant for organizing the layout of build graph nodes +-} +serviceClusterID : Int +serviceClusterID = + 0 + + {-| renderBuildGraphDOT : takes model and build graph, and returns a string representation of a DOT graph using the extended Graph DOT package @@ -83,6 +87,8 @@ renderBuildGraph model = renderBuildGraphDOT : BuildModel.PartialModel a -> BuildGraph -> String renderBuildGraphDOT model buildGraph = let + -- todo: BUG: single step "step" sleep 10 pipeline when you hover + -- the text changes color??? isNodeFocused : String -> BuildGraphNode -> Bool isNodeFocused filter n = n.id diff --git a/src/elm/Pages/Build/View.elm b/src/elm/Pages/Build/View.elm index ed347647d..546babdf6 100644 --- a/src/elm/Pages/Build/View.elm +++ b/src/elm/Pages/Build/View.elm @@ -599,28 +599,22 @@ viewBuildGraph model msgs org repo buildNumber = , class "d3-build-graph-node-outline-rect" , class "d3-build-graph-node-step-a" , class "d3-build-graph-node-step-a-underline" + , class "d3-build-graph-step-connector" , class "d3-build-graph-edge-path" , class "-pending" , class "-running" , class "-success" , class "-failure" , class "-killed" + , class "-canceled" + , class "-skipped" , class "-hover" + , class "-focus" , class "-filtered" ] [] ] - focusedNode = - case model.repo.build.graph.graph of - RemoteData.Success g -> - Dict.get model.repo.build.graph.focusedNode g.nodes - - -- dont render anything when the build graph draw command has been dispatched - -- Maybe.withDefault "" <| List.head <| List.filter (\n -> n.id == model.repo.build.graph.focusedNode ) <| Dict.fromList g.nodes - _ -> - Nothing - actions = div [ class "elm-build-graph-actions" ] [ ul [] @@ -629,34 +623,11 @@ viewBuildGraph model msgs org repo buildNumber = [ FeatherIcons.minimize |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] ] ] - - -- , li [] - -- [ button - -- [ class "button" - -- , class "-icon" - -- , id "action-collapse" - -- , Html.Attributes.title "Collapse all stages" - -- , onClick msgs.buildGraphMsgs.collapseAllStages - -- ] - -- [ FeatherIcons.minimize2 |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] - -- ] - -- ] - -- , li [] - -- [ button - -- [ class "button" - -- , class "-icon" - -- , id "action-expand" - -- , Html.Attributes.title "Expand all stages" - -- , onClick msgs.buildGraphMsgs.expandAllStages - -- ] - -- [ FeatherIcons.maximize2 |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] - -- ] - -- ] , li [] [ button [ class "button" , class "-icon" - , id "action-refresh" + , class "action-refresh" , Html.Attributes.title "Refresh visualization" , onClick <| msgs.buildGraphMsgs.refresh org repo buildNumber ] @@ -717,31 +688,6 @@ viewBuildGraph model msgs org repo buildNumber = ] [ FeatherIcons.x |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] ] ] - - -- todo: cleanup - -- , case focusedNode of - -- Just n -> - -- div [ class "elm-build-graph-focused-node" ] - -- [ span [] [ text <| "focused: " ++ n.name ] - -- , button - -- [ class "button" - -- , class "-icon" - -- , class "elm-build-graph-search-filter-clear" - -- , onClick msgs.buildGraphMsgs.clearFocus - -- ] - -- [ FeatherIcons.x |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] ] - -- ] - -- Nothing -> - -- div [ class "elm-build-graph-focused-node" ] - -- [ span [] [ text <| "focused: none" ] - -- , button - -- [ class "button" - -- , class "-icon" - -- , class "elm-build-graph-search-filter-clear" - -- , onClick msgs.buildGraphMsgs.clearFocus - -- ] - -- [ FeatherIcons.x |> FeatherIcons.withSize 20 |> FeatherIcons.withClass "elm-build-graph-actions-button" |> FeatherIcons.toHtml [] ] - -- ] ] ] diff --git a/src/scss/_graph.scss b/src/scss/_graph.scss index ac7c9d4a0..23d6a2e6f 100644 --- a/src/scss/_graph.scss +++ b/src/scss/_graph.scss @@ -5,35 +5,34 @@ // start: classes controlled by Elm .elm-build-graph-actions { + position: relative; + display: flex; flex-direction: row; justify-content: flex-start; - position: relative; + + border: 1px solid var(--color-bg-light); + border-bottom: 0; .elm-build-graph-actions-toggles { display: flex; - // padding-left: 1rem; } .form-control { div { - margin: 1rem; position: relative; + + margin: 1rem; } border-left: 1px solid var(--color-bg-light); } - #action-refresh { - svg { - transform: rotate(0.2turn); - } - } - ul { - list-style: none; display: flex; padding-left: 0.8rem; + + list-style: none; } li { @@ -43,18 +42,16 @@ button.-icon { display: flex; } +} - // background: var(--color-bg-dark); - border: 1px solid var(--color-bg-light); - border-bottom: 0; - +.action-refresh svg { + transform: rotate(0.2turn); } .elm-build-graph-window { position: relative; display: flex; - width: 100%; min-height: 250px; @@ -107,21 +104,21 @@ stroke: var(--color-green); } - .-failure { + &.-failure { stroke: var(--color-red); } .-selected { - stroke: var(--color-cyan); + stroke: var(--color-primary); } - // not used? .-killed { stroke: var(--color-lavender); } } .elm-build-graph-search-filter { + padding-right: 1rem; // color: var(--color-text); // background-color: var(--color-bg); // border: none; @@ -133,7 +130,7 @@ // font-size: 1rem; // position: relative; padding-left: 1rem; - padding-right: 1rem; + background-color: var(--color-bg); border-right: 1px solid var(--color-bg-light); @@ -142,6 +139,7 @@ margin-left: 0; padding-bottom: 0.5rem; padding-left: 0.5rem; + border-bottom: var(--line-width) solid var(--color-primary); } @@ -150,23 +148,24 @@ } } -.elm-build-graph-focused-node { +.elm-build-graph-focus-node { // todo: remove this display: flex; align-items: center; + padding-right: 1rem; + padding-left: 1rem; + + background-color: var(--color-bg); + border-right: 1px solid var(--color-bg-light); span { - background: var(--color-primary); - color: var(--color-bg-dark); - padding: 0.2rem 0.8rem; margin-right: 0.6rem; - } + padding: 0.2rem 0.8rem; - padding-left: 1rem; - padding-right: 1rem; + color: var(--color-bg-dark); - background-color: var(--color-bg); - border-right: 1px solid var(--color-bg-light); + background: var(--color-primary); + } } // end: classes controlled by Elm @@ -176,6 +175,7 @@ .d3-build-graph-node-outline-rect { fill: none; stroke-width: 1.8; + // stroke: var(--color-gray); &.-pending { stroke: var(--color-gray); @@ -193,7 +193,9 @@ stroke: var(--color-green); } - &.-failure { + &.-failure, + &.-canceled, + &.-skipped { stroke: var(--color-red); } @@ -201,35 +203,29 @@ stroke: var(--color-lavender); } - &.-hover { + &.-focus { stroke: var(--color-primary); - stroke-width: 3; + stroke-width: 2; } - // todo: not used - &.-focused { + &.-hover { stroke: var(--color-primary); + stroke-width: 3; } } .d3-build-graph-node-step-a { - - &:focus, - &:active { - outline: none; - } + // todo: lint:css-fix does not like this override + // &:focus, + // &:active { + // outline: none; + // } &.-hover { text { fill: var(--color-primary); } } - - // todo: not used - &.-focused { - outline: 1px solid var(--color-primary); - outline-style: dashed; - } } .d3-build-graph-node-step-a-underline { @@ -238,6 +234,10 @@ } } +.d3-build-graph-step-connector { + fill: var(--color-gray); +} + .d3-build-graph-edge-path { animation: none; @@ -261,20 +261,22 @@ stroke-width: 1; } - &.-failure { + &.-failure, + &.-canceled, + &.-skipped, + &.-killed { stroke: var(--color-gray); stroke-width: 1; } - &.-hover { + &.-focus { stroke: var(--color-primary); - stroke-width: 3; } - // todo: not used - &.-focused { + &.-hover { stroke: var(--color-primary); + stroke-width: 3; } } -// end: classes controlled by d3 \ No newline at end of file +// end: classes controlled by d3 diff --git a/src/static/graph.ts b/src/static/graph.ts index 0f3d6245c..fa8e4723f 100644 --- a/src/static/graph.ts +++ b/src/static/graph.ts @@ -2,8 +2,6 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// todo: remove -// @ts-nocheck import * as d3 from 'd3'; export function drawGraph(opts, content) { @@ -11,9 +9,6 @@ export function drawGraph(opts, content) { // this is why we love javascript var _ = d3; - // todo: (kelly) make non-focused edges/outlines less "in your face" until you engage with - // todo: group/label services subgraph - // define DOM selectors for DOT-generated elements var graphSelectors = { root: '.elm-build-graph-root', @@ -21,31 +16,32 @@ export function drawGraph(opts, content) { edge: '.elm-build-graph-edge', }; - var buildGraphElement = drawBaseGraph(opts, graphSelectors.root, content); + var buildGraphElement = drawBaseGraphWithZoom( + opts, + graphSelectors.root, + content, + ); // check that a valid graph was rendered - if (buildGraphElement.node() == null) { + if (buildGraphElement === null || buildGraphElement.node() === null) { console.log('unable to continue drawing graph, root element is invalid'); return; } drawViewbox(opts, buildGraphElement); - // apply onclick to base node links prior to adding/removing elements - applyNodesOnClick(opts, buildGraphElement, graphSelectors.node); + applyOnClickToNodes(opts, buildGraphElement, graphSelectors.node); var edges = drawEdges(opts, buildGraphElement, graphSelectors.edge); drawNodes(opts, buildGraphElement, graphSelectors.node, edges); } -function drawBaseGraph(opts, selector, content) { +function drawBaseGraphWithZoom(opts, selector, content) { // grab the build graph root element var buildGraphElement = d3.select(selector); - var zoom = d3.zoom() - .scaleExtent([0.1, Infinity]) - .on('zoom', handleZoom); + var zoom = d3.zoom().scaleExtent([0.1, Infinity]).on('zoom', handleZoom); // define d3 zoom function function handleZoom(event) { @@ -58,14 +54,10 @@ function drawBaseGraph(opts, selector, content) { if (isNaN(event.transform.y)) { event.transform.y = 0; } - var zoomG = d3.select(selector + ' g'); - zoomG - .attr('transform', event.transform); + zoomG.attr('transform', event.transform); } - var w = 0; - var h = 0; function resetZoomAndCenter(opts, zoom) { var zoomG = d3.select(selector); @@ -75,12 +67,12 @@ function drawBaseGraph(opts, selector, content) { // the name of this variable is confusing var zoomGg = d3.select(selector); var zoomBBox = zoomGg.node().getBBox(); - w = zoomBBox.width; - h = zoomBBox.height; + var w = zoomBBox.width; + var h = zoomBBox.height; zoomGg .transition() // required to 'chain' these two instant animations together .duration(0) - .call(zoom.translateTo, w * 0.5, h * 0.5) + .call(zoom.translateTo, w * 0.5, h * 0.5); } // enable d3 zoom and pan functionality @@ -89,7 +81,7 @@ function drawBaseGraph(opts, selector, content) { var actionResetPan = d3.select('#action-center'); // apply zoom onclick - actionResetPan.on('click', function (e) { + actionResetPan.on('click', e => { e.preventDefault(); resetZoomAndCenter(opts, zoom); }); @@ -97,26 +89,30 @@ function drawBaseGraph(opts, selector, content) { // apply mousedown zoom effects var g = d3.select('g.node_mousedown'); if (g.empty()) { + console.log('selecting ' + selector); var zoomG = d3.select(selector); + if (zoomG.node()) { + console.log('good zoomg'); + } else { + console.log('bad zoomg'); + return null; + } + var zoomBBox = zoomG.node().getBBox(); - w = zoomBBox.width; - h = zoomBBox.height; - g = buildGraphElement - .append('g'); - g.classed('node_mousedown', true) - .attr('id', 'zoom'); + var w = zoomBBox.width; + var h = zoomBBox.height; + g = buildGraphElement.append('g'); + g.classed('node_mousedown', true).attr('id', 'zoom'); } // apply backdrop onclick - buildGraphElement.on('click', function (e) { + buildGraphElement.on('click', e => { e.preventDefault(); - setTimeout( - () => { - opts.onGraphInteraction.send({ - event_type: 'backdrop_click', - }); - }, 0 - ); + setTimeout(() => { + opts.onGraphInteraction.send({ + event_type: 'backdrop_click', + }); + }, 0); }); // this centers the graph in the viewbox, or something like that @@ -143,159 +139,77 @@ function drawViewbox(opts, buildGraphElement) { // apply viewbox properties to the root element's parent // provide x padding for the legend - const VIEWBOX_PADDING = { x1: 0, x2: 500, y1: 0, y2: 100 }; + const padding = { x1: 0, x2: 500, y1: 0, y2: 100 }; var graphParent = d3.select(buildGraphElement.node().parentNode); graphParent.attr( 'viewBox', '' + - (graphBBox.x - VIEWBOX_PADDING.x1) + - ' ' + - (graphBBox.y - VIEWBOX_PADDING.y1) + - ' ' + - (graphBBox.width + VIEWBOX_PADDING.x2) + - ' ' + - (graphBBox.height + VIEWBOX_PADDING.y2), + (graphBBox.x - padding.x1) + + ' ' + + (graphBBox.y - padding.y1) + + ' ' + + (graphBBox.width + padding.x2) + + ' ' + + (graphBBox.height + padding.y2), ); } -function drawNodes(opts, buildGraphElement, selector, edges) { - buildGraphElement.selectAll(selector).filter(function () { - let stageNode = d3.select(this); +function drawNodes(opts, buildGraphElement, nodeSelector, edges) { + buildGraphElement.selectAll(nodeSelector).filter(function () { + let node = d3.select(this); // apply an outline using rect, since nodes are rect and this will allow for animation - var outline = stageNode.append('rect'); - var nodeBBox = stageNode.node().getBBox(); + var nodeBBox = node.node().getBBox(); + var outline = node.append('rect'); outline .attr('x', nodeBBox.x) .attr('y', nodeBBox.y) .attr('width', nodeBBox.width) .attr('height', nodeBBox.height); - var stageInfo = stageNode.attr('id').replace('#', '').split(','); - var stageID = '-2'; - var stageName = ''; - var stageStatus = 'pending'; - var focused = 'false'; - if (stageInfo && stageInfo.length == 4) { - stageID = stageInfo[0]; - stageName = stageInfo[1]; - stageStatus = stageInfo[2]; - focused = stageInfo[3]; - } - + // extract information embedded in the element id for advanced styling + var data = getNodeDataFromID(node); // restore base class and build modifiers outline.attr('class', 'd3-build-graph-node-outline-rect'); + outline.classed('-' + data.status, true); - var restoreNodeClass = o => { - o.classed('-pending', true); - }; - - if (stageStatus === 'failure') { - restoreNodeClass = o => { - o.classed('-failure', true); - }; - } - if (stageStatus === 'success') { - restoreNodeClass = o => { - o.classed('-success', true); - }; - } - if (stageStatus === 'running') { - restoreNodeClass = o => { - o.classed('-running', true); - }; + // apply click-focus styling + if (data.focused && data.focused === 'true') { + outline.classed('-focus', true); } - if (stageStatus === 'killed') { - restoreNodeClass = o => { - o.classed('-killed', true); - }; - } - - // apply appropriate node outline styles - restoreNodeClass(outline); - // todo: this doesnt work with its own class - if (focused && focused === 'true') { - restoreNodeClass = o => { - // todo: would be cool to animate this - o.classed('-hover', true); - }; - } - - // todo: we shouldnt need to run this twice - // just apply the "running" outline/animation on top of the hover... - restoreNodeClass(outline); - - // apply stage node styles - stageNode.on('mouseover', e => { + node.on('mouseover', e => { + // apply outline styling outline.classed('-hover', true); - // take this stage and - // filter out all the edges that arent source/dest of each edge - edges.filter(function (edgeInfo) { - if (stageID === edgeInfo.source || - stageID === edgeInfo.destination) { - edgeInfo.target.classed('-hover', true); + // apply styling to edges relevant to this node + edges.filter(edge => { + if (data.id === edge.source || data.id === edge.destination) { + edge.target.classed('-hover', true); } }); }); - stageNode.on('mouseout', e => { - // remove node outline styling + node.on('mouseout', e => { + // remove outline styling outline.classed('-hover', false); - // restore node styling - restoreNodeClass(outline); - - // restore edge styling - edges.filter(function (edgeInfo) { - // modify styling only on related edges - if (stageID === edgeInfo.source || - stageID === edgeInfo.destination) { - var status = edgeInfo.status; - - // apply edge element hover styles - edgeInfo.target.classed('-hover', false); - - var restoreEdgeClass = o => { - o.classed('-pending', true); - }; - - if (status === 'running') { - restoreEdgeClass = o => { - o.classed('-running', true); - }; - } - if (status === 'success') { - restoreEdgeClass = o => { - o.classed('-success', true); - }; - } - if (status === 'failure') { - restoreEdgeClass = o => { - o.classed('-failure', true); - }; - } - - if (edgeInfo.focused === 'true') { - restoreEdgeClass = o => { - o.classed('-hover', true); - }; - } - - // apply the appropriate styles - restoreEdgeClass(edgeInfo.target); + // remove styling from edges relevant to this node + edges.filter(edge => { + if (data.id === edge.source || data.id === edge.destination) { + edge.target.classed('-hover', false); + edge.target.classed('-' + edge.status, true); } }); }); var stepIconSize = 16; - stageNode.selectAll('a').filter(function () { - var step = d3.select(this); - if (step.attr('xlink:href').includes('#step')) { + node.selectAll('a').filter(function () { + var step = d3.select(this); + if (step.attr('xlink:href').includes('#step:')) { // restore base class and build modifiers step.attr('class', 'd3-build-graph-node-step-a'); @@ -305,8 +219,7 @@ function drawNodes(opts, buildGraphElement, selector, edges) { underline .attr('x', aBBox.x + stepIconSize) .attr('y', aBBox.y + aBBox.height) - .attr('width', aBBox.width - stepIconSize) - .attr('fill', 'var(--color-red)'); //todo: replace with style + .attr('width', aBBox.width - stepIconSize); // restore base class and build modifiers underline.attr('class', 'd3-build-graph-node-step-a-underline'); @@ -315,11 +228,15 @@ function drawNodes(opts, buildGraphElement, selector, edges) { step.on('mouseover', e => { step.classed('-hover', true); underline.classed('-hover', true); + + // draw underline underline.attr('height', 1); }); step.on('mouseout', e => { step.classed('-hover', false); underline.classed('-hover', false); + + // clear underline underline.attr('height', 0); }); } @@ -327,170 +244,100 @@ function drawNodes(opts, buildGraphElement, selector, edges) { // track step number for applying styles var i = 0; - stageNode.selectAll('#a_node-cell').filter(function () { - var cell = d3.select(this); - var cellNode = cell.select('text').node(); - if (cellNode) { - let parent = d3.select(cellNode.parentNode); - let nodeBox = cellNode.getBBox(); - // extract href to dispatch to Elm - var href = parent.attr('xlink:href'); + // draw node cells (steps) + node.selectAll('#a_node-cell').filter(function () { + var cell = d3.select(this).select('text').node(); + if (cell) { + let cellParent = d3.select(cell.parentNode); - // remove actual href attribute - parent.attr('xlink:href', null); + var step = getStepDataFromTitle(cellParent); - var stepInfo = parent.attr('xlink:title').split(','); - - // todo: safety check - var status = 'pending'; - if (stepInfo && stepInfo.length > 2) { - status = stepInfo[2]; - } - - // todo: this image step icon mapping needs to be better - if (status === 'canceled') { - status = 'failure'; - } - - if (status === 'skipped') { - status = 'failure'; - } + let cellBBox = cell.getBBox(); // todo: static/*.png seems like a bad way to do icon images - parent + cellParent .append('image') - .attr('xlink:href', '/images/vela_' + status + '.png') - .attr('x', nodeBox.x - 6) - .attr('y', nodeBox.y) + .attr('xlink:href', '/images/vela_' + step.status + '.png') + .attr('x', cellBBox.x - 6) + .attr('y', cellBBox.y) .attr('width', stepIconSize) .attr('height', stepIconSize); // step connector if (i > 0) { - parent.append('rect') + var connector = cellParent.append('rect'); + connector.classed('d3-build-graph-step-connector', true); + + connector // tweak position for visual effect - .attr('x', nodeBox.x + 2) - .attr('y', nodeBox.y - 7) + .attr('x', cellBBox.x + 2) + .attr('y', cellBBox.y - 7) // apply size manually .attr('width', 1) - .attr('height', 5) - .attr('fill', 'var(--color-gray)'); //todo: replace with style + .attr('height', 5); //todo: replace with style } i++; - parent.on('click', function (e) { + // extract and remove the href to dispatch link clicks to Elm + var href = cellParent.attr('xlink:href'); + cellParent.attr('xlink:href', null); + + cellParent.on('click', e => { e.preventDefault(); // prevents multiple link events getting fired from a single click e.stopImmediatePropagation(); - setTimeout( - () => { - opts.onGraphInteraction.send({ - event_type: 'href', - href: href, - step_id: '', - }); - }, - 0, - ); + setTimeout(() => { + opts.onGraphInteraction.send({ + event_type: 'href', + href: href, + step_id: '', + }); + }, 0); }); } }); - - return ''; // used by filter (?) }); } -function drawEdges(opts, buildGraphElement, selector) { - // collect edge information to use in other d3 interactivity - var edges = []; +function drawEdges(opts, buildGraphElement, edgeSelector) { + // collect edge information to use in other interactivity + var edges: any[] = []; - buildGraphElement.selectAll(selector).filter(function () { + buildGraphElement.selectAll(edgeSelector).filter(function () { let a = d3.select(this); - var edgeInfo = a.attr('id').replace('#', '').split(','); var p = a.select('path'); - // extract edge information - var source = "-1"; - var destination = "-1"; - var status = "pending"; - var focused = "false"; - if (edgeInfo && edgeInfo.length == 4) { - source = edgeInfo[0]; - destination = edgeInfo[1]; - status = edgeInfo[2]; - focused = edgeInfo[3]; - } - - // track edge information for advanced styling - edges.push({ + // extract information embedded in the element id for advanced styling + var data = getEdgeDataFromID(a); + var edge = { target: p, - source: source, - destination: destination, - status: status, - focused: focused, - }); + ...data, + }; + edges.push(edge); // restore base class and build modifiers p.attr('class', 'd3-build-graph-edge-path'); - var restoreEdgeClass = o => { - o.classed('-pending', true); - }; - - if (status === 'running') { - restoreEdgeClass = o => { - o.classed('-running', true); - }; - } - if (status === 'success') { - restoreEdgeClass = o => { - o.classed('-success', true); - }; - } - if (status === 'failure') { - restoreEdgeClass = o => { - o.classed('-failure', true); - }; - } - // apply the appropriate styles - restoreEdgeClass(p); - + p.classed('-' + data.status, true); - if (focused && focused === 'true') { - restoreEdgeClass = o => { - o.classed('-hover', true); - }; + if (data.focused && data.focused === 'true') { + p.classed('-focus', true); } - - // apply the appropriate styles - restoreEdgeClass(p); - - // apply edge hover styles - // a.on('mouseover', e => { - // p.classed('-hover', true); - // }); - // a.on('mouseout', e => { - // restoreEdgeClass(p); - // p.classed('-hover', false); - // }); - - return ''; // used by filter (?) }); return edges; } -// applyNodesOnClick takes root graph element, selects node links and applies onclick functionality -function applyNodesOnClick(opts, buildGraphElement, selector) { +// applyOnClickToNodes takes root graph element, selects node links and applies onclick functionality +function applyOnClickToNodes(opts, buildGraphElement, nodeSelector) { // process and return all 'linked' stage nodes - return buildGraphElement.selectAll(selector + ' a').filter(function () { - // todo: figure out .each + return buildGraphElement.selectAll(nodeSelector + ' a').filter(function () { // add onclick to nodes with valid href attributes var href = d3.select(this).attr('xlink:href'); if (href !== null) { - d3.select(this).on('click', function (e) { + d3.select(this).on('click', e => { e.preventDefault(); e.stopImmediatePropagation(); @@ -503,16 +350,90 @@ function applyNodesOnClick(opts, buildGraphElement, selector) { stageID = stageInfo[0]; } - setTimeout( - () => { - opts.onGraphInteraction.send({ - event_type: 'node_click', - node_id: stageID, - }); - }, 0 - ); + // dispatch Elm interop + setTimeout(() => { + opts.onGraphInteraction.send({ + event_type: 'node_click', + node_id: stageID, + }); + }, 0); }); } - return ''; }); } + +function getNodeDataFromID(element) { + // extract information embedded in the element id + var id = element.attr('id').replace('#', '').split(','); + + // default info + var data = { + id: '-2', + name: '-', + status: 'pending', + focused: 'false', + }; + + // extract from split id + if (id && id.length == 4) { + data = { + id: id[0], + name: id[1], + status: id[2], + focused: id[3], + }; + } + + return data; +} + +function getEdgeDataFromID(element) { + // extract information embedded in the element id + var id = element.attr('id').replace('#', '').split(','); + + // default info + var data = { + source: '-1', + destination: '-1', + status: 'pending', + focused: 'false', + }; + + // extract from split id + if (id && id.length >= 4) { + data = { + source: id[0], + destination: id[1], + status: id[2], + focused: id[3], + }; + } + + return data; +} + +function getStepDataFromTitle(element) { + // extract information embedded in the element title + var title = element.attr('xlink:title').split(','); + + // default info + var data = { + id: '-3', + name: '-', + status: 'pending', + }; + + // extract from split title + if (title && title.length >= 2) { + data = { + id: title[0], + name: title[1], + status: title[2], + }; + if (data.status === 'canceled' || data.status === 'skipped') { + data.status = 'failure'; + } + } + + return data; +} diff --git a/src/static/index.ts b/src/static/index.ts index 42d0ee5af..cd3e6b987 100644 --- a/src/static/index.ts +++ b/src/static/index.ts @@ -13,7 +13,7 @@ import { Graphviz } from '@hpcc-js/wasm'; import { Elm } from '../elm/Main.elm'; import '../scss/style.scss'; import { App, Config, Flags, Theme } from './index.d'; -import * as Graph from './graph'; +import * as Graph from './graph'; // Vela consts const feedbackURL: string = @@ -51,13 +51,13 @@ const flags: Flags = { velaRedirect: currentRedirectKey || '', velaLogBytesLimit: Number( process.env.VELA_LOG_BYTES_LIMIT || - envOrNull('VELA_LOG_BYTES_LIMIT', '$VELA_LOG_BYTES_LIMIT') || - defaultLogBytesLimit, + envOrNull('VELA_LOG_BYTES_LIMIT', '$VELA_LOG_BYTES_LIMIT') || + defaultLogBytesLimit, ), velaMaxBuildLimit: Number( process.env.VELA_MAX_BUILD_LIMIT || - envOrNull('VELA_MAX_BUILD_LIMIT', 'VELA_MAX_BUILD_LIMIT') || - maximumBuildLimit, + envOrNull('VELA_MAX_BUILD_LIMIT', 'VELA_MAX_BUILD_LIMIT') || + maximumBuildLimit, ), velaScheduleAllowlist: @@ -137,7 +137,7 @@ function envOrNull(env: string, subst: string): string | null { var opts = { currentBuild: -1, isRefreshDraw: false, - contentFilter: "", + contentFilter: '', }; app.ports.renderBuildGraph.subscribe(function (graphData) { @@ -147,7 +147,7 @@ app.ports.renderBuildGraph.subscribe(function (graphData) { var content = res.layout(dot, 'svg', 'dot'); // construct graph building options - opts.isRefreshDraw = (opts.currentBuild === graphData.build_id); + opts.isRefreshDraw = opts.currentBuild === graphData.build_id; opts.currentBuild = graphData.build_id; opts.contentFilter = graphData.filter; opts.onGraphInteraction = app.ports.onGraphInteraction;