Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions tasktiger_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
__all__ = ["TaskTigerView", "tasktiger_admin"]

tasktiger_admin = Blueprint(
"tasktiger_admin", __name__, template_folder="templates"
)
"tasktiger_admin", __name__, template_folder="templates",
static_folder="assets"
)
140 changes: 140 additions & 0 deletions tasktiger_admin/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from __future__ import annotations
from typing import Dict, List
from tasktiger import Task, TaskNotFound
from tasktiger._internal import COMPLETED
import textwrap

WRAP_MAX_CHARS = 40

class Graph:
def __init__(self):
"""
Generate vis-network graph for task workflow display
"""

def generate(self, tiger, queue, state, task_id) -> VisData:
nodes: Dict[str, VisNode] = {}
edges: Dict[str, VisEdge] = {}
visited: set[str] = set()
try:
task: Task = Task.from_id(tiger, queue, state, task_id)
except:
# check completion state
if state != COMPLETED:
task: Task = Task.from_id(tiger, queue, COMPLETED, task_id)
self.generate_node_edges(task, nodes, edges, visited)

visData: VisData = VisData(list(nodes.values()), list(edges.values()))
return visData

def generate_node_edges(
self,
task: Task,
nodes: Dict[str, VisNode],
edges: Dict[str, VisNode],
visited: set[str],
level: int = 0
):
if not task:
return

node: VisNode | None = None

if task.id in visited:
return
label = self.get_label(task)
# colors for nodes can be set in groups on the js side
node: VisNode = VisNode(task.id, label)
nodes[task.id] = node
node.level = level
visited.add(task.id)
if task.state:
node.group = task.state
else:
node.group = "unknown"
nodes
if task.depends:
dep_tasks: List[Task] = task.get_dependencies()
for dep_task in dep_tasks:
self.generate_node_edges(dep_task, nodes, edges, visited, level + 1)
edge_id: str = dep_task.id + "->" + node.id
if edge_id not in edges:
edge = VisEdge(edge_id, "", dep_task.id, node.id, arrows=Arrows())
# colors cannot be set in groups so we set here
edge.color = Color("#6466f3")
edges[edge_id] = edge

def get_label(self, task: Task):
label = "ID: " + task.id[0:6] + "\n"
if task.state:
label += "Run At: " + task.ts.strftime("%Y-%m-%d %H:%M:%S") + "\n"
label += "Queue: " + task.queue + "\n"
label += "State: " + task.state + "\n"
label += "Func: " + task.serialized_func + "\n"
label += "args: " + self.wrap(str(task.args)) + "\n"
label += "kwargs: " + self.wrap(str(task.kwargs)) + "\n"
else:
label += "Not Found" + "\n"
return label

def wrap(self, text: str):
return textwrap.fill(text, width=WRAP_MAX_CHARS)

class VisData:
def __init__(self, nodes: List[VisNode], edges: List[VisEdge]):
self.nodes = nodes
self.edges = edges


class VisNode:
def __init__(self, id: str, label: str, group: str = None):
self.id: str = id
self.label: str = label
self.level: int = 0
if group:
self.group: str = group


# cannot use from and to for attributes
# so we implement edges as a dict instead
class VisEdge(Dict):
def __init__(
self,
id: str,
label: str,
From: str,
To: str,
arrows: Arrows | None = None,
color: Color | None = None,
):
self["id"] = id
self["label"] = label
self["from"] = From
self["to"] = To
if arrows:
self["arrows"] = arrows
if color:
self["color"] = color
self["smooth"] = False


class Arrows:
def __init__(self):
self.to = {"enabled": True, "type": "arrow"}


class Color:
def __init__(
self,
color: str | None,
highlight: str | None = None,
hover: str | None = None,
opacity: float = 0,
):
self.color = color
if highlight:
self.highlight = highlight
if hover:
self.hover = hover
if opacity:
self.opacity = opacity
Empty file.
114 changes: 114 additions & 0 deletions tasktiger_admin/static/js/graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
function load_graph() {
url = `/tasktiger/${task_data["queue"]}/${task_data["state"]}/${task_data["id"]}/graph`;
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async response => {
load_graph_data(await response.json());
}).catch(error => {
console.error('Error:', error);
});
}

function load_graph_data(data) {
console.log(data);
nodes = new vis.DataSet(data.nodes);
edges = new vis.DataSet(data.edges);
var visData = {
nodes: nodes,
edges: edges,
};
var container = document.getElementById("graph");
var options = {
nodes: {
mass: 4,
font: {
face: 'monospace',
size: 14,
color: 'black',
align: 'left'
}
},
edges: {
physics: false,
},
layout: {
randomSeed: 0,
improvedLayout: true,
hierarchical: getTreeLayout()
},
physics: {
enabled: false
},
groups: {
completed: {
color: { background: "#8ed1f0" },
borderWidth: 2,
shape: 'box',
mass: 2
},
active: {
color: { background: "#8ef0a3" },
borderWidth: 2,
shape: 'box',
mass: 2
},
waiting: {
color: { background: "#d3e473" },
borderWidth: 2,
shape: 'box',
mass: 2
},
scheduled: {
color: { background: "#b173e4" },
borderWidth: 2,
shape: 'box',
mass: 2
},
queued: {
color: { background: "#73e4de" },
borderWidth: 2,
shape: 'box',
mass: 2
},
error: {
color: { background: "#f07272" },
borderWidth: 2,
shape: 'box',
mass: 2
},
unknown: {
color: { background: "#d43b3b" },
borderWidth: 2,
shape: 'box',
mass: 2
}
}
};
var network = new vis.Network(container, visData, options);

network.once('afterDrawing', (ctx) => {
// workaround for resizing
container.style.height = '300px';
});
}

