feat(execution): refine tree and subflow layouts

This commit is contained in:
Lucio Lelii 2026-08-03 17:21:52 +02:00
parent 14f78b79ce
commit 4d92cbc51c
11 changed files with 149 additions and 36 deletions

View File

@ -16,13 +16,58 @@
}
.tasks-executor-rail-list {
flex: 1 1 auto;
flex: 1 1 0;
min-height: 0;
}
.tasks-executor-rail-tree {
.tasks-executor-tree-panel {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1 1 0;
min-height: 0;
}
.tasks-executor-tree-panel-collapsed {
flex: 0 0 auto;
max-height: 45%;
gap: 0;
}
.tasks-executor-tree-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 12px;
border: 1px solid #dbe2ea;
border-radius: 10px;
background: #ffffff;
color: #334155;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
}
.tasks-executor-tree-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 14px;
line-height: 1;
transform: rotate(180deg);
transition: transform 0.15s ease;
}
.tasks-executor-tree-toggle-icon-open {
transform: rotate(0deg);
}
.tasks-executor-rail-tree {
flex: 1 1 auto;
min-height: 0;
}
.interactive-subflow-switcher {

View File

@ -10,12 +10,25 @@
</app-tasks-executions-list>
@if (selectedExecution(); as run) {
<app-execution-tree
class="tasks-executor-rail-tree"
[rootExecution]="run"
[selectedExecutionId]="displayedExecution()?.id ?? null"
(executionSelected)="selectTreeExecution($event)">
</app-execution-tree>
<section class="tasks-executor-tree-panel" [class.tasks-executor-tree-panel-collapsed]="!executionTreeOpen()">
<button
type="button"
class="tasks-executor-tree-toggle"
[attr.aria-expanded]="executionTreeOpen()"
(click)="toggleExecutionTree()">
<span>Execution tree</span>
<span class="tasks-executor-tree-toggle-icon" [class.tasks-executor-tree-toggle-icon-open]="executionTreeOpen()">⌃</span>
</button>
@if (executionTreeOpen()) {
<app-execution-tree
class="tasks-executor-rail-tree"
[rootExecution]="run"
[selectedExecutionId]="displayedExecution()?.id ?? null"
(executionSelected)="selectTreeExecution($event)">
</app-execution-tree>
}
</section>
}
</div>

View File

@ -105,6 +105,16 @@ describe('TasksExecutor', () => {
expect(component).toBeTruthy();
});
it('toggles the execution tree panel', () => {
expect(component.executionTreeOpen()).toBe(true);
component.toggleExecutionTree();
expect(component.executionTreeOpen()).toBe(false);
component.toggleExecutionTree();
expect(component.executionTreeOpen()).toBe(true);
});
it('follows only container steps waiting for a child subflow', () => {
const execution = {
id: 'parent-1',

View File

@ -91,6 +91,7 @@ export class TasksExecutor {
readonly selectedExecutionId = signal<string | null>(null);
readonly requestedExecutionId = signal<string | null>(null);
readonly executionTreeOpen = signal(true);
readonly selectedExecution = computed<TaskExecution | null>(() => {
const selectedId = this.selectedExecutionId();
@ -240,6 +241,10 @@ export class TasksExecutor {
this.manualTreeSelectionId.set(selection.executionId);
}
toggleExecutionTree() {
this.executionTreeOpen.update((open) => !open);
}
private resolveExecutionById(executionId: string | null): TaskExecution | null {
if (!executionId) return null;
return this.executionDetails().find((execution) => execution.id === executionId)

View File

@ -1,5 +1,7 @@
:host {
display: block;
display: flex;
flex: 1 1 auto;
min-height: 0;
}
:host(.execution-tree-empty) {
@ -8,7 +10,9 @@
.execution-tree-shell {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
max-height: 100%;
overflow: hidden;
background: #ffffff;
@ -16,16 +20,6 @@
border-radius: 8px;
}
.execution-tree-title {
padding: 10px 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
border-bottom: 1px solid #e2e8f0;
}
.execution-tree-body {
flex: 1;
overflow-y: auto;

View File

@ -1,7 +1,6 @@
@if (rootExecution(); as root) {
@if (hasContainerContent()) {
<div class="execution-tree-shell">
<div class="execution-tree-title">Execution tree</div>
<div class="execution-tree-body">
<ng-container *ngTemplateOutlet="execNode; context: { $implicit: root, depth: 0 }"></ng-container>
</div>

View File

@ -460,16 +460,17 @@ button.llm-node-bias-summary:focus-visible {
.llm-pill-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.15;
}
.llm-pill-context {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
font-size: 9px;
line-height: 1;
font-weight: 700;

View File

@ -150,17 +150,22 @@
}
.execution-aside-tab {
flex: 0 0 auto;
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
color: #64748b;
font-size: 12px;
font-size: 11px;
font-weight: 700;
padding: 9px 9px;
padding: 9px 6px;
cursor: pointer;
white-space: nowrap;
}
.execution-aside-tabs {
flex-wrap: nowrap;
}
.execution-aside-tab-active {
border-bottom-color: #2563eb;
color: #1d4ed8;

View File

@ -160,8 +160,8 @@
</button>
@if (contextAsideOpen()) {
<aside class="w-90 border border-slate-200 rounded-md bg-white min-h-0 overflow-hidden flex flex-col">
<div class="flex border-b border-slate-200 bg-slate-50 px-2 pt-2">
<aside class="w-[22.5rem] shrink-0 border border-slate-200 rounded-md bg-white min-h-0 overflow-hidden flex flex-col">
<div class="execution-aside-tabs flex border-b border-slate-200 bg-slate-50 px-2 pt-2">
<button
type="button"
class="execution-aside-tab"
@ -196,7 +196,7 @@
class="execution-aside-tab"
[class.execution-aside-tab-active]="activeAsideTab() === 'bias-reports'"
(click)="selectAsideTab('bias-reports')">
Bias impact reports
Bias impact
</button>
</div>

View File

@ -105,11 +105,8 @@
</div>
</div>
@if (flow()) {
@if (flow() && !editingSubflow()) {
<div class="title-toolbar-meta">
@if (editingSubflow()) {
<span><span class="title-toolbar-meta-label">Main flow:</span> {{ flow()!.name }}</span>
}
<span><span class="title-toolbar-meta-label">Author:</span> {{ flow()!.author }}</span>
<span><span class="title-toolbar-meta-label">Created:</span> {{ flow()!.createdAt | date:'dd/MM/yyyy HH:mm' }}</span>
<span><span class="title-toolbar-meta-label">Updated:</span> {{ flow()!.updatedAt | date:'dd/MM/yyyy HH:mm' }}</span>

View File

@ -38,6 +38,50 @@ describe('TitleToolbar', () => {
expect(component).toBeTruthy();
});
it('hides parent metadata and global inputs while editing a subflow', async () => {
editorState.currentFlow.set({
...makeFlow(),
description: 'Main flow description',
data: {
blocks: [],
containers: [{
id: 'container-1',
name: 'Container',
typeName: 'LoopContainer',
nodeFamily: 'container',
inputs: [],
outputs: [],
specificConfiguration: {
subFlow: {
blocks: [],
containers: [],
connections: [],
dependencies: [],
globalInputs: [{ name: 'topic', type: 'TEXT', multiple: false }],
lanes: []
}
}
}],
connections: [],
dependencies: [],
globalInputs: [{ name: 'rootInput', type: 'TEXT', multiple: false }],
lanes: []
}
});
editorState.openSubflow([{ containerId: 'container-1', configurationPath: 'subFlow' }]);
fixture.detectChanges();
await fixture.whenStable();
const host = fixture.nativeElement as HTMLElement;
expect(host.textContent).not.toContain('Description:');
expect(host.textContent).not.toContain('Global Inputs');
expect(host.textContent).not.toContain('Author:');
expect(host.textContent).not.toContain('Created:');
expect(host.textContent).not.toContain('Updated:');
expect(host.textContent).not.toContain('Main flow:');
});
describe('lanes', () => {
beforeEach(() => {
editorState.currentFlow.set(makeFlow());