From 57e626b2b037e9cea4df963f284980f068ae1575 Mon Sep 17 00:00:00 2001 From: davidvader Date: Mon, 16 Oct 2023 16:49:54 -0500 Subject: [PATCH] wip: click-focus and clear-focus --- src/elm/Main.elm | 310 ++++++++-------------------------- src/elm/Pages/Build/Graph.elm | 94 ++++++++--- src/elm/Pages/Build/View.elm | 36 ++++ src/elm/Util.elm | 23 ++- src/elm/Vela.elm | 30 +++- src/scss/_graph.scss | 44 ++++- src/static/graph.ts | 117 +++++++++---- 7 files changed, 351 insertions(+), 303 deletions(-) diff --git a/src/elm/Main.elm b/src/elm/Main.elm index 7b80aedc3..80ce6e4eb 100644 --- a/src/elm/Main.elm +++ b/src/elm/Main.elm @@ -76,7 +76,7 @@ import Maybe.Extra exposing (unwrap) import Nav exposing (viewUtil) import Pager import Pages exposing (Page) -import Pages.Build.Graph exposing (renderBuildGraphDOT) +import Pages.Build.Graph exposing (renderBuildGraph, renderBuildGraphDOT) import Pages.Build.Logs exposing ( addLog @@ -228,6 +228,7 @@ import Vela , updateHooksPage , updateHooksPager , updateHooksPerPage + , updateModels , updateOrgRepo , updateOrgReposPage , updateOrgReposPager @@ -514,6 +515,12 @@ update msg model = rm = model.repo + bm = + rm.build + + gm = + model.repo.build.graph + sm = model.schedulesModel @@ -641,192 +648,97 @@ update msg model = OnBuildGraphInteraction interaction -> let - bm = - model.repo.build + ( ugm_, cmd ) = + case interaction.event_type of + "href" -> + ( model.repo.build.graph + , Util.dispatch <| FocusOn (focusFragmentToFocusId "step" (Just <| String.Extra.rightOf "#" interaction.href)) + ) - gm = - model.repo.build.graph + "backdrop_click" -> + let + ugm = + { gm | focusedNode = -1 } + + um_ = + updateModels model rm bm ugm + in + ( ugm, renderBuildGraph um_ ) + + "node_click" -> + let + ugm = + { gm | focusedNode = Maybe.withDefault -1 <| String.toInt interaction.node_id } + + um_ = + updateModels model rm bm ugm + in + ( ugm, renderBuildGraph um_ ) + + _ -> + ( model.repo.build.graph, Cmd.none ) in - ( { model - | repo = - { rm - | build = - { bm - | graph = - { gm - | showSteps = not gm.showSteps - } - } - } - } + ( updateModels model rm bm ugm_ , Cmd.batch [ Navigation.pushUrl model.navigationKey interaction.href - , Util.dispatch <| FocusOn (focusFragmentToFocusId "step" (Just <| String.Extra.rightOf "#" interaction.href)) + , cmd ] ) BuildGraphRefresh org repo buildNumber -> let - bm = - model.repo.build - - gm = - model.repo.build.graph - - updatedModel = - { model - | repo = - { rm - | build = - { bm - | graph = - { gm - | graph = Loading - } - } - } + ugm = + { gm + | graph = Loading } + + um_ = + updateModels model rm bm ugm in - ( updatedModel - , getBuildGraph updatedModel org repo buildNumber True + ( um_ + , getBuildGraph um_ org repo buildNumber True ) BuildGraphUpdateFilter filter -> let - bm = - model.repo.build - - gm = - model.repo.build.graph - - updatedModel = - { model - | repo = - { rm - | build = - { bm - | graph = - { gm - | filter = filter - } - } - } + ugm = + { gm + | filter = String.toLower filter } - renderGraph = - case gm.graph of - Success g -> - case bm.build of - Success b -> - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT updatedModel g - , buildID = b.id - , filter = filter - , showServices = model.repo.build.graph.showServices - , showSteps = model.repo.build.graph.showSteps - } - - _ -> - Cmd.none - - _ -> - Cmd.none + um_ = + updateModels model rm bm ugm in - ( updatedModel - , renderGraph + ( um_ + , renderBuildGraph um_ ) BuildGraphShowServices show -> let - bm = - model.repo.build - - gm = - model.repo.build.graph - - updatedModel = - { model - | repo = - { rm - | build = - { bm - | graph = - { gm - | showServices = show - } - } - } + ugm = + { gm + | showServices = show } - renderGraph = - case gm.graph of - Success g -> - case bm.build of - Success b -> - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT updatedModel g - , buildID = b.id - , filter = updatedModel.repo.build.graph.filter - , showServices = updatedModel.repo.build.graph.showServices - , showSteps = updatedModel.repo.build.graph.showSteps - } - - _ -> - Cmd.none - - _ -> - Cmd.none + um_ = + updateModels model rm bm ugm in - ( updatedModel - , renderGraph + ( um_ + , renderBuildGraph um_ ) BuildGraphShowSteps show -> let - bm = - model.repo.build - - gm = - model.repo.build.graph - - updatedModel = - { model - | repo = - { rm - | build = - { bm - | graph = - { gm - | showSteps = show - } - } - } + ugm = + { gm + | showSteps = show } - renderGraph = - case gm.graph of - Success g -> - case bm.build of - Success b -> - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT updatedModel g - , buildID = b.id - , filter = updatedModel.repo.build.graph.filter - , showServices = updatedModel.repo.build.graph.showServices - , showSteps = updatedModel.repo.build.graph.showSteps - } - - _ -> - Cmd.none - - _ -> - Cmd.none + um_ = + updateModels model rm bm ugm in - ( updatedModel - , renderGraph + ( um_ + , renderBuildGraph um_ ) GotoPage pageNumber -> @@ -2210,18 +2122,9 @@ update msg model = case model.page of Pages.BuildGraph _ _ _ -> let - bm = - rm.build - - gm = - bm.graph - sameBuild = gm.buildNumber == buildNumber - buildID = - RemoteData.unwrap -1 .id bm.build - showSteps = if not sameBuild then True @@ -2229,19 +2132,15 @@ update msg model = else gm.showSteps + ugm = + { gm | buildNumber = buildNumber, graph = RemoteData.succeed g, showSteps = showSteps } + updatedModel = - { model | repo = { rm | build = { bm | graph = { gm | buildNumber = buildNumber, graph = RemoteData.succeed g, showSteps = showSteps } } } } + updateModels model rm bm ugm cmd = if not sameBuild then - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT updatedModel g - , buildID = buildID - , filter = updatedModel.repo.build.graph.filter - , showServices = model.repo.build.graph.showServices - , showSteps = model.repo.build.graph.showSteps - } + renderBuildGraph updatedModel else Cmd.none @@ -2254,13 +2153,6 @@ update msg model = ( model, Cmd.none ) Err error -> - let - bm = - rm.build - - gm = - bm.graph - in ( { model | repo = { rm | build = { bm | graph = { gm | graph = toFailure error } } } } , Cmd.none ) @@ -2282,41 +2174,11 @@ update msg model = let ( favicon, updateFavicon ) = refreshFavicon model.page model.favicon rm.build.build - - renderGraph = - case model.page of - Pages.BuildGraph org repo buildNumber -> - let - bm = - model.repo.build - - gm = - bm.graph - - buildID = - RemoteData.unwrap -1 .id bm.build - in - case gm.graph of - Success g -> - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT model g - , buildID = buildID - , filter = model.repo.build.graph.filter - , showServices = model.repo.build.graph.showServices - , showSteps = model.repo.build.graph.showSteps - } - - _ -> - Cmd.none - - _ -> - Cmd.none in ( { model | time = time, favicon = favicon } , Cmd.batch [ updateFavicon - , renderGraph + , renderBuildGraph model ] ) @@ -4419,28 +4281,6 @@ loadBuildGraphPage model org repo buildNumber = gm = bm.graph - buildID = - RemoteData.unwrap -1 .id bm.build - - renderGraph = - case gm.graph of - Success g -> - if sameBuild then - Interop.renderBuildGraph <| - encodeBuildGraphRenderData - { dot = renderBuildGraphDOT model g - , buildID = buildID - , filter = model.repo.build.graph.filter - , showServices = model.repo.build.graph.showServices - , showSteps = model.repo.build.graph.showSteps - } - - else - Cmd.none - - _ -> - Cmd.none - graph = if sameBuild then RemoteData.unwrap RemoteData.Loading (\g_ -> RemoteData.succeed g_) gm.graph @@ -4455,14 +4295,14 @@ loadBuildGraphPage model org repo buildNumber = -- do not load resources if transition is auto refresh, line focus, etc -- MUST render graph here, or clicking on nodes won't cause an immediate change , if sameBuild && sameResource then - renderGraph + renderBuildGraph model else Cmd.batch [ getBuilds m org repo Nothing Nothing Nothing , getBuild m org repo buildNumber , getBuildGraph m org repo buildNumber False - , renderGraph + , renderBuildGraph model ] ) diff --git a/src/elm/Pages/Build/Graph.elm b/src/elm/Pages/Build/Graph.elm index 76bf3042b..09e239cc8 100644 --- a/src/elm/Pages/Build/Graph.elm +++ b/src/elm/Pages/Build/Graph.elm @@ -1,9 +1,11 @@ -module Pages.Build.Graph exposing (renderBuildGraphDOT) +module Pages.Build.Graph exposing (renderBuildGraph, renderBuildGraphDOT) import Dict exposing (Dict) import Focus import Graph exposing (Edge, Node) +import Interop import Pages.Build.Model as BuildModel +import RemoteData exposing (RemoteData(..)) import Routes exposing (Route(..)) import Util import Vela @@ -11,6 +13,7 @@ import Vela ( BuildGraph , BuildGraphEdge , BuildGraphNode + , encodeBuildGraphRenderData , statusToString ) import Visualization.DOT as DOT @@ -46,6 +49,33 @@ serviceClusterID = 0 +renderBuildGraph model = + let + rm = + model.repo + + bm = + rm.build + + gm = + rm.build.graph + in + case gm.graph of + Success g -> + Interop.renderBuildGraph <| + encodeBuildGraphRenderData + { dot = renderBuildGraphDOT model g + , buildID = RemoteData.unwrap -1 .id bm.build + , filter = gm.filter + , showServices = gm.showServices + , showSteps = gm.showSteps + , focusedNode = gm.focusedNode + } + + _ -> + Cmd.none + + {-| renderBuildGraphDOT : takes model and build graph, and returns a string representation of a DOT graph using the extended Graph DOT package @@ -53,18 +83,34 @@ serviceClusterID = renderBuildGraphDOT : BuildModel.PartialModel a -> BuildGraph -> String renderBuildGraphDOT model buildGraph = let + isNodeFocused : String -> BuildGraphNode -> Bool + isNodeFocused filter n = + n.id + == model.repo.build.graph.focusedNode + || (String.length filter > 2) + && (String.contains filter n.name + || List.any (\s -> String.contains filter s.name) n.steps + ) + + isEdgeFocused : Int -> BuildGraphEdge -> Bool + isEdgeFocused focusedNode e = + focusedNode == e.destination || focusedNode == e.source + -- convert BuildGraphNode to Graph.Node inNodes = buildGraph.nodes |> Dict.toList |> List.map - (\( _, n ) -> Node n.id (BuildGraphNode n.cluster n.id n.name n.status n.startedAt n.finishedAt n.steps)) + (\( _, n ) -> + Node n.id + (BuildGraphNode n.cluster n.id n.name n.status n.startedAt n.finishedAt n.steps (isNodeFocused model.repo.build.graph.filter n)) + ) -- convert BuildGraphEdge to Graph.Edge inEdges = buildGraph.edges |> List.map - (\e -> Edge e.source e.destination (BuildGraphEdge e.cluster e.source e.destination e.status)) + (\e -> Edge e.source e.destination (BuildGraphEdge e.cluster e.source e.destination e.status (isEdgeFocused model.repo.build.graph.focusedNode e))) -- construct a Graph to extract nodes and edges ( nodes, edges ) = @@ -307,7 +353,7 @@ baseGraphStyles = , edge = escapeAttributes [ ( "color", DefaultEscape "azure2" ) - , ( "penwidth", DefaultEscape "2" ) + , ( "penwidth", DefaultEscape "1" ) , ( "arrowhead", DefaultEscape "dot" ) , ( "arrowsize", DefaultEscape "0.5" ) , ( "minlen", DefaultEscape "1" ) @@ -387,6 +433,24 @@ serviceSubgraphStyles = } +defaultNodeAttrs : List ( String, Attribute ) +defaultNodeAttrs = + [ ( "class", DefaultJSONLabelEscape "elm-build-graph-node" ) + , ( "shape", DefaultJSONLabelEscape "rect" ) + , ( "style", DefaultJSONLabelEscape "filled" ) + , ( "border", DefaultJSONLabelEscape "white" ) + ] + + +nodeTableAttrs : List ( String, AttributeValue ) +nodeTableAttrs = + [ ( "border", DefaultEscape "0" ) + , ( "cellborder", DefaultEscape "0" ) + , ( "cellspacing", DefaultEscape "5" ) + , ( "margin", DefaultEscape "0" ) + ] + + nodeAttrs : BuildModel.PartialModel a -> BuildGraphNode -> Dict String Attribute nodeAttrs model node = let @@ -397,6 +461,7 @@ nodeAttrs model node = [ String.fromInt node.id , node.name , node.status + , Util.boolToString node.focused ] -- track step expansion using the model and OnGraphInteraction @@ -410,28 +475,10 @@ nodeAttrs model node = [ ( "id", DefaultJSONLabelEscape id ) , ( "href", DefaultJSONLabelEscape ("#" ++ node.name) ) , ( "label", HtmlLabelEscape <| nodeLabel model showSteps node ) - , ( "tooltip", DefaultJSONLabelEscape node.name ) + , ( "tooltip", DefaultJSONLabelEscape id ) ] -defaultNodeAttrs : List ( String, Attribute ) -defaultNodeAttrs = - [ ( "class", DefaultJSONLabelEscape "elm-build-graph-node" ) - , ( "shape", DefaultJSONLabelEscape "rect" ) - , ( "style", DefaultJSONLabelEscape "filled" ) - , ( "border", DefaultJSONLabelEscape "white" ) - ] - - -nodeTableAttrs : List ( String, AttributeValue ) -nodeTableAttrs = - [ ( "border", DefaultEscape "0" ) - , ( "cellborder", DefaultEscape "0" ) - , ( "cellspacing", DefaultEscape "5" ) - , ( "margin", DefaultEscape "0" ) - ] - - edgeAttrs : BuildGraphEdge -> Dict String Attribute edgeAttrs e = let @@ -442,6 +489,7 @@ edgeAttrs e = [ String.fromInt e.source , String.fromInt e.destination , e.status + , Util.boolToString e.focused ] in Dict.fromList <| diff --git a/src/elm/Pages/Build/View.elm b/src/elm/Pages/Build/View.elm index 6fa1ed3d4..ed347647d 100644 --- a/src/elm/Pages/Build/View.elm +++ b/src/elm/Pages/Build/View.elm @@ -16,6 +16,7 @@ import Ansi import Ansi.Log import Array import DateFormat.Relative exposing (relativeTime) +import Dict import FeatherIcons import Focus exposing @@ -610,6 +611,16 @@ viewBuildGraph model msgs org repo buildNumber = [] ] + 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 [] @@ -706,6 +717,31 @@ 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/elm/Util.elm b/src/elm/Util.elm index 6ec2aa443..1a3b5c702 100644 --- a/src/elm/Util.elm +++ b/src/elm/Util.elm @@ -9,22 +9,23 @@ module Util exposing , ariaHidden , attrIf , base64Decode + , boolToString , boolToYesNo , buildRefURL - , formatDuration , checkScheduleAllowlist , dispatch , extractFocusIdFromRange , filterEmptyList , filterEmptyLists , fiveSecondsMillis + , formatDuration , formatFilesize , formatRunTime + , formatRunTimeWithDefault , formatTestTag , getNameFromRef , humanReadableDateTimeFormatter , humanReadableWithDefault - ,formatRunTimeWithDefault , isLoading , isSuccess , largeLoader @@ -161,20 +162,20 @@ noSomeSecondsAgo _ = "just now" - {-| formatDuration : calculates build runtime using a duration -} formatDuration : Int -> String formatDuration duration = let minutes = - runTimeMinutes <| duration + runTimeMinutes <| duration seconds = - runTimeSeconds <| duration + runTimeSeconds <| duration in String.join ":" [ minutes, seconds ] + {-| formatRunTime : calculates build runtime using current application time and build times -} formatRunTime : Posix -> Int -> Int -> String @@ -198,6 +199,7 @@ formatRunTimeWithDefault : Posix -> Int -> Int -> String formatRunTimeWithDefault now started finished = if started == 0 then "--:--" + else let runtime = @@ -417,6 +419,17 @@ attrIf cond attr = class "" +{-| boolToString : takes bool and converts to true/false string +-} +boolToString : Bool -> String +boolToString bool = + if bool then + "true" + + else + "false" + + {-| boolToYesNo : takes bool and converts to yes/no string -} boolToYesNo : Bool -> String diff --git a/src/elm/Vela.elm b/src/elm/Vela.elm index 18eeb4a0d..44aa9c79a 100644 --- a/src/elm/Vela.elm +++ b/src/elm/Vela.elm @@ -167,6 +167,7 @@ module Vela exposing , updateHooksPage , updateHooksPager , updateHooksPerPage + , updateModels , updateOrgRepo , updateOrgReposPage , updateOrgReposPager @@ -436,6 +437,20 @@ type alias ServicesModel = } +updateModels : { a | repo : RepoModel } -> RepoModel -> BuildModel -> BuildGraphModel -> { a | repo : RepoModel } +updateModels m rm bm gm = + { m + | repo = + { rm + | build = + { bm + | graph = + gm + } + } + } + + defaultBuildModel : BuildModel defaultBuildModel = BuildModel "" NotAsked defaultStepsModel defaultServicesModel defaultBuildGraphModel @@ -1352,7 +1367,7 @@ decodeBuild = defaultBuildGraphModel : BuildGraphModel defaultBuildGraphModel = - BuildGraphModel "" NotAsked "" True True + BuildGraphModel "" NotAsked "" -1 True True defaultBuildGraph : BuildGraph @@ -1366,6 +1381,7 @@ encodeBuildGraphRenderData graphData = [ ( "dot", Encode.string graphData.dot ) , ( "build_id", Encode.int graphData.buildID ) , ( "filter", Encode.string graphData.filter ) + , ( "focused_node", Encode.int graphData.focusedNode ) , ( "show_services", Encode.bool graphData.showServices ) , ( "show_steps", Encode.bool graphData.showSteps ) ] @@ -1375,6 +1391,7 @@ type alias BuildGraphRenderInteropData = { dot : String , buildID : Int , filter : String + , focusedNode : Int , showServices : Bool , showSteps : Bool } @@ -1384,6 +1401,7 @@ type alias BuildGraphModel = { buildNumber : BuildNumber , graph : WebData BuildGraph , filter : String + , focusedNode : Int , showServices : Bool , showSteps : Bool } @@ -1403,6 +1421,7 @@ type alias BuildGraphNode = , startedAt : Int , finishedAt : Int , steps : List Step + , focused : Bool } @@ -1411,6 +1430,7 @@ type alias BuildGraphEdge = , source : Int , destination : Int , status : String + , focused : Bool } @@ -1431,6 +1451,8 @@ decodeBuildGraphNode = |> required "started_at" int |> required "finished_at" int |> optional "steps" (Decode.list decodeStep) [] + -- focused + |> hardcoded False decodeEdge : Decoder BuildGraphEdge @@ -1440,12 +1462,15 @@ decodeEdge = |> required "source" int |> required "destination" int |> optional "status" string "" + -- focused + |> hardcoded False type alias GraphInteraction = { event_type : String , href : String , node_id : String + , step_id : String } @@ -1454,7 +1479,8 @@ decodeGraphInteraction = Decode.succeed GraphInteraction |> required "event_type" string |> optional "href" string "" - |> optional "node_id" string "" + |> optional "node_id" string "-1" + |> optional "step_id" string "-1" {-| decodeBuilds : decodes json from vela into list of builds diff --git a/src/scss/_graph.scss b/src/scss/_graph.scss index 6f9718f60..ac7c9d4a0 100644 --- a/src/scss/_graph.scss +++ b/src/scss/_graph.scss @@ -150,6 +150,25 @@ } } +.elm-build-graph-focused-node { + // todo: remove this + display: flex; + align-items: center; + + span { + background: var(--color-primary); + color: var(--color-bg-dark); + padding: 0.2rem 0.8rem; + margin-right: 0.6rem; + } + + padding-left: 1rem; + padding-right: 1rem; + + background-color: var(--color-bg); + border-right: 1px solid var(--color-bg-light); +} + // end: classes controlled by Elm // start: classes controlled by d3 @@ -184,17 +203,20 @@ &.-hover { stroke: var(--color-primary); + stroke-width: 3; } - &.-filtered { - stroke-dasharray: 10; + // todo: not used + &.-focused { stroke: var(--color-primary); } } .d3-build-graph-node-step-a { - &:focus,&:active { - outline:none; + + &:focus, + &:active { + outline: none; } &.-hover { @@ -202,9 +224,9 @@ fill: var(--color-primary); } } - - // used? - &.-filtered { + + // todo: not used + &.-focused { outline: 1px solid var(--color-primary); outline-style: dashed; } @@ -222,6 +244,7 @@ &.-pending { stroke: var(--color-gray); stroke-dasharray: 10, 4; + stroke-width: 1; } &.-running { @@ -230,21 +253,26 @@ stroke: var(--color-yellow); stroke-dasharray: 10, 4; + stroke-width: 2; } &.-success { stroke: var(--color-gray); + stroke-width: 1; } &.-failure { stroke: var(--color-gray); + stroke-width: 1; } &.-hover { stroke: var(--color-primary); + stroke-width: 3; } - &.-filtered { + // todo: not used + &.-focused { stroke: var(--color-primary); } } diff --git a/src/static/graph.ts b/src/static/graph.ts index 095c69f90..0f3d6245c 100644 --- a/src/static/graph.ts +++ b/src/static/graph.ts @@ -64,6 +64,8 @@ function drawBaseGraph(opts, selector, content) { .attr('transform', event.transform); } + var w = 0; + var h = 0; function resetZoomAndCenter(opts, zoom) { var zoomG = d3.select(selector); @@ -73,8 +75,8 @@ function drawBaseGraph(opts, selector, content) { // the name of this variable is confusing var zoomGg = d3.select(selector); var zoomBBox = zoomGg.node().getBBox(); - var w = zoomBBox.width; - var h = zoomBBox.height; + w = zoomBBox.width; + h = zoomBBox.height; zoomGg .transition() // required to 'chain' these two instant animations together .duration(0) @@ -95,12 +97,28 @@ function drawBaseGraph(opts, selector, content) { // apply mousedown zoom effects var g = d3.select('g.node_mousedown'); if (g.empty()) { + var zoomG = d3.select(selector); + var zoomBBox = zoomG.node().getBBox(); + w = zoomBBox.width; + h = zoomBBox.height; g = buildGraphElement - .append('g') - .classed('node_mousedown', true) + .append('g'); + g.classed('node_mousedown', true) .attr('id', 'zoom'); } + // apply backdrop onclick + buildGraphElement.on('click', function (e) { + e.preventDefault(); + setTimeout( + () => { + opts.onGraphInteraction.send({ + event_type: 'backdrop_click', + }); + }, 0 + ); + }); + // this centers the graph in the viewbox, or something like that buildGraphElement = g; @@ -158,10 +176,12 @@ function drawNodes(opts, buildGraphElement, selector, edges) { var stageID = '-2'; var stageName = ''; var stageStatus = 'pending'; - if (stageInfo && stageInfo.length == 3) { + var focused = 'false'; + if (stageInfo && stageInfo.length == 4) { stageID = stageInfo[0]; stageName = stageInfo[1]; stageStatus = stageInfo[2]; + focused = stageInfo[3]; } @@ -193,15 +213,19 @@ function drawNodes(opts, buildGraphElement, selector, edges) { }; } - // apply content filter styles - // todo: make this 'searching' more intelligent (search for step names) - if (opts.contentFilter && opts.contentFilter.length > 2 && stageName.includes(opts.contentFilter)) { + // 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); }; } - // apply appropriate node outline styles + // 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 @@ -255,6 +279,12 @@ function drawNodes(opts, buildGraphElement, selector, edges) { }; } + if (edgeInfo.focused === 'true') { + restoreEdgeClass = o => { + o.classed('-hover', true); + }; + } + // apply the appropriate styles restoreEdgeClass(edgeInfo.target); } @@ -318,6 +348,15 @@ function drawNodes(opts, buildGraphElement, selector, edges) { status = stepInfo[2]; } + // todo: this image step icon mapping needs to be better + if (status === 'canceled') { + status = 'failure'; + } + + if (status === 'skipped') { + status = 'failure'; + } + // todo: static/*.png seems like a bad way to do icon images parent .append('image') @@ -345,12 +384,13 @@ function drawNodes(opts, buildGraphElement, selector, edges) { // prevents multiple link events getting fired from a single click e.stopImmediatePropagation(); setTimeout( - () => + () => { opts.onGraphInteraction.send({ event_type: 'href', href: href, step_id: '', - }), + }); + }, 0, ); }); @@ -374,10 +414,12 @@ function drawEdges(opts, buildGraphElement, selector) { var source = "-1"; var destination = "-1"; var status = "pending"; - if (edgeInfo && edgeInfo.length == 3) { + 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 @@ -386,6 +428,7 @@ function drawEdges(opts, buildGraphElement, selector) { source: source, destination: destination, status: status, + focused: focused, }); // restore base class and build modifiers @@ -414,14 +457,24 @@ function drawEdges(opts, buildGraphElement, selector) { // apply the appropriate styles restoreEdgeClass(p); + + if (focused && focused === 'true') { + restoreEdgeClass = o => { + o.classed('-hover', 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); - }); + // a.on('mouseover', e => { + // p.classed('-hover', true); + // }); + // a.on('mouseout', e => { + // restoreEdgeClass(p); + // p.classed('-hover', false); + // }); return ''; // used by filter (?) }); @@ -439,20 +492,24 @@ function applyNodesOnClick(opts, buildGraphElement, selector) { if (href !== null) { d3.select(this).on('click', function (e) { e.preventDefault(); + e.stopImmediatePropagation(); + var nodeA = d3.select(this); - // extract identifier from href - // todo: make this use title - var data = nodeA.attr('xlink:href'); nodeA.attr('xlink:href', null); - let id = data.replace('#', ''); + + var stageInfo = nodeA.attr('xlink:title').replace('#', '').split(','); + var stageID = '-1'; + if (stageInfo && stageInfo.length == 4) { + stageID = stageInfo[0]; + } + setTimeout( - () => - // todo: change node-click from "step expansion" to "focus toggle" - // opts.onGraphInteraction.send({ - // event_type: 'node_click', - // node_id: id, - // }), - 0, + () => { + opts.onGraphInteraction.send({ + event_type: 'node_click', + node_id: stageID, + }); + }, 0 ); }); }