# 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": '', "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 }