summaryrefslogtreecommitdiff
path: root/web/src/containers/jobgraph/JobGraphDisplay.jsx
blob: 1fbcef33250db0c48ff287c6bea5026ce1aa6036 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// Copyright 2022 Acme Gating, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

import React, { useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import * as d3 from 'd3'

import { makeJobGraphKey, fetchJobGraphIfNeeded } from '../../actions/jobgraph'
import { graphviz } from 'd3-graphviz'

function makeDot(jobGraph) {
  let ret = 'digraph job_graph {'
  ret += '  rankdir=LR;\n'
  ret += '  node [shape=box];\n'
  jobGraph.forEach((job) => {
    if (job.dependencies.length) {
      job.dependencies.forEach((dep) => {
        let soft = ' [dir=back]'
        if (dep.soft) {
          soft = ' [style=dashed dir=back]'
        }
        ret += '  "' + dep.name + '" -> "' + job.name + '"' + soft + ';\n'
      })
    } else {
      ret += '  "' + job.name + '";\n'
    }
  })
  ret += '}\n'
  return ret
}

function GraphViz(props) {
  useEffect(() => {
    const gv = graphviz('#graphviz')
          .options({
            fit: false,
            zoom: true,
            tweenPaths: false,
            scale: 0.75,
          }).renderDot(props.dot)

    // Fix up the initial values of the internal transform data;
    // without this the first time we pan the graph jumps.
    const element = d3.select('.zuul-job-graph > svg')
    const transform = element[0][0].firstElementChild.attributes.transform.value
    const match = transform.match(/translate\(\d+,(\d+)\).*/)
    if (match && match.length > 0) {
      const val = parseInt(match[1])
      gv._translation.y = val
      gv._originalTransform.y = val
    }
  }, [props.dot])

  return (
    <div className="zuul-job-graph" id="graphviz"/>
  )
}

GraphViz.propTypes = {
  dot: PropTypes.string.isRequired,
}

function JobGraphDisplay(props) {
  const [dot, setDot] = useState()
  const {fetchJobGraphIfNeeded, tenant, project, pipeline, branch} = props

  useEffect(() => {
    fetchJobGraphIfNeeded(tenant, project.name, pipeline, branch)
  }, [fetchJobGraphIfNeeded, tenant, project, pipeline, branch])

  const tenantJobGraph = props.jobgraph.jobGraphs[props.tenant.name]
  const jobGraphKey = makeJobGraphKey(props.project.name,
                                      props.pipeline,
                                      props.branch)
  const jobGraph = tenantJobGraph ? tenantJobGraph[jobGraphKey] : undefined
  useEffect(() => {
    if (jobGraph) {
      setDot(makeDot(jobGraph))
    }
  }, [jobGraph])
  return (
    <>
      {dot && <GraphViz dot={dot}/>}
    </>
  )
}

JobGraphDisplay.propTypes = {
  fetchJobGraphIfNeeded: PropTypes.func,
  tenant: PropTypes.object,
  project: PropTypes.object.isRequired,
  pipeline: PropTypes.string.isRequired,
  branch: PropTypes.string.isRequired,
  jobgraph: PropTypes.object,
  dispatch: PropTypes.func,
  state: PropTypes.object,
}
function mapStateToProps(state) {
  return {
    tenant: state.tenant,
    jobgraph: state.jobgraph,
    state: state,
  }
}

const mapDispatchToProps = { fetchJobGraphIfNeeded }

export default connect(mapStateToProps, mapDispatchToProps)(JobGraphDisplay)