pipeline-sdlc/wwwroot/api/cockpit_timeline.dspy

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
}