160 lines
6.0 KiB
Plaintext
160 lines
6.0 KiB
Plaintext
# cockpit_timeline.dspy - Returns pipeline step timeline as Bricks widget JSON
|
|
# params: iteration_id (optional), task_id (optional)
|
|
|
|
iteration_id = (params_kw or {}).get('iteration_id', '')
|
|
task_id = (params_kw or {}).get('task_id', '')
|
|
dbname = get_module_dbname('pipeline-sdlc')
|
|
|
|
if not task_id and iteration_id:
|
|
# Look up task_id from iteration
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
iters = await sor.sqlExe(
|
|
"SELECT task_id FROM sd_iterations WHERE id=${iid}$", {"iid": iteration_id})
|
|
if iters and hasattr(iters[0], 'task_id') and iters[0].task_id:
|
|
task_id = iters[0].task_id
|
|
|
|
if not task_id:
|
|
return {
|
|
"widgettype": "VBox",
|
|
"options": {"padding": "20px", "alignItems": "center"},
|
|
"subwidgets": [
|
|
{"widgettype": "Text", "options": {
|
|
"text": "暂无产线任务。\n请先创建迭代以启动开发产线。",
|
|
"cfontsize": 0.9, "color": "#94a3b8", "textAlign": "center"
|
|
}}
|
|
]
|
|
}
|
|
|
|
# Query pipeline steps via pipeline_service
|
|
try:
|
|
from pipeline_service.executor import get_task_steps
|
|
steps = await get_task_steps(task_id)
|
|
except Exception:
|
|
# Fallback: query pipeline_tasks and pipeline_task_steps directly
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
steps = await sor.sqlExe(
|
|
"SELECT step_name, state, step_order, step_config, started_at, completed_at "
|
|
"FROM pipeline_task_steps WHERE task_id=${tid}$ ORDER BY step_order",
|
|
{"tid": task_id}
|
|
)
|
|
|
|
# Status icon mapping
|
|
status_config = {
|
|
'completed': {'icon': '✓', 'color': '#22c55e', 'bg': '#f0fdf4', 'label': '已完成'},
|
|
'running': {'icon': '⟳', 'color': '#f59e0b', 'bg': '#fffbeb', 'label': '执行中'},
|
|
'pending': {'icon': '○', 'color': '#94a3b8', 'bg': '#f8fafc', 'label': '待执行'},
|
|
'waiting': {'icon': '⏳', 'color': '#3b82f6', 'bg': '#eff6ff', 'label': '等待中'},
|
|
'failed': {'icon': '✗', 'color': '#ef4444', 'bg': '#fef2f2', 'label': '失败'},
|
|
'paused': {'icon': '⏸', 'color': '#8b5cf6', 'bg': '#f5f3ff', 'label': '已暂停'},
|
|
'skipped': {'icon': '⊘', 'color': '#94a3b8', 'bg': '#f1f5f9', 'label': '已跳过'},
|
|
}
|
|
|
|
step_widgets = []
|
|
|
|
# Group by phase
|
|
current_phase = None
|
|
for step in (steps or []):
|
|
sn = step.step_name if hasattr(step, 'step_name') else step.get('step_name', '')
|
|
state = step.state if hasattr(step, 'state') else step.get('state', 'pending')
|
|
step_config_raw = step.step_config if hasattr(step, 'step_config') else step.get('step_config', '{}')
|
|
step_order = step.step_order if hasattr(step, 'step_order') else step.get('step_order', 0)
|
|
|
|
# Parse step_config for phase grouping
|
|
try:
|
|
cfg = json.loads(step_config_raw) if isinstance(step_config_raw, str) else (step_config_raw or {})
|
|
except Exception:
|
|
cfg = {}
|
|
phase = cfg.get('phase', '')
|
|
phase_label = cfg.get('phase_label', '')
|
|
|
|
# Phase header
|
|
if phase and phase != current_phase:
|
|
current_phase = phase
|
|
step_widgets.append({
|
|
"widgettype": "HBox",
|
|
"options": {
|
|
"width": "100%", "alignItems": "center",
|
|
"padding": "12px 0 6px 0", "gap": "8px"
|
|
},
|
|
"subwidgets": [
|
|
{"widgettype": "Svg", "options": {
|
|
"svg": '<svg width="16" height="16" viewBox="0 0 16 16"><polygon points="8,0 16,8 8,16 0,8" fill="#cbd5e1"/></svg>',
|
|
"width": "12px", "height": "12px"
|
|
}},
|
|
{"widgettype": "Text", "options": {
|
|
"text": phase_label or phase, "cfontsize": 0.85,
|
|
"color": "#64748b", "fontWeight": "bold"
|
|
}}
|
|
]
|
|
})
|
|
|
|
# Status
|
|
sc = status_config.get(state, status_config['pending'])
|
|
# Connection line
|
|
connector = {
|
|
"widgettype": "VBox",
|
|
"options": {"width": "2px", "height": "100%", "minHeight": "8px",
|
|
"bgcolor": "#e2e8f0", "marginLeft": "9px"}
|
|
}
|
|
|
|
step_widgets.append({
|
|
"widgettype": "HBox",
|
|
"options": {
|
|
"width": "100%", "alignItems": "flex-start",
|
|
"padding": "4px 0", "gap": "10px", "cursor": "pointer"
|
|
},
|
|
"binds": [{
|
|
"wid": "self", "event": "click",
|
|
"actiontype": "popup",
|
|
"popup_desc": {
|
|
"widgettype": "urlwidget",
|
|
"options": {
|
|
"url": entire_url(f"/pipeline_task/step_panel.ui?task_id=" + task_id + "&step_name=" + sn)
|
|
}
|
|
},
|
|
"popupwindow": True,
|
|
"options": {"title": f"步骤详情: {sn}", "width": "700px", "height": "500px"}
|
|
}],
|
|
"subwidgets": [
|
|
{
|
|
"widgettype": "VBox",
|
|
"options": {"alignItems": "center", "minWidth": "24px"},
|
|
"subwidgets": [
|
|
{"widgettype": "Text", "options": {
|
|
"text": sc['icon'], "cfontsize": 1.2,
|
|
"color": sc['color'], "fontWeight": "bold"
|
|
}}
|
|
]
|
|
},
|
|
{
|
|
"widgettype": "VBox",
|
|
"options": {"flex": 1, "gap": "2px"},
|
|
"subwidgets": [
|
|
{"widgettype": "Text", "options": {
|
|
"text": sn, "cfontsize": 0.9,
|
|
"color": "#1e293b", "fontWeight": "500"
|
|
}},
|
|
{"widgettype": "Text", "options": {
|
|
"text": sc['label'], "cfontsize": 0.75,
|
|
"color": sc['color']
|
|
}}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
|
|
if not step_widgets:
|
|
step_widgets.append({
|
|
"widgettype": "Text",
|
|
"options": {
|
|
"text": "产线步骤尚未生成。\n任务初始化中...",
|
|
"cfontsize": 0.9, "color": "#94a3b8", "padding": "20px"
|
|
}
|
|
})
|
|
|
|
return {
|
|
"widgettype": "VBox",
|
|
"options": {"width": "100%", "padding": "4px"},
|
|
"subwidgets": step_widgets
|
|
}
|