Skip to content

Commit 878c42a

Browse files
authored
Merge pull request #5 from Azure/haitao/rich-template
Haitao/rich template
2 parents edfcf4a + 74f8203 commit 878c42a

11 files changed

+422
-104
lines changed

graph/error.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package graph
2+
3+
type GraphErrorCode string
4+
5+
const (
6+
ErrDuplicateNode GraphErrorCode = "node with same key already exists in this graph"
7+
ErrConnectNotExistingNode GraphErrorCode = "node to connect does not exist in this graph"
8+
)
9+
10+
func (ge GraphErrorCode) Error() string {
11+
return string(ge)
12+
}
13+
14+
type GraphError struct {
15+
Code GraphErrorCode
16+
Message string
17+
}
18+
19+
func (ge *GraphError) Error() string {
20+
return ge.Code.Error() + ": " + ge.Message
21+
}
22+
23+
func (ge *GraphError) Unwrap() error {
24+
return ge.Code
25+
}

graph/graph.go

+48-22
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,57 @@ import (
44
"bytes"
55
)
66

7-
type GraphErrorCode string
8-
9-
const (
10-
ErrDuplicateNode GraphErrorCode = "node with same key already exists in this graph"
11-
ErrConnectNotExistingNode GraphErrorCode = "node to connect does not exist in this graph"
12-
)
13-
14-
func (ge GraphErrorCode) Error() string {
15-
return string(ge)
16-
}
17-
7+
// NodeConstrain is a constraint for a node in a graph
188
type NodeConstrain interface {
19-
// Name of the node, used as key in the graph, so should be unique.
20-
GetName() string
9+
DotSpec() *DotNodeSpec
2110
}
2211

12+
// EdgeSpecFunc is a function that returns the DOT specification for an edge.
13+
type EdgeSpecFunc[T NodeConstrain] func(from, to T) *DotEdgeSpec
14+
2315
type Edge[NT NodeConstrain] struct {
2416
From NT
2517
To NT
2618
}
2719

20+
// DotNodeSpec is the specification for a node in a DOT graph
21+
type DotNodeSpec struct {
22+
ID string
23+
Name string
24+
Tooltip string
25+
Shape string
26+
Style string
27+
FillColor string
28+
}
29+
30+
// DotEdgeSpec is the specification for an edge in DOT graph
31+
type DotEdgeSpec struct {
32+
FromNodeID string
33+
ToNodeID string
34+
Tooltip string
35+
Style string
36+
Color string
37+
}
38+
39+
// Graph hold the nodes and edges of a graph
2840
type Graph[NT NodeConstrain] struct {
29-
nodes map[string]NT
30-
nodeEdges map[string][]*Edge[NT]
41+
nodes map[string]NT
42+
nodeEdges map[string][]*Edge[NT]
43+
edgeSpecFunc EdgeSpecFunc[NT]
3144
}
3245