function getTreeLayout() {
return {
direction: "RL",
sortMethod: 'directed',
parentCentralization: true,
edgeMinimization: true,
levelSeparation: 600,
nodeSpacing: 300,
treeSpacing: 600,
blockShifting: true,
shakeTowards: 'leaves'
};
}

window.addEventListener("load", event => {
load_graph();
});
6 changes: 6 additions & 0 deletions tasktiger_admin/templates/tasktiger_admin/tasktiger.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ <h2>TaskTiger</h2>
<th>Active</th>
<th>Scheduled</th>
<th>Error</th>
<th>Waiting</th>
<th>Completed</th>
</tr>
</thead>
<tbody class="searchable">
Expand All @@ -30,6 +32,8 @@ <h2>TaskTiger</h2>
<td>{{ group_stats.active }}</td>
<td>{{ group_stats.scheduled }}</td>
<td>{{ group_stats.error }}</td>
<td>{{ group_stats.waiting }}</td>
<td>{{ group_stats.completed }}</td>
</tr>
{% endif %}

Expand All @@ -44,6 +48,8 @@ <h2>TaskTiger</h2>
<td><a href="{{ queue }}/active/">{{ stats.active }}</td>
<td><a href="{{ queue }}/scheduled/">{{ stats.scheduled }}</td>
<td><a href="{{ queue }}/error/">{{ stats.error }}</a></td>
<td><a href="{{ queue }}/waiting/">{{ stats.waiting }}</a></td>
<td><a href="{{ queue }}/completed/">{{ stats.completed }}</a></td>
</tr>
{% endfor %}
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ <h2><a href="../../">TaskTiger</a> – {{ queue }} ({{ state }}, {{ n }} items)
<table class="metrics table table-striped table-bordered">
<thead>
<tr>
<th style="width:135px;">ID</th>
<th style="width:135px;">Run At</th>
<th style="min-width:135px;">Func</th>
<th style="min-width:135px;">Args</th>
Expand All @@ -20,7 +21,8 @@ <h2><a href="../../">TaskTiger</a> – {{ queue }} ({{ state }}, {{ n }} items)
<tbody>
{% for task in tasks %}
<tr>
<td><a href="{{ task.id }}/">{{ task.ts.strftime("%Y-%m-%d %H:%M:%S") }}</a></td>
<td><a href="{{ task.id }}/">{{ task.id[:6] }}</a></td>
<td>{{ task.ts.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ task.serialized_func }}</td>
<td>{{ task.args }} {{ task.kwargs }}</td>
<td>{% if task.executions %}{{ task.executions.0.exception_name }}{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@
cursor: pointer;
}
</style>
<script
type="text/javascript"
src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"
></script>

<script>
this.task_data = {
queue: "{{ task.queue }}",
state: "{{ task.state }}",
id: "{{ task.id }}",
};
</script>
<script src="{{url_for('static', filename='js/graph.js')}}"></script>
<style type="text/css" src="{{url_for('static', filename='css/main.css')}}"></style>

{% endblock %}

{% block body %}
Expand All @@ -36,6 +51,37 @@ <h2><a href="../../../">TaskTiger</a> – <a href="../">{{ queue }} ({{ state }
<th>Kwargs</th>
<td>{{ task_data.kwargs }}</td>
</tr>
<tr>
<th>Dependencies</th>
<td>
<details>
<summary>Show/Hide</summary>
{% for task in task_dependencies %}
<div>
<a href="../../../{{ task.queue }}/{{ task.state }}/{{ task.id }}" target="_blank">{{ task.id[:6] }}</a>
{% if task.state %}
| {{ task.state }}
| {{ task.ts.strftime("%Y-%m-%d %H:%M:%S") }}
| {{ task.serialized_func }}
| {{ task.args }} {{ task.kwargs }}
{% endif %}
</div>
{% endfor %}
</details>
</td>
</tr>
<tr>
<th>Workflow</th>
<td>
<details>
<summary>Show/Hide</summary>
<div>
<input type="button" value="Refresh" onclick="javascript:load_graph()">
<div id="graph"></div>
</div>
</details>
</td>
</tr>
{% if task.ts %}
<tr>
<th>Run At</th>
Expand Down
5 changes: 3 additions & 2 deletions tasktiger_admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
@click.option("-p", "--port", help="Redis server port")
@click.option("-a", "--password", help="Redis password")
@click.option("-n", "--db", help="Redis database number")
@click.option("-b", "--bind", help="bind addr, default=127.0.0.1")
@click.option("-l", "--listen", help="Admin port to listen on")
def run_admin(host, port, db, password, listen):
def run_admin(host, port, db, password, bind, listen):
conn = redis.Redis(
host, int(port or 6379), int(db or 0), password, decode_responses=True
)
Expand All @@ -23,4 +24,4 @@ def run_admin(host, port, db, password, listen):
admin.add_view(
TaskTigerView(tiger, name="TaskTiger", endpoint="tasktiger")
)
app.run(debug=True, port=int(listen or 5000))
app.run(debug=True, host=(bind or "127.0.0.1"), port=int(listen or 5000))
Loading