Skip to content

Commit

Permalink
Merge pull request #5 from Azure/haitao/rich-template
Browse files Browse the repository at this point in the history
Haitao/rich template
  • Loading branch information
haitch committed Nov 1, 2022
2 parents edfcf4a + 74f8203 commit 878c42a
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 104 deletions.
25 changes: 25 additions & 0 deletions graph/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package graph

type GraphErrorCode string

const (
ErrDuplicateNode GraphErrorCode = "node with same key already exists in this graph"
ErrConnectNotExistingNode GraphErrorCode = "node to connect does not exist in this graph"
)

func (ge GraphErrorCode) Error() string {
return string(ge)
}

type GraphError struct {
Code GraphErrorCode
Message string
}

func (ge *GraphError) Error() string {
return ge.Code.Error() + ": " + ge.Message
}

func (ge *GraphError) Unwrap() error {
return ge.Code
}
70 changes: 48 additions & 22 deletions graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,57 @@ import (
"bytes"
)

type GraphErrorCode string

const (
ErrDuplicateNode GraphErrorCode = "node with same key already exists in this graph"
ErrConnectNotExistingNode GraphErrorCode = "node to connect does not exist in this graph"
)

func (ge GraphErrorCode) Error() string {
return string(ge)
}

// NodeConstrain is a constraint for a node in a graph
type NodeConstrain interface {
// Name of the node, used as key in the graph, so should be unique.
GetName() string
DotSpec() *DotNodeSpec
}

// EdgeSpecFunc is a function that returns the DOT specification for an edge.
type EdgeSpecFunc[T NodeConstrain] func(from, to T) *DotEdgeSpec

type Edge[NT NodeConstrain] struct {
From NT
To NT
}

// DotNodeSpec is the specification for a node in a DOT graph
type DotNodeSpec struct {
ID string
Name string
Tooltip string
Shape string
Style string
FillColor string
}

// DotEdgeSpec is the specification for an edge in DOT graph
type DotEdgeSpec struct {
FromNodeID string
ToNodeID string
Tooltip string
Style string
Color string
}

// Graph hold the nodes and edges of a graph
type Graph[NT NodeConstrain] struct {
nodes map[string]NT
nodeEdges map[string][]*Edge[NT]
nodes map[string]NT
nodeEdges map[string][]*Edge[NT]
edgeSpecFunc EdgeSpecFunc[NT]
}