33-
func NewGraph[NT NodeConstrain]() *Graph[NT] {
46+
// NewGraph creates a new graph
47+
func NewGraph[NT NodeConstrain](edgeSpecFunc EdgeSpecFunc[NT]) *Graph[NT] {
3448
return &Graph[NT]{
35-
nodes: make(map[string]NT),
36-
nodeEdges: make(map[string][]*Edge[NT]),
49+
nodes: make(map[string]NT),
50+
nodeEdges: make(map[string][]*Edge[NT]),
51+
edgeSpecFunc: edgeSpecFunc,
3752
}
3853
}
3954

55+
// AddNode adds a node to the graph
4056
func (g *Graph[NT]) AddNode(n NT) error {
41-
nodeKey := n.GetName()
57+
nodeKey := n.DotSpec().ID
4258
if _, ok := g.nodes[nodeKey]; ok {
4359
return ErrDuplicateNode
4460
}
@@ -64,17 +80,27 @@ func (g *Graph[NT]) Connect(from, to string) error {
6480

6581
// https://en.wikipedia.org/wiki/DOT_(graph_description_language)
6682
func (g *Graph[NT]) ToDotGraph() (string, error) {
67-
edges := make(map[string][]string)
83+
nodes := make([]*DotNodeSpec, 0)
84+
for _, node := range g.nodes {
85+
nodes = append(nodes, node.DotSpec())
86+
}
87+
88+
edges := make([]*DotEdgeSpec, 0)
6889
for _, nodeEdges := range g.nodeEdges {
6990
for _, edge := range nodeEdges {
70-
edges[edge.From.GetName()] = append(edges[edge.From.GetName()], edge.To.GetName())
91+
edges = append(edges, g.edgeSpecFunc(edge.From, edge.To))
7192
}
7293
}
7394

7495
buf := new(bytes.Buffer)
75-
err := digraphTemplate.Execute(buf, edges)
96+
err := digraphTemplate.Execute(buf, templateRef{Nodes: nodes, Edges: edges})
7697
if err != nil {
7798
return "", err
7899
}
79100
return buf.String(), nil
80101
}
102+
103+
type templateRef struct {
104+
Nodes []*DotNodeSpec
105+
Edges []*DotEdgeSpec
106+
}

graph/graph_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package graph_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/Azure/go-asyncjob/graph"
8+
)
9+
10+
func TestSimpleJob(t *testing.T) {
11+
g := graph.NewGraph[*testNode](edgeSpecFromConnection)
12+
root := &testNode{Name: "root"}
13+
g.AddNode(root)
14+
calc1 := &testNode{Name: "calc1"}
15+
g.AddNode(calc1)
16+
calc2 := &testNode{Name: "calc2"}
17+
g.AddNode(calc2)
18+
summary := &testNode{Name: "summary"}
19+
g.AddNode(summary)
20+
21+
g.Connect(root.DotSpec().ID, calc1.DotSpec().ID)
22+
g.Connect(root.DotSpec().ID, calc2.DotSpec().ID)
23+
g.Connect(calc1.DotSpec().ID, summary.DotSpec().ID)
24+
g.Connect(calc2.DotSpec().ID, summary.DotSpec().ID)
25+
26+
graph, err := g.ToDotGraph()
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
fmt.Println(graph)
31+
}
32+
33+
type testNode struct {
34+
Name string
35+
}
36+
37+
func (tn *testNode) DotSpec() *graph.DotNodeSpec {
38+
return &graph.DotNodeSpec{
39+
ID: tn.Name,
40+
Name: tn.Name,
41+
Tooltip: tn.Name,
42+
Shape: "box",
43+
Style: "filled",
44+
FillColor: "green",
45+
}
46+
}
47+
48+
func edgeSpecFromConnection(from, to *testNode) *graph.DotEdgeSpec {
49+
return &graph.DotEdgeSpec{
50+
FromNodeID: from.DotSpec().ID,
51+
ToNodeID: to.DotSpec().ID,
52+
Tooltip: fmt.Sprintf("%s -> %s", from.DotSpec().Name, to.DotSpec().Name),
53+
Style: "solid",
54+
Color: "black",
55+
}
56+
}

graph/template.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,49 @@ import (
44
"text/template"
55
)
66

7+
// https://www.graphviz.org/docs/
8+
// http://magjac.com/graphviz-visual-editor/
9+
710
var digraphTemplate = template.Must(template.New("digraph").Parse(digraphTemplateText))
811

912
const digraphTemplateText = `digraph {
1013
compound = "true"
1114
newrank = "true"
1215
subgraph "root" {
13-
{{ range $from, $toList := $}}{{ range $_, $to := $toList}} "{{$from}}" -> "{{$to}}"
14-
{{ end }}{{ end }} }
16+
{{ range $node := $.Nodes}} {{$node.ID}} [label="{{$node.Name}}" shape={{$node.Shape}} style={{$node.Style}} tooltip="{{$node.Tooltip}}" fillcolor={{$node.FillColor}}]
17+
{{ end }}
18+
{{ range $edge := $.Edges}} {{$edge.FromNodeID}} -> {{$edge.ToNodeID}} [style={{$edge.Style}} tooltip="{{$edge.Tooltip}}" color={{$edge.Color}}]
19+
{{ end }}
20+
}
1521
}`
22+
23+
/* ideal output
24+
digraph G {
25+
jobroot [shape=triangle style=filled fillcolor=gold tooltip="State: Failed\nDuration:5s"]
26+
param_servername [label="servername" shape=doublecircle style=filled fillcolor=green tooltip="Value: dummy.server.io"]
27+
param_table1 [label="table1" shape=doublecircle style=filled fillcolor=green tooltip="Value: table1"]
28+
param_query1 [label="query1" shape=doublecircle style=filled fillcolor=green tooltip="Value: select * from table1"]
29+
param_table2 [label="table2" shape=doublecircle style=filled fillcolor=green tooltip="Value: table2"]
30+
param_query2 [label="query2" shape=doublecircle style=filled fillcolor=green tooltip="Value: select * from table2"]
31+
jobroot -> param_servername [tooltip="time:2022-10-28T21:16:07Z"]
32+
param_servername -> func_getConnection
33+
func_getConnection [label="getConnection" shape=ellipse style=filled fillcolor=green tooltip="State: Finished\nDuration:1s"]
34+
func_query1 [label="query1" shape=ellipse style=filled fillcolor=green tooltip="State: Finished\nDuration:2s"]
35+
func_query2 [label="query2" shape=ellipse style=filled fillcolor=red tooltip="State: Failed\nDuration:2s"]
36+
jobroot -> param_table1 [style=bold color=green tooltip="time:2022-10-28T21:16:07Z"]
37+
param_table1 -> func_query1 [tooltip="time:2022-10-28T21:16:07Z"]
38+
jobroot -> param_query1 [tooltip="time:2022-10-28T21:16:07Z"]
39+
param_query1 -> func_query1
40+
jobroot -> param_table2 [tooltip="time:2022-10-28T21:16:07Z"]
41+
param_table2 -> func_query2
42+
jobroot -> param_query2 [tooltip="time:2022-10-28T21:16:07Z"]
43+
param_query2 -> func_query2
44+
func_getConnection -> func_query1
45+
func_query1 -> func_summarize
46+
func_getConnection -> func_query2
47+
func_query2 -> func_summarize [color=red]
48+
func_summarize [label="summarize" shape=ellipse style=filled fillcolor=red tooltip="State: Blocked"]
49+
func_email [label="email" shape=ellipse style=filled tooltip="State: Pending"]
50+
func_summarize -> func_email [style=dotted]
51+
}
52+
*/

graph_node.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package asyncjob
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/Azure/go-asyncjob/graph"
8+
)
9+
10+
type stepNode struct {
11+
StepMeta
12+
}
13+
14+
func newStepNode(sm StepMeta) *stepNode {
15+
return &stepNode{
16+
StepMeta: sm,
17+
}
18+
}
19+
20+
func (sn *stepNode) DotSpec() *graph.DotNodeSpec {
21+
return &graph.DotNodeSpec{
22+
ID: sn.getID(),
23+
Name: sn.GetName(),
24+
Shape: sn.getShape(),
25+
Style: "filled",
26+
FillColor: sn.getFillColor(),
27+
Tooltip: sn.getTooltip(),
28+
}
29+
}
30+
31+
func (sn *stepNode) getShape() string {
32+
switch sn.getType() {
33+
case stepTypeRoot:
34+
return "triangle"
35+
case stepTypeParam:
36+
return "doublecircle"
37+
case stepTypeTask:
38+
return "box"
39+
default:
40+
return "egg"
41+
}
42+
}
43+
44+
func (sn *stepNode) getFillColor() string {
45+
switch sn.GetState() {
46+
case StepStatePending:
47+
return "gray"
48+
case StepStateRunning:
49+
return "yellow"
50+
case StepStateCompleted:
51+
return "green"
52+
case StepStateFailed:
53+
return "red"
54+
default:
55+
return "white"
56+
}
57+
}
58+
59+
func (sn *stepNode) getTooltip() string {
60+
state := sn.GetState()
61+
executionData := sn.ExecutionData()
62+
63+
if state != StepStatePending && executionData != nil {
64+
return fmt.Sprintf("Type: %s\\nName: %s\\nState: %s\\nStartAt: %s\\nDuration: %s", string(sn.getType()), sn.GetName(), state, executionData.StartTime.Format(time.RFC3339Nano), executionData.Duration)
65+
}
66+
67+
return fmt.Sprintf("Type: %s\\nName: %s", sn.getType(), sn.GetName())
68+
}
69+
70+
func stepConn(snFrom, snTo *stepNode) *graph.DotEdgeSpec {
71+
edgeSpec := &graph.DotEdgeSpec{
72+
FromNodeID: snFrom.getID(),
73+
ToNodeID: snTo.getID(),
74+
Color: "black",
75+
Style: "bold",
76+
}
77+
78+
// update edge color, tooltip if NodeTo is started already.
79+
if snTo.GetState() != StepStatePending {
80+
executionData := snTo.ExecutionData()
81+
edgeSpec.Tooltip = fmt.Sprintf("Time: %s", executionData.StartTime.Format(time.RFC3339Nano))
82+
}
83+
84+
fromNodeState := snFrom.GetState()
85+
if fromNodeState == StepStateCompleted {
86+
edgeSpec.Color = "green"
87+
} else if fromNodeState == StepStateFailed {
88+
edgeSpec.Color = "red"
89+
}
90+
91+
return edgeSpec
92+
}

0 commit comments

Comments
 (0)