look and feel modified in node outputs
This commit is contained in:
parent
3bf669180d
commit
a2a4f699c7
|
|
@ -78,4 +78,13 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (initialTypesLoading()) {
|
||||
<div class="rete-editor-loading">
|
||||
<div class="rete-editor-loading-card">
|
||||
<span class="rete-editor-spinner" aria-hidden="true"></span>
|
||||
<span>Loading node types...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
private viewReady = false;
|
||||
private loadVersion = 0;
|
||||
private suppressDirtyEvents = false;
|
||||
private typesLoadingPromise: Promise<void> | null = null;
|
||||
creatingEmptyBlock = false;
|
||||
creatingEmptyBlockType = '';
|
||||
initialTypesLoading = signal(false);
|
||||
editorMode = signal<'standard' | 'select'>('standard');
|
||||
selectionBox = signal<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
private selectionPointerId: number | null = null;
|
||||
|
|
@ -222,6 +224,8 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
const host = this.container?.nativeElement as HTMLElement | undefined;
|
||||
if (!host) return;
|
||||
|
||||
await this.ensureNodeTypesLoaded();
|
||||
|
||||
const currentVersion = ++this.loadVersion;
|
||||
this.editorMode.set('standard');
|
||||
this.selectionPointerId = null;
|
||||
|
|
@ -493,4 +497,30 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
|
||||
await area.zoom(nextZoom, 0, 0);
|
||||
}
|
||||
|
||||
private async ensureNodeTypesLoaded() {
|
||||
if (this.blocksService.hasLoadedBlockTypes() && this.containersService.hasLoadedContainerTypes()) {
|
||||
this.initialTypesLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.typesLoadingPromise) {
|
||||
this.initialTypesLoading.set(true);
|
||||
await this.typesLoadingPromise;
|
||||
this.initialTypesLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialTypesLoading.set(true);
|
||||
this.typesLoadingPromise = Promise.all([
|
||||
this.blocksService.getAllBlocksTypes(),
|
||||
this.containersService.getAllContainerTypes()
|
||||
]).then(() => undefined)
|
||||
.finally(() => {
|
||||
this.typesLoadingPromise = null;
|
||||
this.initialTypesLoading.set(false);
|
||||
});
|
||||
|
||||
await this.typesLoadingPromise;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,36 +90,51 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.execution-output-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 52px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
|
||||
color: #1d4ed8;
|
||||
line-height: 1.2;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
.execution-output-group {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.execution-output-title {
|
||||
.execution-output-group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.execution-output-subtitle {
|
||||
margin-top: 2px;
|
||||
.execution-output-group-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.execution-output-item {
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.execution-output-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.execution-output-item-name {
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.execution-output-preview {
|
||||
margin-top: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #0f172a;
|
||||
|
|
@ -127,6 +142,28 @@
|
|||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.execution-output-view-btn {
|
||||
flex: 0 0 auto;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.execution-output-view-btn:hover {
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.execution-output-view-btn i {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.execution-log-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
|
|
|
|||
|
|
@ -190,25 +190,31 @@
|
|||
</div>
|
||||
} @else {
|
||||
<div class="p-3 space-y-3">
|
||||
@if (!executionOutputs().length) {
|
||||
@if (!executionOutputGroups().length) {
|
||||
<div class="text-xs text-slate-500">No execution output available.</div>
|
||||
} @else {
|
||||
@for (output of executionOutputs(); track output.key) {
|
||||
<div class="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="execution-output-badge">
|
||||
<div class="execution-output-title">{{ output.title }}</div>
|
||||
<div class="execution-output-subtitle">{{ output.subtitle }}</div>
|
||||
@for (group of executionOutputGroups(); track group.nodeTitle) {
|
||||
<div class="execution-output-group">
|
||||
<div class="execution-output-group-title">{{ group.nodeTitle }}</div>
|
||||
<div class="execution-output-group-list">
|
||||
@for (output of group.outputs; track output.key) {
|
||||
<div class="execution-output-item">
|
||||
<div class="execution-output-item-head">
|
||||
<div class="execution-output-item-name">{{ output.outputName }}</div>
|
||||
@if (output.isLong) {
|
||||
<button
|
||||
type="button"
|
||||
class="execution-output-view-btn"
|
||||
aria-label="View full output"
|
||||
(click)="openOutputPreview(output, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="execution-output-preview">{{ output.preview }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="execution-output-preview mt-3">{{ output.preview }}</div>
|
||||
@if (output.isLong) {
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
class="mt-3"
|
||||
(click)="openOutputPreview(output, $event)">
|
||||
View full output
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -225,8 +231,8 @@
|
|||
<div class="relative z-10 flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl">
|
||||
<div class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-lg font-semibold text-slate-900">{{ outputModal.title }}</h3>
|
||||
<p class="mt-1 break-all text-xs text-slate-500">{{ outputModal.subtitle }}</p>
|
||||
<h3 class="text-lg font-semibold text-slate-900">{{ outputPreviewTitle(outputModal) }}</h3>
|
||||
<p class="mt-1 break-all text-xs text-slate-500">{{ outputPreviewSubtitle(outputModal) }}</p>
|
||||
</div>
|
||||
<button type="button" mat-stroked-button (click)="closeOutputPreview($event)">Close</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,13 +37,18 @@ import { firstValueFrom } from 'rxjs';
|
|||
|
||||
type ExecutionOutputEntry = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
nodeTitle: string;
|
||||
outputName: string;
|
||||
value: string;
|
||||
preview: string;
|
||||
isLong: boolean;
|
||||
};
|
||||
|
||||
type ExecutionOutputGroup = {
|
||||
nodeTitle: string;
|
||||
outputs: ExecutionOutputEntry[];
|
||||
};
|
||||
|
||||
type ExecutionLogEntryView = ExecutionEventLogEntry & {
|
||||
messageText: string;
|
||||
levelText: string;
|
||||
|
|
@ -58,6 +63,7 @@ type ExecutionLogEntryView = ExecutionEventLogEntry & {
|
|||
export class TaskExecutionViewerComponent implements OnDestroy {
|
||||
private static readonly TEXT_INPUT_DEBOUNCE_MS = 1200;
|
||||
private static readonly EVENTS_POLL_INTERVAL_MS = 5000;
|
||||
private static readonly OUTPUT_PREVIEW_LIMIT = 80;
|
||||
private taskExecutionsService = inject(TaskExecutionsService);
|
||||
private humanInteractionDialog = inject(HumanInteractionDialogService);
|
||||
private settingsDialog = inject(NodeSettingsDialogService);
|
||||
|
|
@ -302,18 +308,36 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
return Object.entries(resultMap)
|
||||
.map(([key, rawValue]) => {
|
||||
const value = this.stringifyOutputValue(rawValue);
|
||||
const isLong = value.length > 160;
|
||||
const isLong = value.length > TaskExecutionViewerComponent.OUTPUT_PREVIEW_LIMIT;
|
||||
const label = this.formatExecutionOutputLabel(key, steps);
|
||||
return {
|
||||
key,
|
||||
title: label.title,
|
||||
subtitle: label.subtitle,
|
||||
nodeTitle: label.nodeTitle,
|
||||
outputName: label.outputName,
|
||||
value,
|
||||
preview: isLong ? `${value.slice(0, 160)}...` : value,
|
||||
preview: isLong ? `${value.slice(0, TaskExecutionViewerComponent.OUTPUT_PREVIEW_LIMIT)}...` : value,
|
||||
isLong
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.title.localeCompare(b.title) || a.subtitle.localeCompare(b.subtitle));
|
||||
.sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle) || a.outputName.localeCompare(b.outputName));
|
||||
});
|
||||
|
||||
readonly executionOutputGroups = computed<ExecutionOutputGroup[]>(() => {
|
||||
const groups = new Map<string, ExecutionOutputEntry[]>();
|
||||
|
||||
for (const output of this.executionOutputs()) {
|
||||
if (!groups.has(output.nodeTitle)) {
|
||||
groups.set(output.nodeTitle, []);
|
||||
}
|
||||
groups.get(output.nodeTitle)!.push(output);
|
||||
}
|
||||
|
||||
return Array.from(groups.entries())
|
||||
.map(([nodeTitle, outputs]) => ({
|
||||
nodeTitle,
|
||||
outputs
|
||||
}))
|
||||
.sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle));
|
||||
});
|
||||
|
||||
readonly inputsReadOnly = computed(() => {
|
||||
|
|
@ -440,6 +464,16 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
this.outputPreviewModal.set(output);
|
||||
}
|
||||
|
||||
outputPreviewTitle(output: ExecutionOutputEntry | null): string {
|
||||
if (!output) return 'Output';
|
||||
return output.nodeTitle;
|
||||
}
|
||||
|
||||
outputPreviewSubtitle(output: ExecutionOutputEntry | null): string {
|
||||
if (!output) return '';
|
||||
return output.outputName;
|
||||
}
|
||||
|
||||
closeOutputPreview(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
|
@ -800,10 +834,10 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
private formatExecutionOutputLabel(
|
||||
key: string,
|
||||
steps: Record<string, TaskExecutionStep>
|
||||
): { title: string; subtitle: string } {
|
||||
): { nodeTitle: string; outputName: string } {
|
||||
const separatorIndex = key.indexOf(':');
|
||||
if (separatorIndex < 0) {
|
||||
return { title: key, subtitle: 'Execution output' };
|
||||
return { nodeTitle: 'Execution output', outputName: key };
|
||||
}
|
||||
|
||||
const nodeId = key.slice(0, separatorIndex);
|
||||
|
|
@ -812,8 +846,8 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
const nodeName = this.stepTitle(step).trim();
|
||||
|
||||
return {
|
||||
title: nodeName || key,
|
||||
subtitle: outputName || 'Execution output'
|
||||
nodeTitle: nodeName || key,
|
||||
outputName: outputName || 'Execution output'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue