@flowdrop/flowdrop 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/enhanced-client.d.ts +29 -16
- package/dist/api/enhanced-client.js +0 -14
- package/dist/components/PipelineStatus.svelte +9 -12
- package/dist/components/WorkflowEditor.svelte +3 -0
- package/dist/components/interrupt/ChoicePrompt.svelte +24 -5
- package/dist/components/interrupt/ConfirmationPrompt.svelte +5 -0
- package/dist/components/interrupt/InterruptBubble.svelte +12 -0
- package/dist/components/interrupt/ReviewPrompt.svelte +20 -0
- package/dist/components/interrupt/TextInputPrompt.svelte +5 -0
- package/dist/components/nodes/GatewayNode.svelte +2 -6
- package/dist/components/nodes/WorkflowNode.svelte +2 -6
- package/dist/components/playground/ChatInput.svelte +359 -0
- package/dist/components/playground/ChatInput.svelte.d.ts +14 -0
- package/dist/components/playground/ChatPanel.svelte +100 -724
- package/dist/components/playground/ChatPanel.svelte.d.ts +9 -26
- package/dist/components/playground/ControlPanel.svelte +496 -0
- package/dist/components/playground/ControlPanel.svelte.d.ts +20 -0
- package/dist/components/playground/ExecutionConsole.svelte +163 -0
- package/dist/components/playground/ExecutionConsole.svelte.d.ts +14 -0
- package/dist/components/playground/MessageStream.svelte +283 -0
- package/dist/components/playground/MessageStream.svelte.d.ts +27 -0
- package/dist/components/playground/PipelineKanbanView.svelte +284 -0
- package/dist/components/playground/PipelineKanbanView.svelte.d.ts +11 -0
- package/dist/components/playground/PipelinePanel.svelte +204 -65
- package/dist/components/playground/PipelinePanel.svelte.d.ts +3 -1
- package/dist/components/playground/PipelineTableView.svelte +376 -0
- package/dist/components/playground/PipelineTableView.svelte.d.ts +11 -0
- package/dist/components/playground/Playground.svelte +262 -1200
- package/dist/components/playground/Playground.svelte.d.ts +0 -13
- package/dist/components/playground/PlaygroundStudio.svelte +35 -61
- package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -1
- package/dist/components/playground/pipelineViewUtils.svelte.d.ts +22 -0
- package/dist/components/playground/pipelineViewUtils.svelte.js +77 -0
- package/dist/messages/defaults.d.ts +24 -0
- package/dist/messages/defaults.js +24 -0
- package/dist/playground/index.d.ts +6 -1
- package/dist/playground/index.js +6 -0
- package/dist/playground/mount.d.ts +3 -0
- package/dist/playground/mount.js +3 -2
- package/dist/stores/playgroundStore.svelte.d.ts +6 -0
- package/dist/stores/playgroundStore.svelte.js +21 -1
- package/dist/types/index.d.ts +28 -2
- package/dist/types/playground.d.ts +5 -2
- package/dist/types/playground.js +5 -7
- package/dist/utils/nodeStatus.js +15 -5
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { KanbanColumnDef } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_COLUMNS: KanbanColumnDef[] = [
|
|
5
|
+
{ key: 'pending', label: 'Pending', statuses: ['idle', 'pending'], icon: 'mdi:clock-outline', color: 'var(--fd-muted-foreground)' },
|
|
6
|
+
{ key: 'in_progress', label: 'In Progress', statuses: ['running', 'paused', 'interrupted'], icon: 'mdi:play-circle-outline', color: 'var(--fd-warning)' },
|
|
7
|
+
{ key: 'done', label: 'Done', statuses: ['completed', 'skipped'], icon: 'mdi:check-circle', color: 'var(--fd-success)' },
|
|
8
|
+
{ key: 'failed', label: 'Failed', statuses: ['failed', 'cancelled'], icon: 'mdi:alert-circle', color: 'var(--fd-error)' },
|
|
9
|
+
];
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { onMount } from 'svelte';
|
|
14
|
+
import Icon from '@iconify/svelte';
|
|
15
|
+
import { createPipelineDataFetcher, resolveStatus } from './pipelineViewUtils.svelte.js';
|
|
16
|
+
import { getStatusLabel, getStatusTextColor, getStatusBackgroundColor } from '../../utils/nodeStatus.js';
|
|
17
|
+
import type { NodeStatus } from './pipelineViewUtils.svelte.js';
|
|
18
|
+
import type { Workflow, WorkflowNode } from '../../types/index.js';
|
|
19
|
+
import type { EndpointConfig } from '../../config/endpoints.js';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
pipelineId: string;
|
|
23
|
+
workflow: Workflow;
|
|
24
|
+
endpointConfig: EndpointConfig;
|
|
25
|
+
refreshTrigger?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
|
|
29
|
+
|
|
30
|
+
const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
|
|
31
|
+
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (refreshTrigger <= 0) return;
|
|
34
|
+
const timer = setTimeout(() => fetcher.fetchData(), 300);
|
|
35
|
+
return () => clearTimeout(timer);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
interface CardItem {
|
|
39
|
+
node: WorkflowNode;
|
|
40
|
+
status: NodeStatus;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columnedNodes = $derived.by(() => {
|
|
44
|
+
const columns = fetcher.kanbanConfig ?? DEFAULT_COLUMNS;
|
|
45
|
+
|
|
46
|
+
const statusToColumn = new Map<string, string>();
|
|
47
|
+
for (const col of columns) {
|
|
48
|
+
for (const status of col.statuses) {
|
|
49
|
+
statusToColumn.set(status, col.key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const fallbackKey = columns[0]?.key ?? 'pending';
|
|
53
|
+
|
|
54
|
+
const nodesByColumn = new Map<string, CardItem[]>();
|
|
55
|
+
for (const col of columns) {
|
|
56
|
+
nodesByColumn.set(col.key, []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const node of workflow.nodes) {
|
|
60
|
+
const status = resolveStatus(fetcher.nodeStatusMap[node.id]);
|
|
61
|
+
const colKey = statusToColumn.get(status) ?? fallbackKey;
|
|
62
|
+
nodesByColumn.get(colKey)?.push({ node, status });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { columns, nodesByColumn };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
onMount(() => {
|
|
69
|
+
fetcher.fetchData();
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div class="pipeline-kanban">
|
|
74
|
+
{#if fetcher.isError}
|
|
75
|
+
<div class="pipeline-kanban__error">Could not refresh status data</div>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if fetcher.isLoading && Object.keys(fetcher.nodeStatusMap).length === 0}
|
|
78
|
+
<div class="pipeline-kanban__loading">
|
|
79
|
+
<Icon icon="mdi:loading" class="pipeline-kanban__spinner" />
|
|
80
|
+
</div>
|
|
81
|
+
{:else}
|
|
82
|
+
<div class="pipeline-kanban__board">
|
|
83
|
+
{#each columnedNodes.columns as col (col.key)}
|
|
84
|
+
{@const items = columnedNodes.nodesByColumn.get(col.key) ?? []}
|
|
85
|
+
{@const showStatusPill = col.statuses.length > 1}
|
|
86
|
+
<div
|
|
87
|
+
class="pipeline-kanban__column"
|
|
88
|
+
style="--col-color: {col.color ?? 'var(--fd-muted-foreground)'}"
|
|
89
|
+
>
|
|
90
|
+
<div class="pipeline-kanban__column-header">
|
|
91
|
+
<Icon
|
|
92
|
+
icon={col.icon ?? 'mdi:circle-outline'}
|
|
93
|
+
class="pipeline-kanban__col-icon"
|
|
94
|
+
/>
|
|
95
|
+
<span class="pipeline-kanban__col-label">{col.label}</span>
|
|
96
|
+
<span class="pipeline-kanban__col-count">{items.length}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="pipeline-kanban__cards">
|
|
99
|
+
{#each items as { node, status } (node.id)}
|
|
100
|
+
<div class="pipeline-kanban__card">
|
|
101
|
+
<div class="pipeline-kanban__card-body">
|
|
102
|
+
<div class="pipeline-kanban__card-top">
|
|
103
|
+
<span class="pipeline-kanban__card-label">{node.data.label}</span>
|
|
104
|
+
{#if showStatusPill}
|
|
105
|
+
<span
|
|
106
|
+
class="pipeline-kanban__card-status"
|
|
107
|
+
style="color: {getStatusTextColor(status)}; background-color: {getStatusBackgroundColor(status)}"
|
|
108
|
+
>{getStatusLabel(status)}</span>
|
|
109
|
+
{/if}
|
|
110
|
+
</div>
|
|
111
|
+
<span class="pipeline-kanban__card-type">{node.data.metadata.id}</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{/each}
|
|
115
|
+
{#if items.length === 0}
|
|
116
|
+
<div class="pipeline-kanban__empty">—</div>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<style>
|
|
126
|
+
.pipeline-kanban {
|
|
127
|
+
height: 100%;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
display: flex;
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
padding: var(--fd-space-sm);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.pipeline-kanban__error {
|
|
135
|
+
padding: var(--fd-space-xs) var(--fd-space-md);
|
|
136
|
+
font-size: var(--fd-text-2xs);
|
|
137
|
+
color: var(--fd-error);
|
|
138
|
+
background-color: color-mix(in srgb, var(--fd-error) 8%, transparent);
|
|
139
|
+
border-bottom: 1px solid color-mix(in srgb, var(--fd-error) 20%, transparent);
|
|
140
|
+
flex-shrink: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.pipeline-kanban__loading {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
flex: 1;
|
|
148
|
+
color: var(--fd-muted-foreground);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:global(.pipeline-kanban__spinner) {
|
|
152
|
+
font-size: 1.5rem;
|
|
153
|
+
animation: kanban-spin 1s linear infinite;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@keyframes kanban-spin {
|
|
157
|
+
to {
|
|
158
|
+
transform: rotate(360deg);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pipeline-kanban__board {
|
|
163
|
+
display: flex;
|
|
164
|
+
gap: var(--fd-space-sm);
|
|
165
|
+
height: 100%;
|
|
166
|
+
overflow-x: auto;
|
|
167
|
+
overflow-y: hidden;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.pipeline-kanban__column {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
min-width: 160px;
|
|
174
|
+
flex: 1;
|
|
175
|
+
background-color: var(--fd-background);
|
|
176
|
+
border-radius: var(--fd-radius-md);
|
|
177
|
+
border: 1px solid var(--fd-border);
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.pipeline-kanban__column-header {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: var(--fd-space-xs);
|
|
185
|
+
padding: var(--fd-space-sm) var(--fd-space-md);
|
|
186
|
+
border-bottom: 1px solid var(--fd-border);
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.pipeline-kanban__col-label {
|
|
191
|
+
font-size: var(--fd-text-xs);
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
flex: 1;
|
|
194
|
+
color: var(--fd-foreground);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.pipeline-kanban__col-count {
|
|
198
|
+
font-size: var(--fd-text-2xs);
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
padding: 1px var(--fd-space-xs);
|
|
201
|
+
border-radius: var(--fd-radius-sm);
|
|
202
|
+
background-color: var(--fd-muted);
|
|
203
|
+
color: var(--fd-muted-foreground);
|
|
204
|
+
min-width: 18px;
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
:global(.pipeline-kanban__col-icon) {
|
|
209
|
+
font-size: var(--fd-text-sm);
|
|
210
|
+
flex-shrink: 0;
|
|
211
|
+
color: var(--col-color, var(--fd-muted-foreground));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.pipeline-kanban__cards {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
gap: var(--fd-space-xs);
|
|
218
|
+
padding: var(--fd-space-xs);
|
|
219
|
+
overflow-y: auto;
|
|
220
|
+
flex: 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.pipeline-kanban__card {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: flex-start;
|
|
226
|
+
padding: var(--fd-space-sm);
|
|
227
|
+
border-radius: var(--fd-radius-sm);
|
|
228
|
+
border: 1px solid var(--fd-border);
|
|
229
|
+
border-left-width: 3px;
|
|
230
|
+
border-left-color: var(--col-color, var(--fd-border));
|
|
231
|
+
background-color: var(--fd-card);
|
|
232
|
+
font-size: var(--fd-text-xs);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.pipeline-kanban__card-body {
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
gap: 3px;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
flex: 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.pipeline-kanban__card-top {
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: var(--fd-space-xs);
|
|
247
|
+
min-width: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.pipeline-kanban__card-label {
|
|
251
|
+
font-weight: 500;
|
|
252
|
+
color: var(--fd-foreground);
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
text-overflow: ellipsis;
|
|
255
|
+
white-space: nowrap;
|
|
256
|
+
flex: 1;
|
|
257
|
+
min-width: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.pipeline-kanban__card-status {
|
|
261
|
+
display: inline-block;
|
|
262
|
+
font-size: var(--fd-text-2xs);
|
|
263
|
+
font-weight: 500;
|
|
264
|
+
padding: 1px var(--fd-space-xs);
|
|
265
|
+
border-radius: var(--fd-radius-sm);
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
flex-shrink: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.pipeline-kanban__card-type {
|
|
271
|
+
color: var(--fd-muted-foreground);
|
|
272
|
+
font-size: var(--fd-text-2xs);
|
|
273
|
+
overflow: hidden;
|
|
274
|
+
text-overflow: ellipsis;
|
|
275
|
+
white-space: nowrap;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.pipeline-kanban__empty {
|
|
279
|
+
color: var(--fd-muted-foreground);
|
|
280
|
+
font-size: var(--fd-text-xs);
|
|
281
|
+
text-align: center;
|
|
282
|
+
padding: var(--fd-space-md);
|
|
283
|
+
}
|
|
284
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Workflow } from '../../types/index.js';
|
|
2
|
+
import type { EndpointConfig } from '../../config/endpoints.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
pipelineId: string;
|
|
5
|
+
workflow: Workflow;
|
|
6
|
+
endpointConfig: EndpointConfig;
|
|
7
|
+
refreshTrigger?: number;
|
|
8
|
+
}
|
|
9
|
+
declare const PipelineKanbanView: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type PipelineKanbanView = ReturnType<typeof PipelineKanbanView>;
|
|
11
|
+
export default PipelineKanbanView;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
const VIEW_MODE_KEY = 'fd-pipeline-view-mode';
|
|
3
|
+
const BUILTIN_VIEWS = ['graph', 'kanban', 'table'] as const;
|
|
4
|
+
// `string & {}` preserves autocomplete for built-in values while still accepting arbitrary strings from extraViews.
|
|
5
|
+
type ViewMode = typeof BUILTIN_VIEWS[number] | (string & {});
|
|
6
|
+
</script>
|
|
7
|
+
|
|
1
8
|
<script lang="ts">
|
|
9
|
+
import { onMount } from 'svelte';
|
|
2
10
|
import PipelineStatus from '../PipelineStatus.svelte';
|
|
11
|
+
import PipelineKanbanView from './PipelineKanbanView.svelte';
|
|
12
|
+
import PipelineTableView from './PipelineTableView.svelte';
|
|
13
|
+
import App from '../App.svelte';
|
|
3
14
|
import Icon from '@iconify/svelte';
|
|
4
|
-
import
|
|
15
|
+
import { logger } from '../../utils/logger.js';
|
|
16
|
+
import type { Workflow, PipelineViewDef } from '../../types/index.js';
|
|
5
17
|
import type { EndpointConfig } from '../../config/endpoints.js';
|
|
6
18
|
import type { PlaygroundExecution } from '../../types/playground.js';
|
|
7
19
|
|
|
@@ -18,6 +30,8 @@
|
|
|
18
30
|
onSelectExecution?: (id: string | null) => void;
|
|
19
31
|
/** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
|
|
20
32
|
refreshTrigger?: number;
|
|
33
|
+
/** Additional views injected by the library consumer */
|
|
34
|
+
extraViews?: PipelineViewDef[];
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
let {
|
|
@@ -29,12 +43,38 @@
|
|
|
29
43
|
latestExecutionId = null,
|
|
30
44
|
onSelectExecution,
|
|
31
45
|
refreshTrigger = 0,
|
|
46
|
+
extraViews = []
|
|
32
47
|
}: Props = $props();
|
|
33
48
|
|
|
49
|
+
let viewMode = $state<ViewMode>('graph');
|
|
50
|
+
|
|
51
|
+
onMount(() => {
|
|
52
|
+
const stored = localStorage.getItem(VIEW_MODE_KEY);
|
|
53
|
+
if (!stored) return;
|
|
54
|
+
const validKeys = [...BUILTIN_VIEWS, ...extraViews.map((v) => v.key)];
|
|
55
|
+
if (validKeys.includes(stored)) viewMode = stored;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function selectViewMode(mode: ViewMode) {
|
|
59
|
+
viewMode = mode;
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(VIEW_MODE_KEY, mode);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
logger.warn('[FlowDrop] Could not persist view mode to localStorage:', e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
34
67
|
let runDropdownOpen = $state(false);
|
|
35
68
|
let chipWrapEl = $state<HTMLElement | null>(null);
|
|
69
|
+
let runChipEl = $state<HTMLElement | null>(null);
|
|
70
|
+
let runPopoverEl = $state<HTMLElement | null>(null);
|
|
71
|
+
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (runDropdownOpen && runPopoverEl) {
|
|
74
|
+
runPopoverEl.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
36
77
|
|
|
37
|
-
// Close run popover on outside click
|
|
38
78
|
$effect(() => {
|
|
39
79
|
if (!runDropdownOpen) return;
|
|
40
80
|
function handleOutside(e: MouseEvent) {
|
|
@@ -46,17 +86,17 @@
|
|
|
46
86
|
return () => document.removeEventListener('click', handleOutside);
|
|
47
87
|
});
|
|
48
88
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
89
|
+
const STATUS_ICON: Record<PlaygroundExecution['status'], string> = {
|
|
90
|
+
running: 'mdi:play-circle-outline',
|
|
91
|
+
failed: 'mdi:alert-circle',
|
|
92
|
+
completed: 'mdi:check-circle'
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const STATUS_CLASS: Record<PlaygroundExecution['status'], string> = {
|
|
96
|
+
running: 'pipeline-panel__run-status--running',
|
|
97
|
+
failed: 'pipeline-panel__run-status--failed',
|
|
98
|
+
completed: 'pipeline-panel__run-status--completed'
|
|
99
|
+
};
|
|
60
100
|
</script>
|
|
61
101
|
|
|
62
102
|
<div class="pipeline-panel">
|
|
@@ -64,27 +104,91 @@
|
|
|
64
104
|
<Icon icon="mdi:source-branch" class="pipeline-panel__icon" />
|
|
65
105
|
<span class="pipeline-panel__title">Pipeline</span>
|
|
66
106
|
|
|
107
|
+
{#if pipelineId}
|
|
108
|
+
<div class="pipeline-panel__view-toggle" role="group" aria-label="View mode">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
class="pipeline-panel__view-btn"
|
|
112
|
+
class:pipeline-panel__view-btn--active={viewMode === 'graph'}
|
|
113
|
+
onclick={() => selectViewMode('graph')}
|
|
114
|
+
title="Graph view"
|
|
115
|
+
>
|
|
116
|
+
<Icon icon="mdi:sitemap-outline" />
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
class="pipeline-panel__view-btn"
|
|
121
|
+
class:pipeline-panel__view-btn--active={viewMode === 'kanban'}
|
|
122
|
+
onclick={() => selectViewMode('kanban')}
|
|
123
|
+
title="Kanban view"
|
|
124
|
+
>
|
|
125
|
+
<Icon icon="mdi:view-column-outline" />
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
class="pipeline-panel__view-btn"
|
|
130
|
+
class:pipeline-panel__view-btn--active={viewMode === 'table'}
|
|
131
|
+
onclick={() => selectViewMode('table')}
|
|
132
|
+
title="Table view"
|
|
133
|
+
>
|
|
134
|
+
<Icon icon="mdi:table-large" />
|
|
135
|
+
</button>
|
|
136
|
+
{#each extraViews as view (view.key)}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
class="pipeline-panel__view-btn"
|
|
140
|
+
class:pipeline-panel__view-btn--active={viewMode === view.key}
|
|
141
|
+
onclick={() => selectViewMode(view.key)}
|
|
142
|
+
title={view.label}
|
|
143
|
+
>
|
|
144
|
+
<Icon icon={view.icon} />
|
|
145
|
+
</button>
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
67
150
|
{#if pipelineId && executions.length > 0}
|
|
68
|
-
<!-- Run picker chip -->
|
|
69
151
|
<div class="pipeline-panel__run-chip-wrap" bind:this={chipWrapEl}>
|
|
70
152
|
<button
|
|
71
153
|
type="button"
|
|
72
154
|
class="pipeline-panel__run-chip"
|
|
73
155
|
class:pipeline-panel__run-chip--pinned={isPinned}
|
|
74
156
|
class:pipeline-panel__run-chip--open={runDropdownOpen}
|
|
157
|
+
bind:this={runChipEl}
|
|
158
|
+
aria-haspopup="menu"
|
|
159
|
+
aria-expanded={runDropdownOpen}
|
|
75
160
|
onclick={() => (runDropdownOpen = !runDropdownOpen)}
|
|
161
|
+
onkeydown={(e) => {
|
|
162
|
+
if (e.key === 'Escape') {
|
|
163
|
+
runDropdownOpen = false;
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
76
166
|
title="Switch run"
|
|
77
167
|
>
|
|
78
|
-
<span class="pipeline-panel__run-chip-label">{pipelineId
|
|
79
|
-
<Icon
|
|
168
|
+
<span class="pipeline-panel__run-chip-label">{pipelineId}</span>
|
|
169
|
+
<Icon
|
|
170
|
+
icon={runDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'}
|
|
171
|
+
class="pipeline-panel__run-chip-chevron"
|
|
172
|
+
/>
|
|
80
173
|
</button>
|
|
81
174
|
|
|
82
175
|
{#if runDropdownOpen}
|
|
83
|
-
<div
|
|
176
|
+
<div
|
|
177
|
+
class="pipeline-panel__run-popover"
|
|
178
|
+
bind:this={runPopoverEl}
|
|
179
|
+
role="menu"
|
|
180
|
+
onkeydown={(e) => {
|
|
181
|
+
if (e.key === 'Escape') {
|
|
182
|
+
runDropdownOpen = false;
|
|
183
|
+
runChipEl?.focus();
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
84
187
|
{#each [...executions].reverse() as exec (exec.id)}
|
|
85
188
|
{@const isActive = pipelineId === exec.id}
|
|
86
189
|
<button
|
|
87
190
|
type="button"
|
|
191
|
+
role="menuitem"
|
|
88
192
|
class="pipeline-panel__run-popover-item"
|
|
89
193
|
class:pipeline-panel__run-popover-item--active={isActive}
|
|
90
194
|
onclick={() => {
|
|
@@ -93,8 +197,8 @@
|
|
|
93
197
|
}}
|
|
94
198
|
>
|
|
95
199
|
<Icon
|
|
96
|
-
icon={
|
|
97
|
-
class="pipeline-panel__run-status {
|
|
200
|
+
icon={STATUS_ICON[exec.status]}
|
|
201
|
+
class="pipeline-panel__run-status {STATUS_CLASS[exec.status]}"
|
|
98
202
|
/>
|
|
99
203
|
<span class="pipeline-panel__run-id">{exec.id}</span>
|
|
100
204
|
{#if isActive}
|
|
@@ -106,7 +210,6 @@
|
|
|
106
210
|
{/if}
|
|
107
211
|
</div>
|
|
108
212
|
|
|
109
|
-
<!-- Latest toggle -->
|
|
110
213
|
<button
|
|
111
214
|
type="button"
|
|
112
215
|
class="pipeline-panel__latest-toggle"
|
|
@@ -118,7 +221,9 @@
|
|
|
118
221
|
onSelectExecution?.(latestExecutionId);
|
|
119
222
|
}
|
|
120
223
|
}}
|
|
121
|
-
title={isPinned
|
|
224
|
+
title={isPinned
|
|
225
|
+
? 'Following latest is off — click to resume'
|
|
226
|
+
: 'Always showing the most recent run'}
|
|
122
227
|
>
|
|
123
228
|
<Icon icon="mdi:refresh" />
|
|
124
229
|
Latest
|
|
@@ -126,23 +231,48 @@
|
|
|
126
231
|
{:else if pipelineId}
|
|
127
232
|
<span
|
|
128
233
|
class="pipeline-panel__status-badge pipeline-panel__status-badge--live"
|
|
129
|
-
title="Showing the most recent execution"
|
|
130
|
-
>
|
|
234
|
+
title="Showing the most recent execution">Latest</span
|
|
235
|
+
>
|
|
131
236
|
{/if}
|
|
132
237
|
</div>
|
|
133
238
|
|
|
134
|
-
|
|
135
|
-
{#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
239
|
+
<div class="pipeline-panel__content">
|
|
240
|
+
{#if pipelineId}
|
|
241
|
+
{#key pipelineId}
|
|
242
|
+
{#if viewMode === 'kanban'}
|
|
243
|
+
<PipelineKanbanView {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
|
|
244
|
+
{:else if viewMode === 'table'}
|
|
245
|
+
<PipelineTableView {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
|
|
246
|
+
{:else if viewMode === 'graph'}
|
|
247
|
+
<PipelineStatus
|
|
248
|
+
{pipelineId}
|
|
249
|
+
{workflow}
|
|
250
|
+
{endpointConfig}
|
|
251
|
+
runLabel={pipelineId}
|
|
252
|
+
{refreshTrigger}
|
|
253
|
+
isEmbedded={true}
|
|
254
|
+
/>
|
|
255
|
+
{:else}
|
|
256
|
+
{@const activeView = extraViews.find((v) => v.key === viewMode)}
|
|
257
|
+
{#if activeView}
|
|
258
|
+
{@const View = activeView.component}
|
|
259
|
+
<View {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
|
|
260
|
+
{/if}
|
|
261
|
+
{/if}
|
|
262
|
+
{/key}
|
|
263
|
+
{:else}
|
|
264
|
+
<App
|
|
265
|
+
{workflow}
|
|
266
|
+
height="100%"
|
|
267
|
+
width="100%"
|
|
268
|
+
showNavbar={false}
|
|
269
|
+
disableSidebar={true}
|
|
270
|
+
lockWorkflow={true}
|
|
271
|
+
readOnly={true}
|
|
272
|
+
{endpointConfig}
|
|
273
|
+
/>
|
|
274
|
+
{/if}
|
|
275
|
+
</div>
|
|
146
276
|
</div>
|
|
147
277
|
|
|
148
278
|
<style>
|
|
@@ -194,6 +324,45 @@
|
|
|
194
324
|
color: var(--fd-success);
|
|
195
325
|
}
|
|
196
326
|
|
|
327
|
+
/* View mode toggle */
|
|
328
|
+
.pipeline-panel__view-toggle {
|
|
329
|
+
display: inline-flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: 2px;
|
|
332
|
+
padding: 2px;
|
|
333
|
+
border: 1px solid var(--fd-border);
|
|
334
|
+
border-radius: var(--fd-radius-md);
|
|
335
|
+
background-color: var(--fd-muted);
|
|
336
|
+
flex-shrink: 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.pipeline-panel__view-btn {
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
width: 24px;
|
|
344
|
+
height: 24px;
|
|
345
|
+
border: none;
|
|
346
|
+
border-radius: calc(var(--fd-radius-md) - 2px);
|
|
347
|
+
background: transparent;
|
|
348
|
+
color: var(--fd-muted-foreground);
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
transition: all var(--fd-transition-fast);
|
|
351
|
+
font-size: var(--fd-text-sm);
|
|
352
|
+
line-height: 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.pipeline-panel__view-btn:hover {
|
|
356
|
+
background-color: var(--fd-background);
|
|
357
|
+
color: var(--fd-foreground);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.pipeline-panel__view-btn--active {
|
|
361
|
+
background-color: var(--fd-background);
|
|
362
|
+
color: var(--fd-primary);
|
|
363
|
+
box-shadow: var(--fd-shadow-sm);
|
|
364
|
+
}
|
|
365
|
+
|
|
197
366
|
/* Run picker chip */
|
|
198
367
|
.pipeline-panel__run-chip-wrap {
|
|
199
368
|
position: relative;
|
|
@@ -300,7 +469,6 @@
|
|
|
300
469
|
|
|
301
470
|
:global(.pipeline-panel__run-status--running) {
|
|
302
471
|
color: var(--fd-warning);
|
|
303
|
-
animation: pp-spin 1s linear infinite;
|
|
304
472
|
}
|
|
305
473
|
|
|
306
474
|
:global(.pipeline-panel__run-status--completed) {
|
|
@@ -311,11 +479,6 @@
|
|
|
311
479
|
color: var(--fd-error);
|
|
312
480
|
}
|
|
313
481
|
|
|
314
|
-
@keyframes pp-spin {
|
|
315
|
-
from { transform: rotate(0deg); }
|
|
316
|
-
to { transform: rotate(360deg); }
|
|
317
|
-
}
|
|
318
|
-
|
|
319
482
|
/* Latest toggle */
|
|
320
483
|
.pipeline-panel__latest-toggle {
|
|
321
484
|
display: inline-flex;
|
|
@@ -355,28 +518,4 @@
|
|
|
355
518
|
min-height: 0;
|
|
356
519
|
overflow: hidden;
|
|
357
520
|
}
|
|
358
|
-
|
|
359
|
-
.pipeline-panel__empty {
|
|
360
|
-
flex: 1;
|
|
361
|
-
display: flex;
|
|
362
|
-
flex-direction: column;
|
|
363
|
-
align-items: center;
|
|
364
|
-
justify-content: center;
|
|
365
|
-
gap: var(--fd-space-md);
|
|
366
|
-
color: var(--fd-muted-foreground);
|
|
367
|
-
padding: var(--fd-space-4xl);
|
|
368
|
-
text-align: center;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
:global(.pipeline-panel__empty-icon) {
|
|
372
|
-
font-size: var(--fd-space-6xl);
|
|
373
|
-
opacity: 0.4;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.pipeline-panel__empty-text {
|
|
377
|
-
font-size: var(--fd-text-sm);
|
|
378
|
-
margin: 0;
|
|
379
|
-
max-width: 200px;
|
|
380
|
-
line-height: var(--fd-leading-relaxed);
|
|
381
|
-
}
|
|
382
521
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Workflow } from '../../types/index.js';
|
|
1
|
+
import type { Workflow, PipelineViewDef } from '../../types/index.js';
|
|
2
2
|
import type { EndpointConfig } from '../../config/endpoints.js';
|
|
3
3
|
import type { PlaygroundExecution } from '../../types/playground.js';
|
|
4
4
|
interface Props {
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
onSelectExecution?: (id: string | null) => void;
|
|
15
15
|
/** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
|
|
16
16
|
refreshTrigger?: number;
|
|
17
|
+
/** Additional views injected by the library consumer */
|
|
18
|
+
extraViews?: PipelineViewDef[];
|
|
17
19
|
}
|
|
18
20
|
declare const PipelinePanel: import("svelte").Component<Props, {}, "">;
|
|
19
21
|
type PipelinePanel = ReturnType<typeof PipelinePanel>;
|