func NewGraph[NT NodeConstrain]() *Graph[NT] {
// NewGraph creates a new graph
func NewGraph[NT NodeConstrain](edgeSpecFunc EdgeSpecFunc[NT]) *Graph[NT] {
return &Graph[NT]{
nodes: make(map[string]NT),
nodeEdges: make(map[string][]*Edge[NT]),
nodes: make(map[string]NT),
nodeEdges: make(map[string][]*Edge[NT]),
edgeSpecFunc: edgeSpecFunc,
}
}

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

// https://en.wikipedia.org/wiki/DOT_(graph_description_language)
func (g *Graph[NT]) ToDotGraph() (string, error) {
edges := make(map[string][]string)
nodes := make([]*DotNodeSpec, 0)
for _, node := range g.nodes {
nodes = append(nodes, node.DotSpec())
}

edges := make([]*DotEdgeSpec, 0)
for _, nodeEdges := range g.nodeEdges {
for _, edge := range nodeEdges {
edges[edge.From.GetName()] = append(edges[edge.From.GetName()], edge.To.GetName())
edges = append(edges, g.edgeSpecFunc(edge.From, edge.To))
}
}

buf := new(bytes.Buffer)
err := digraphTemplate.Execute(buf, edges)
err := digraphTemplate.Execute(buf, templateRef{Nodes: nodes, Edges: edges})
if err != nil {
return "", err
}
return buf.String(), nil
}

type templateRef struct {
Nodes []*DotNodeSpec
Edges []*DotEdgeSpec
}
56 changes: 56 additions & 0 deletions graph/graph_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package graph_test

import (
"fmt"
"testing"

"github.com/Azure/go-asyncjob/graph"
)

func TestSimpleJob(t *testing.T) {
g := graph.NewGraph[*testNode](edgeSpecFromConnection)
root := &testNode{Name: "root"}
g.AddNode(root)
calc1 := &testNode{Name: "calc1"}
g.AddNode(calc1)
calc2 := &testNode{Name: "calc2"}
g.AddNode(calc2)
summary := &testNode{Name: "summary"}
g.AddNode(summary)

g.Connect(root.DotSpec().ID, calc1.DotSpec().ID)
g.Connect(root.DotSpec().ID, calc2.DotSpec().ID)
g.Connect(calc1.DotSpec().ID, summary.DotSpec().ID)
g.Connect(calc2.DotSpec().ID, summary.DotSpec().ID)

graph, err := g.ToDotGraph()
if err != nil {
t.Fatal(err)
}
fmt.Println(graph)
}

type testNode struct {
Name string
}

func (tn *testNode) DotSpec() *graph.DotNodeSpec {
return &graph.DotNodeSpec{
ID: tn.Name,
Name: tn.Name,
Tooltip: tn.Name,
Shape: "box",
Style: "filled",
FillColor: "green",
}
}

func edgeSpecFromConnection(from, to *testNode) *graph.DotEdgeSpec {
return &graph.DotEdgeSpec{
FromNodeID: from.DotSpec().ID,
ToNodeID: to.DotSpec().ID,
Tooltip: fmt.Sprintf("%s -> %s", from.DotSpec().Name, to.DotSpec().Name),
Style: "solid",
Color: "black",
}
}
41 changes: 39 additions & 2 deletions graph/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,49 @@ import (
"text/template"
)

// https://www.graphviz.org/docs/
// http://magjac.com/graphviz-visual-editor/

var digraphTemplate = template.Must(template.New("digraph").Parse(digraphTemplateText))

const digraphTemplateText = `digraph {
compound = "true"
newrank = "true"
subgraph "root" {
{{ range $from, $toList := $}}{{ range $_, $to := $toList}} "{{$from}}" -> "{{$to}}"
{{ end }}{{ end }} }
{{ range $node := $.Nodes}} {{$node.ID}} [label="{{$node.Name}}" shape={{$node.Shape}} style={{$node.Style}} tooltip="{{$node.Tooltip}}" fillcolor={{$node.FillColor}}]
{{ end }}
{{ range $edge := $.Edges}} {{$edge.FromNodeID}} -> {{$edge.ToNodeID}} [style={{$edge.Style}} tooltip="{{$edge.Tooltip}}" color={{$edge.Color}}]
{{ end }}
}
}`

/* ideal output
digraph G {
jobroot [shape=triangle style=filled fillcolor=gold tooltip="State: Failed\nDuration:5s"]
param_servername [label="servername" shape=doublecircle style=filled fillcolor=green tooltip="Value: dummy.server.io"]
param_table1 [label="table1" shape=doublecircle style=filled fillcolor=green tooltip="Value: table1"]
param_query1 [label="query1" shape=doublecircle style=filled fillcolor=green tooltip="Value: select * from table1"]
param_table2 [label="table2" shape=doublecircle style=filled fillcolor=green tooltip="Value: table2"]
param_query2 [label="query2" shape=doublecircle style=filled fillcolor=green tooltip="Value: select * from table2"]
jobroot -> param_servername [tooltip="time:2022-10-28T21:16:07Z"]
param_servername -> func_getConnection
func_getConnection [label="getConnection" shape=ellipse style=filled fillcolor=green tooltip="State: Finished\nDuration:1s"]
func_query1 [label="query1" shape=ellipse style=filled fillcolor=green tooltip="State: Finished\nDuration:2s"]
func_query2 [label="query2" shape=ellipse style=filled fillcolor=red tooltip="State: Failed\nDuration:2s"]
jobroot -> param_table1 [style=bold color=green tooltip="time:2022-10-28T21:16:07Z"]
param_table1 -> func_query1 [tooltip="time:2022-10-28T21:16:07Z"]
jobroot -> param_query1 [tooltip="time:2022-10-28T21:16:07Z"]
param_query1 -> func_query1
jobroot -> param_table2 [tooltip="time:2022-10-28T21:16:07Z"]
param_table2 -> func_query2
jobroot -> param_query2 [tooltip="time:2022-10-28T21:16:07Z"]
param_query2 -> func_query2
func_getConnection -> func_query1
func_query1 -> func_summarize
func_getConnection -> func_query2
func_query2 -> func_summarize [color=red]
func_summarize [label="summarize" shape=ellipse style=filled fillcolor=red tooltip="State: Blocked"]
func_email [label="email" shape=ellipse style=filled tooltip="State: Pending"]
func_summarize -> func_email [style=dotted]
}
*/
92 changes: 92 additions & 0 deletions graph_node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package asyncjob

import (
"fmt"
"time"

"github.com/Azure/go-asyncjob/graph"
)

type stepNode struct {
StepMeta
}

func newStepNode(sm StepMeta) *stepNode {
return &stepNode{
StepMeta: sm,
}
}

func (sn *stepNode) DotSpec() *graph.DotNodeSpec {
return &graph.DotNodeSpec{
ID: sn.getID(),
Name: sn.GetName(),
Shape: sn.getShape(),
Style: "filled",
FillColor: sn.getFillColor(),
Tooltip: sn.getTooltip(),
}
}

func (sn *stepNode) getShape() string {
switch sn.getType() {
case stepTypeRoot:
return "triangle"
case stepTypeParam:
return "doublecircle"
case stepTypeTask:
return "box"
default:
return "egg"
}
}

func (sn *stepNode) getFillColor() string {
switch sn.GetState() {
case StepStatePending:
return "gray"
case StepStateRunning:
return "yellow"
case StepStateCompleted:
return "green"
case StepStateFailed:
return "red"
default:
return "white"
}
}

func (sn *stepNode) getTooltip() string {
state := sn.GetState()
executionData := sn.ExecutionData()

if state != StepStatePending && executionData != nil {
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)
}

return fmt.Sprintf("Type: %s\\nName: %s", sn.getType(), sn.GetName())
}

func stepConn(snFrom, snTo *stepNode) *graph.DotEdgeSpec {
edgeSpec := &graph.DotEdgeSpec{
FromNodeID: snFrom.getID(),
ToNodeID: snTo.getID(),
Color: "black",
Style: "bold",
}

// update edge color, tooltip if NodeTo is started already.
if snTo.GetState() != StepStatePending {
executionData := snTo.ExecutionData()
edgeSpec.Tooltip = fmt.Sprintf("Time: %s", executionData.StartTime.Format(time.RFC3339Nano))
}

fromNodeState := snFrom.GetState()
if fromNodeState == StepStateCompleted {
edgeSpec.Color = "green"
} else if fromNodeState == StepStateFailed {
edgeSpec.Color = "red"
}

return edgeSpec
}
Loading

0 comments on commit 878c42a

Please sign in to comment.