@proofhound/web-ui 0.1.8 → 0.1.9
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/hooks/annotation.d.ts +37 -16
- package/dist/hooks/annotation.d.ts.map +1 -1
- package/dist/hooks/canary-release.d.ts +62 -37
- package/dist/hooks/canary-release.d.ts.map +1 -1
- package/dist/hooks/dataset.d.ts +101 -0
- package/dist/hooks/dataset.d.ts.map +1 -1
- package/dist/hooks/dataset.js +27 -0
- package/dist/hooks/dataset.js.map +1 -1
- package/dist/hooks/optimization.d.ts +1 -1
- package/dist/hooks/production-release.d.ts +8 -4
- package/dist/hooks/production-release.d.ts.map +1 -1
- package/dist/hooks/prompt.d.ts +149 -38
- package/dist/hooks/prompt.d.ts.map +1 -1
- package/dist/hooks/prompt.js +20 -0
- package/dist/hooks/prompt.js.map +1 -1
- package/dist/hooks/release-line.d.ts +2522 -72
- package/dist/hooks/release-line.d.ts.map +1 -1
- package/dist/hooks/release-line.js +125 -0
- package/dist/hooks/release-line.js.map +1 -1
- package/dist/hooks/run-result.d.ts +9 -6
- package/dist/hooks/run-result.d.ts.map +1 -1
- package/dist/hooks/run-result.js +2 -1
- package/dist/hooks/run-result.js.map +1 -1
- package/dist/i18n/index.d.ts +652 -160
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +652 -160
- package/dist/i18n/index.js.map +1 -1
- package/dist/lib/releases/release-line-model.d.ts +8 -2
- package/dist/lib/releases/release-line-model.d.ts.map +1 -1
- package/dist/lib/releases/release-line-model.js +66 -29
- package/dist/lib/releases/release-line-model.js.map +1 -1
- package/dist/screens/annotations/annotation-detail-page.js +1 -1
- package/dist/screens/annotations/annotation-new-page.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-new-page.js +213 -49
- package/dist/screens/annotations/annotation-new-page.js.map +1 -1
- package/dist/screens/annotations/annotation-task-model.d.ts +3 -2
- package/dist/screens/annotations/annotation-task-model.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-task-model.js +5 -4
- package/dist/screens/annotations/annotation-task-model.js.map +1 -1
- package/dist/screens/annotations/annotation-ui.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-ui.js +9 -4
- package/dist/screens/annotations/annotation-ui.js.map +1 -1
- package/dist/screens/annotations/annotations-list-page.js +1 -1
- package/dist/screens/connectors/connector-detail-page.js +2 -2
- package/dist/screens/connectors/connector-detail-page.js.map +1 -1
- package/dist/screens/connectors/connector-form-page.js +1 -1
- package/dist/screens/connectors/connector-form-page.js.map +1 -1
- package/dist/screens/connectors/connector-ui.d.ts +6 -0
- package/dist/screens/connectors/connector-ui.d.ts.map +1 -1
- package/dist/screens/connectors/connector-ui.js +7 -1
- package/dist/screens/connectors/connector-ui.js.map +1 -1
- package/dist/screens/connectors/connectors-list-page.d.ts.map +1 -1
- package/dist/screens/connectors/connectors-list-page.js +5 -5
- package/dist/screens/connectors/connectors-list-page.js.map +1 -1
- package/dist/screens/dashboard/dashboard-screen.d.ts.map +1 -1
- package/dist/screens/dashboard/dashboard-screen.js +27 -15
- package/dist/screens/dashboard/dashboard-screen.js.map +1 -1
- package/dist/screens/datasets/dataset-mappers.js +1 -1
- package/dist/screens/datasets/dataset-mappers.js.map +1 -1
- package/dist/screens/datasets/dataset-types.d.ts +1 -1
- package/dist/screens/datasets/dataset-types.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-ui.d.ts +1 -1
- package/dist/screens/datasets/dataset-ui.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-ui.js +2 -2
- package/dist/screens/datasets/dataset-ui.js.map +1 -1
- package/dist/screens/datasets/datasets-list-page.d.ts.map +1 -1
- package/dist/screens/datasets/datasets-list-page.js +35 -24
- package/dist/screens/datasets/datasets-list-page.js.map +1 -1
- package/dist/screens/experiments/experiment-detail-page.js +1 -1
- package/dist/screens/experiments/experiment-detail-page.js.map +1 -1
- package/dist/screens/experiments/run-result-labels.d.ts.map +1 -1
- package/dist/screens/experiments/run-result-labels.js +3 -4
- package/dist/screens/experiments/run-result-labels.js.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.d.ts.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.js +7 -8
- package/dist/screens/prompts/prompt-detail-page.js.map +1 -1
- package/dist/screens/prompts/prompt-model.d.ts +5 -2
- package/dist/screens/prompts/prompt-model.d.ts.map +1 -1
- package/dist/screens/prompts/prompt-model.js +3 -1
- package/dist/screens/prompts/prompt-model.js.map +1 -1
- package/dist/screens/prompts/prompts-list-page.d.ts.map +1 -1
- package/dist/screens/prompts/prompts-list-page.js +44 -20
- package/dist/screens/prompts/prompts-list-page.js.map +1 -1
- package/dist/screens/releases/release-input-route-editor.d.ts +39 -0
- package/dist/screens/releases/release-input-route-editor.d.ts.map +1 -0
- package/dist/screens/releases/release-input-route-editor.js +355 -0
- package/dist/screens/releases/release-input-route-editor.js.map +1 -0
- package/dist/screens/releases/release-line-detail-page.d.ts +62 -0
- package/dist/screens/releases/release-line-detail-page.d.ts.map +1 -1
- package/dist/screens/releases/release-line-detail-page.js +1877 -323
- package/dist/screens/releases/release-line-detail-page.js.map +1 -1
- package/dist/screens/releases/release-line-ui.d.ts.map +1 -1
- package/dist/screens/releases/release-line-ui.js +55 -39
- package/dist/screens/releases/release-line-ui.js.map +1 -1
- package/dist/screens/releases/release-new-model.d.ts.map +1 -1
- package/dist/screens/releases/release-new-model.js +1 -6
- package/dist/screens/releases/release-new-model.js.map +1 -1
- package/dist/screens/releases/release-new-page.d.ts.map +1 -1
- package/dist/screens/releases/release-new-page.js +101 -66
- package/dist/screens/releases/release-new-page.js.map +1 -1
- package/dist/screens/releases/release-topology-canvas.d.ts +11 -2
- package/dist/screens/releases/release-topology-canvas.d.ts.map +1 -1
- package/dist/screens/releases/release-topology-canvas.js +1015 -174
- package/dist/screens/releases/release-topology-canvas.js.map +1 -1
- package/dist/screens/releases/releases-list-page.d.ts.map +1 -1
- package/dist/screens/releases/releases-list-page.js +81 -32
- package/dist/screens/releases/releases-list-page.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Background, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, Position, ReactFlow, getSmoothStepPath, } from '@xyflow/react';
|
|
6
|
+
import { Cable, Check, ChevronDown, GitCompareArrows, Plus, RadioTower, Rocket, Save, Search, Split, Square, X, } from 'lucide-react';
|
|
7
|
+
import { canaryReleaseFilterRulesSchema, deriveClassificationOptionsFromPromptOutputSchema } from '@proofhound/shared';
|
|
8
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, cn, } from '@proofhound/ui';
|
|
7
9
|
import { useDateTimeFormatter } from '../../hooks';
|
|
8
10
|
import { useI18n } from '../../i18n';
|
|
9
11
|
import { getApiErrorMessage } from '../../lib';
|
|
12
|
+
import { FieldMappingTable, FilterRulesBuilder, canaryInputRouteMappingFromRecord, collectFilterRuleFields, extractInputFieldOptionsFromSnapshot, extractPromptVariablesFromSnapshot, inputRouteMappingRecord, mergeInputFieldOptions, } from './release-input-route-editor';
|
|
10
13
|
import { ReleasePill, formatPercent } from './release-line-ui';
|
|
14
|
+
const EMPTY_RECORD = {};
|
|
11
15
|
const TONE_STYLES = {
|
|
12
16
|
neutral: {
|
|
13
17
|
bg: 'var(--card)',
|
|
@@ -44,19 +48,48 @@ const NODE_ICONS = {
|
|
|
44
48
|
upstream: RadioTower,
|
|
45
49
|
router: Split,
|
|
46
50
|
production: Rocket,
|
|
47
|
-
canary:
|
|
51
|
+
canary: Split,
|
|
52
|
+
canarySplit: Split,
|
|
53
|
+
canaryDualRun: GitCompareArrows,
|
|
48
54
|
downstream: Cable,
|
|
49
55
|
addCanary: Plus,
|
|
50
56
|
};
|
|
57
|
+
function canaryTopologyIcon(canary) {
|
|
58
|
+
return canary?.trafficMode === 'dual_run' ? 'canaryDualRun' : 'canarySplit';
|
|
59
|
+
}
|
|
60
|
+
function EdgeLaneBadge({ label, items, text }) {
|
|
61
|
+
return (_jsx(TooltipProvider, { delayDuration: 140, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("span", { className: "flex h-6 items-center gap-1 rounded-md border bg-card px-1.5 font-mono text-[10.5px] font-semibold text-muted-foreground shadow-sm", "aria-label": label, title: label, children: [items.map(({ icon: Icon, tone }, index) => (_jsx(Icon, { className: "size-3.5", style: { color: TONE_STYLES[tone].dot }, "aria-hidden": "true" }, `${tone}-${index}`))), text ? _jsx("span", { className: "leading-none", children: text }) : null] }) }), _jsx(TooltipContent, { side: "top", children: label })] }) }));
|
|
62
|
+
}
|
|
63
|
+
function EdgeTextBadge({ label, title = label }) {
|
|
64
|
+
return (_jsx("span", { className: "flex h-5 items-center rounded-md border bg-card px-1.5 font-mono text-[10.5px] font-semibold leading-none text-muted-foreground shadow-sm", "aria-label": title, title: title, children: label }));
|
|
65
|
+
}
|
|
51
66
|
function ReleaseTopologyNodeCard({ data, selected }) {
|
|
52
67
|
const token = TONE_STYLES[data.tone];
|
|
53
68
|
const Icon = NODE_ICONS[data.icon];
|
|
54
69
|
const borderColor = data.mutedBorder ? 'var(--border)' : token.bd;
|
|
55
|
-
return (_jsxs("div", { className: cn('relative h-[116px] w-[236px] cursor-pointer rounded-lg border px-3 py-3 shadow-sm transition', data.tone === 'muted' && 'border-dashed opacity-80', data.action === 'addCanary' && 'hover:border-[var(--status-canary-bd)] hover:opacity-100', selected && 'ring-2 ring-primary/25'), style: { background: token.bg, borderColor }, children: [_jsx(Handle, { type: "target", position: Position.Left, className: "!size-2 !border !bg-card !opacity-0", style: { borderColor: token.bd } }), _jsx(Handle, { type: "source", position: Position.Right, className: "!size-2 !border !bg-card !opacity-0", style: { borderColor: token.bd } }), _jsxs("div", { className: "flex items-start gap-2.5", children: [_jsx("span", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border", style: { background: 'var(--background)', borderColor: token.bd, color: token.dot }, children: _jsx(Icon, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-[11.5px] font-medium text-muted-foreground", children: data.label }), data.badges?.[0] ? (_jsx(ReleasePill, { tone: data.tone === 'production' ? 'production' : data.tone === 'canary' ? 'canary' : 'neutral', className: "max-w-[92px] shrink-0 truncate", children: data.badges[0] })) : null] }), _jsx("div", { className: "mt-1 truncate font-mono text-[13px] font-semibold", title: data.title, children: data.title }), data.meta ? (_jsx("div", { className: "mt-1 truncate font-mono text-[11.5px] text-muted-foreground", title: data.meta, children: data.meta })) : null, data.detail ? (_jsx("div", { className: "mt-1.5 truncate text-[11.5px] font-medium", style: { color: token.fg }, title: data.detail, children: data.detail })) : null] })] })] }));
|
|
70
|
+
return (_jsxs("div", { className: cn('relative h-[116px] w-[236px] cursor-pointer rounded-lg border px-3 py-3 shadow-sm transition', data.tone === 'muted' && 'border-dashed opacity-80', data.action === 'addCanary' && 'hover:border-[var(--status-canary-bd)] hover:opacity-100', selected && 'ring-2 ring-primary/25'), style: { background: token.bg, borderColor }, children: [_jsx(Handle, { type: "target", position: Position.Left, className: "!size-2 !border !bg-card !opacity-0", style: { borderColor: token.bd } }), _jsx(Handle, { type: "source", position: Position.Right, className: "!size-2 !border !bg-card !opacity-0", style: { borderColor: token.bd } }), data.compact ? (_jsxs("div", { className: "flex h-full items-center justify-center gap-2.5", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-md border", style: { background: 'var(--background)', borderColor: token.bd, color: token.dot }, children: _jsx(Icon, { className: "size-4" }) }), _jsx("span", { className: "truncate text-[13px] font-semibold", children: data.label })] })) : (_jsxs("div", { className: "flex items-start gap-2.5", children: [_jsx("span", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border", style: { background: 'var(--background)', borderColor: token.bd, color: token.dot }, children: _jsx(Icon, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-[11.5px] font-medium text-muted-foreground", children: data.label }), data.badges?.[0] ? (_jsx(ReleasePill, { tone: data.tone === 'production' ? 'production' : data.tone === 'canary' ? 'canary' : 'neutral', className: "max-w-[92px] shrink-0 truncate", children: data.badges[0] })) : null] }), _jsx("div", { className: "mt-1 truncate font-mono text-[13px] font-semibold", title: data.title, children: data.title }), data.meta ? (_jsx("div", { className: "mt-1 truncate font-mono text-[11.5px] text-muted-foreground", title: data.meta, children: data.meta })) : null, data.detail ? (_jsx("div", { className: "mt-1.5 truncate text-[11.5px] font-medium", style: { color: token.fg }, title: data.detail, children: data.detail })) : null] })] }))] }));
|
|
56
71
|
}
|
|
57
72
|
const nodeTypes = {
|
|
58
73
|
releaseTopology: ReleaseTopologyNodeCard,
|
|
59
74
|
};
|
|
75
|
+
function ReleaseTopologyEdgePath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, markerEnd, style, label, interactionWidth, }) {
|
|
76
|
+
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
|
77
|
+
sourceX,
|
|
78
|
+
sourceY,
|
|
79
|
+
sourcePosition,
|
|
80
|
+
targetX,
|
|
81
|
+
targetY,
|
|
82
|
+
targetPosition,
|
|
83
|
+
});
|
|
84
|
+
return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { path: edgePath, markerEnd: markerEnd, style: style, interactionWidth: interactionWidth }), label ? (_jsx(EdgeLabelRenderer, { children: _jsx("div", { className: "nodrag nopan absolute -translate-x-1/2 -translate-y-1/2", style: {
|
|
85
|
+
pointerEvents: 'all',
|
|
86
|
+
zIndex: 20,
|
|
87
|
+
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
88
|
+
}, children: label }) })) : null] }));
|
|
89
|
+
}
|
|
90
|
+
const edgeTypes = {
|
|
91
|
+
releaseTopology: ReleaseTopologyEdgePath,
|
|
92
|
+
};
|
|
60
93
|
function createEdge({ id, source, target, label, tone = 'neutral', dashed = false, animated, }) {
|
|
61
94
|
const color = EDGE_STYLES[tone];
|
|
62
95
|
return {
|
|
@@ -64,7 +97,7 @@ function createEdge({ id, source, target, label, tone = 'neutral', dashed = fals
|
|
|
64
97
|
source,
|
|
65
98
|
target,
|
|
66
99
|
label,
|
|
67
|
-
type: '
|
|
100
|
+
type: 'releaseTopology',
|
|
68
101
|
animated: animated ?? (tone === 'production' || tone === 'canary'),
|
|
69
102
|
markerEnd: { type: MarkerType.ArrowClosed, color },
|
|
70
103
|
style: {
|
|
@@ -72,25 +105,13 @@ function createEdge({ id, source, target, label, tone = 'neutral', dashed = fals
|
|
|
72
105
|
strokeWidth: tone === 'muted' ? 1.3 : 2,
|
|
73
106
|
strokeDasharray: dashed ? '6 5' : undefined,
|
|
74
107
|
},
|
|
75
|
-
labelStyle: {
|
|
76
|
-
fill: 'var(--muted-foreground)',
|
|
77
|
-
fontSize: 11,
|
|
78
|
-
fontFamily: 'var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
79
|
-
fontWeight: 600,
|
|
80
|
-
},
|
|
81
|
-
labelBgStyle: {
|
|
82
|
-
fill: 'var(--card)',
|
|
83
|
-
fillOpacity: 0.92,
|
|
84
|
-
},
|
|
85
|
-
labelBgPadding: [6, 4],
|
|
86
|
-
labelBgBorderRadius: 6,
|
|
87
108
|
};
|
|
88
109
|
}
|
|
89
110
|
function outputNodePosition(index, total) {
|
|
90
111
|
const compact = total <= 2;
|
|
91
112
|
const step = compact ? 126 : 108;
|
|
92
113
|
const startY = 136 - ((total - 1) * step) / 2;
|
|
93
|
-
return { x:
|
|
114
|
+
return { x: 1420, y: startY + index * step };
|
|
94
115
|
}
|
|
95
116
|
function clampTrafficRatio(value) {
|
|
96
117
|
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
@@ -110,9 +131,12 @@ function getTrafficState(line) {
|
|
|
110
131
|
canaryHasTraffic: Boolean(line.canary) && canaryRatio > 0,
|
|
111
132
|
};
|
|
112
133
|
}
|
|
134
|
+
function getOutputScopeLabel({ inProduction, inCanary, labels, }) {
|
|
135
|
+
return [inProduction ? labels.productionLane : null, inCanary ? labels.canaryLane : null].filter(Boolean).join(' + ');
|
|
136
|
+
}
|
|
113
137
|
function buildTopology(line, labels) {
|
|
114
138
|
const traffic = getTrafficState(line);
|
|
115
|
-
const
|
|
139
|
+
const canAnimateTraffic = line.status === 'running';
|
|
116
140
|
const canAddCanary = !line.canary && line.production?.currentEvent?.status === 'running';
|
|
117
141
|
const nodes = [
|
|
118
142
|
{
|
|
@@ -133,9 +157,9 @@ function buildTopology(line, labels) {
|
|
|
133
157
|
data: {
|
|
134
158
|
icon: 'router',
|
|
135
159
|
label: labels.inputRoute,
|
|
136
|
-
title:
|
|
137
|
-
meta: `${labels.canaryTrafficLabel} ${canaryTraffic}`,
|
|
160
|
+
title: labels.inputRoute,
|
|
138
161
|
tone: 'neutral',
|
|
162
|
+
compact: true,
|
|
139
163
|
},
|
|
140
164
|
},
|
|
141
165
|
];
|
|
@@ -144,14 +168,13 @@ function buildTopology(line, labels) {
|
|
|
144
168
|
id: 'upstream-router',
|
|
145
169
|
source: 'upstream',
|
|
146
170
|
target: 'input-route',
|
|
147
|
-
label: labels.ingress,
|
|
148
171
|
}),
|
|
149
172
|
];
|
|
150
173
|
if (line.production?.currentEvent) {
|
|
151
174
|
nodes.push({
|
|
152
175
|
id: 'production',
|
|
153
176
|
type: 'releaseTopology',
|
|
154
|
-
position: { x:
|
|
177
|
+
position: { x: 680, y: 62 },
|
|
155
178
|
data: {
|
|
156
179
|
icon: 'production',
|
|
157
180
|
label: labels.productionNode,
|
|
@@ -167,16 +190,16 @@ function buildTopology(line, labels) {
|
|
|
167
190
|
id: 'router-production',
|
|
168
191
|
source: 'input-route',
|
|
169
192
|
target: 'production',
|
|
170
|
-
label: line.canary ? labels.productionTraffic : '100%',
|
|
193
|
+
label: (_jsx(EdgeTextBadge, { label: line.canary ? labels.productionTraffic : '100%', title: `${labels.productionTrafficLabel} ${line.canary ? labels.productionTraffic : '100%'}` })),
|
|
171
194
|
tone: 'production',
|
|
172
|
-
animated: traffic.productionHasTraffic,
|
|
195
|
+
animated: canAnimateTraffic && traffic.productionHasTraffic,
|
|
173
196
|
}));
|
|
174
197
|
}
|
|
175
198
|
else {
|
|
176
199
|
nodes.push({
|
|
177
200
|
id: 'production',
|
|
178
201
|
type: 'releaseTopology',
|
|
179
|
-
position: { x:
|
|
202
|
+
position: { x: 680, y: 62 },
|
|
180
203
|
data: {
|
|
181
204
|
icon: 'production',
|
|
182
205
|
label: labels.productionNode,
|
|
@@ -189,7 +212,7 @@ function buildTopology(line, labels) {
|
|
|
189
212
|
id: 'router-production-empty',
|
|
190
213
|
source: 'input-route',
|
|
191
214
|
target: 'production',
|
|
192
|
-
label: labels.noTraffic,
|
|
215
|
+
label: (_jsx(EdgeTextBadge, { label: labels.noTraffic, title: `${labels.productionTrafficLabel} ${labels.noTraffic}` })),
|
|
193
216
|
tone: 'muted',
|
|
194
217
|
dashed: true,
|
|
195
218
|
}));
|
|
@@ -198,9 +221,9 @@ function buildTopology(line, labels) {
|
|
|
198
221
|
nodes.push({
|
|
199
222
|
id: 'canary',
|
|
200
223
|
type: 'releaseTopology',
|
|
201
|
-
position: { x:
|
|
224
|
+
position: { x: 680, y: 210 },
|
|
202
225
|
data: {
|
|
203
|
-
icon:
|
|
226
|
+
icon: canaryTopologyIcon(line.canary),
|
|
204
227
|
label: labels.canaryNode,
|
|
205
228
|
title: line.promptName,
|
|
206
229
|
meta: line.canaryVersionLabel ?? labels.unconfigured,
|
|
@@ -214,18 +237,18 @@ function buildTopology(line, labels) {
|
|
|
214
237
|
id: 'router-canary',
|
|
215
238
|
source: 'input-route',
|
|
216
239
|
target: 'canary',
|
|
217
|
-
label: formatPercent(traffic.canaryRatio, 0),
|
|
240
|
+
label: (_jsx(EdgeTextBadge, { label: formatPercent(traffic.canaryRatio, 0), title: `${labels.canaryTrafficLabel} ${formatPercent(traffic.canaryRatio, 0)}` })),
|
|
218
241
|
tone: 'canary',
|
|
219
|
-
animated: traffic.canaryHasTraffic,
|
|
242
|
+
animated: canAnimateTraffic && traffic.canaryHasTraffic,
|
|
220
243
|
}));
|
|
221
244
|
}
|
|
222
245
|
else {
|
|
223
246
|
nodes.push({
|
|
224
247
|
id: 'canary',
|
|
225
248
|
type: 'releaseTopology',
|
|
226
|
-
position: { x:
|
|
249
|
+
position: { x: 680, y: 210 },
|
|
227
250
|
data: {
|
|
228
|
-
icon: canAddCanary ? 'addCanary' : '
|
|
251
|
+
icon: canAddCanary ? 'addCanary' : 'canarySplit',
|
|
229
252
|
label: labels.canaryNode,
|
|
230
253
|
title: canAddCanary ? labels.addCanary : labels.noCanary,
|
|
231
254
|
meta: canAddCanary ? labels.noCanary : undefined,
|
|
@@ -239,7 +262,7 @@ function buildTopology(line, labels) {
|
|
|
239
262
|
id: 'router-canary-empty',
|
|
240
263
|
source: 'input-route',
|
|
241
264
|
target: 'canary',
|
|
242
|
-
label: labels.noTraffic,
|
|
265
|
+
label: _jsx(EdgeTextBadge, { label: labels.noTraffic, title: `${labels.canaryTrafficLabel} ${labels.noTraffic}` }),
|
|
243
266
|
tone: 'muted',
|
|
244
267
|
dashed: true,
|
|
245
268
|
}));
|
|
@@ -249,32 +272,28 @@ function buildTopology(line, labels) {
|
|
|
249
272
|
nodes.push({
|
|
250
273
|
id: 'output-route',
|
|
251
274
|
type: 'releaseTopology',
|
|
252
|
-
position: { x:
|
|
275
|
+
position: { x: 990, y: 136 },
|
|
253
276
|
data: {
|
|
254
277
|
icon: 'router',
|
|
255
278
|
label: labels.outputRoute,
|
|
256
|
-
title: labels.
|
|
257
|
-
meta: labels.outputMapping,
|
|
258
|
-
detail: line.canary?.outputMapping.length ? `${line.canary.outputMapping.length}` : labels.outputMappingEmpty,
|
|
279
|
+
title: labels.outputRoute,
|
|
259
280
|
tone: 'neutral',
|
|
260
|
-
|
|
281
|
+
compact: true,
|
|
261
282
|
},
|
|
262
283
|
});
|
|
263
284
|
edges.push(createEdge({
|
|
264
285
|
id: 'production-output-route',
|
|
265
286
|
source: 'production',
|
|
266
287
|
target: 'output-route',
|
|
267
|
-
label: 'production',
|
|
268
288
|
tone: line.production?.currentEvent ? 'production' : 'muted',
|
|
269
|
-
animated: traffic.productionHasTraffic,
|
|
289
|
+
animated: canAnimateTraffic && traffic.productionHasTraffic,
|
|
270
290
|
dashed: !line.production?.currentEvent,
|
|
271
291
|
}), createEdge({
|
|
272
292
|
id: 'canary-output-route',
|
|
273
293
|
source: 'canary',
|
|
274
294
|
target: 'output-route',
|
|
275
|
-
label: 'gray',
|
|
276
295
|
tone: line.canary ? 'canary' : 'muted',
|
|
277
|
-
animated: traffic.canaryHasTraffic,
|
|
296
|
+
animated: canAnimateTraffic && traffic.canaryHasTraffic,
|
|
278
297
|
dashed: !line.canary,
|
|
279
298
|
}));
|
|
280
299
|
const outputs = line.outputConnectors.length > 0
|
|
@@ -285,6 +304,12 @@ function buildTopology(line, labels) {
|
|
|
285
304
|
const isEmpty = connector.id === 'empty-output';
|
|
286
305
|
const inProduction = productionOutputIds.has(connector.id);
|
|
287
306
|
const inCanary = canaryOutputIds.has(connector.id);
|
|
307
|
+
const outputScope = getOutputScopeLabel({ inProduction, inCanary, labels });
|
|
308
|
+
const CanaryScopeIcon = line.canary?.trafficMode === 'dual_run' ? GitCompareArrows : Split;
|
|
309
|
+
const outputScopeItems = [
|
|
310
|
+
...(inProduction ? [{ icon: Rocket, tone: 'production' }] : []),
|
|
311
|
+
...(inCanary ? [{ icon: CanaryScopeIcon, tone: 'canary' }] : []),
|
|
312
|
+
];
|
|
288
313
|
nodes.push({
|
|
289
314
|
id,
|
|
290
315
|
type: 'releaseTopology',
|
|
@@ -294,9 +319,7 @@ function buildTopology(line, labels) {
|
|
|
294
319
|
label: labels.downstream,
|
|
295
320
|
title: connector.name,
|
|
296
321
|
meta: connector.type,
|
|
297
|
-
detail: isEmpty
|
|
298
|
-
? labels.noDownstreamDetail
|
|
299
|
-
: [inProduction ? 'production' : null, inCanary ? 'gray' : null].filter(Boolean).join(' + '),
|
|
322
|
+
detail: isEmpty ? labels.noDownstreamDetail : outputScope,
|
|
300
323
|
tone: isEmpty ? 'muted' : 'neutral',
|
|
301
324
|
badges: [connector.type],
|
|
302
325
|
},
|
|
@@ -315,9 +338,10 @@ function buildTopology(line, labels) {
|
|
|
315
338
|
id: `output-route-${id}`,
|
|
316
339
|
source: 'output-route',
|
|
317
340
|
target: id,
|
|
318
|
-
label:
|
|
319
|
-
tone:
|
|
320
|
-
animated:
|
|
341
|
+
label: outputScope ? _jsx(EdgeLaneBadge, { label: outputScope, items: outputScopeItems }) : undefined,
|
|
342
|
+
tone: 'neutral',
|
|
343
|
+
animated: canAnimateTraffic &&
|
|
344
|
+
((inProduction && traffic.productionHasTraffic) || (inCanary && traffic.canaryHasTraffic)),
|
|
321
345
|
}));
|
|
322
346
|
});
|
|
323
347
|
return { nodes, edges };
|
|
@@ -332,6 +356,7 @@ function useTopologyLabels(line) {
|
|
|
332
356
|
model: t('releases.detail.field.model'),
|
|
333
357
|
externalId: t('releases.detail.field.externalId'),
|
|
334
358
|
startedAt: t('releases.detail.field.startedAt'),
|
|
359
|
+
createdAt: t('common.createdAt'),
|
|
335
360
|
status: t('releases.detail.field.status'),
|
|
336
361
|
trafficRatio: t('releases.detail.field.trafficRatio'),
|
|
337
362
|
updatedAt: t('releases.detail.field.updatedAt'),
|
|
@@ -343,11 +368,18 @@ function useTopologyLabels(line) {
|
|
|
343
368
|
canaryBadge: t('releases.detail.topology.badge.canary'),
|
|
344
369
|
productionTrafficLabel: t('releases.detail.topology.traffic.production'),
|
|
345
370
|
canaryTrafficLabel: t('releases.detail.topology.traffic.canary'),
|
|
371
|
+
productionLane: t('releases.detail.topology.lane.production'),
|
|
372
|
+
canaryLane: t('releases.detail.topology.lane.canary'),
|
|
346
373
|
router: t('releases.detail.topology.router'),
|
|
347
374
|
inputRoute: t('releases.detail.topology.inputRoute'),
|
|
348
375
|
inputRouteMeta: t('releases.detail.topology.inputRouteMeta'),
|
|
349
376
|
outputRoute: t('releases.detail.topology.outputRoute'),
|
|
350
377
|
outputRouteTitle: t('releases.detail.topology.outputRouteTitle'),
|
|
378
|
+
inputRouteSaveFailed: t('releases.detail.topology.inputRoute.saveFailed'),
|
|
379
|
+
inputRouteInvalid: t('releases.detail.topology.inputRoute.invalid'),
|
|
380
|
+
inputRouteNoLane: t('releases.detail.topology.inputRoute.noLane'),
|
|
381
|
+
inputRouteEditFilter: t('releases.detail.topology.inputRoute.editFilter'),
|
|
382
|
+
inputRouteHideFilter: t('releases.detail.topology.inputRoute.hideFilter'),
|
|
351
383
|
ingress: t('releases.detail.topology.ingress'),
|
|
352
384
|
routeMeta: t('releases.detail.topology.routeMeta'),
|
|
353
385
|
latestEvent: t('releases.detail.topology.latestEvent'),
|
|
@@ -365,7 +397,6 @@ function useTopologyLabels(line) {
|
|
|
365
397
|
offline: t('releases.traffic.offline'),
|
|
366
398
|
readyForCandidate: t('releases.detail.topology.readyForCandidate'),
|
|
367
399
|
inspector: t('releases.detail.topology.inspector'),
|
|
368
|
-
clickHint: t('releases.detail.topology.clickHint'),
|
|
369
400
|
connectorId: t('releases.detail.topology.field.connectorId'),
|
|
370
401
|
connectorName: t('releases.detail.topology.field.connectorName'),
|
|
371
402
|
connectorType: t('releases.detail.topology.field.connectorType'),
|
|
@@ -380,81 +411,336 @@ function useTopologyLabels(line) {
|
|
|
380
411
|
tpmLimit: t('releases.detail.topology.field.tpmLimit'),
|
|
381
412
|
concurrency: t('releases.detail.topology.field.concurrency'),
|
|
382
413
|
temperature: t('releases.detail.topology.field.temperature'),
|
|
383
|
-
|
|
414
|
+
termination: t('releases.detail.topology.field.termination'),
|
|
384
415
|
trafficMode: t('releases.detail.topology.field.trafficMode'),
|
|
385
416
|
outputScope: t('releases.detail.topology.field.outputScope'),
|
|
386
417
|
productionScope: t('releases.detail.topology.scope.production'),
|
|
387
418
|
canaryScope: t('releases.detail.topology.scope.canary'),
|
|
388
|
-
routeStatus: t('releases.detail.topology.field.routeStatus'),
|
|
389
419
|
fieldMapping: t('releases.detail.topology.field.fieldMapping'),
|
|
390
|
-
fieldMappingEmpty: t('releases.detail.config.mappingEmpty'),
|
|
391
420
|
filterRules: t('releases.detail.topology.field.filterRules'),
|
|
392
421
|
filterEmpty: t('releases.detail.topology.filterEmpty'),
|
|
422
|
+
inputFields: t('releases.detail.topology.field.inputFields'),
|
|
423
|
+
inputFieldsEmpty: t('releases.detail.topology.inputFields.empty'),
|
|
393
424
|
outputMapping: t('releases.detail.topology.field.outputMapping'),
|
|
394
425
|
outputMappingEmpty: t('releases.detail.topology.outputMappingEmpty'),
|
|
426
|
+
outputRouteSaveFailed: t('releases.detail.topology.outputRoute.saveFailed'),
|
|
427
|
+
outputRouteNoLane: t('releases.detail.topology.outputRoute.noLane'),
|
|
428
|
+
outputRouteNoConnector: t('releases.detail.topology.outputRoute.noConnector'),
|
|
429
|
+
outputRouteEdit: t('releases.detail.topology.outputRoute.edit'),
|
|
430
|
+
outputRouteDialogTitle: t('releases.detail.topology.outputRoute.dialogTitle'),
|
|
431
|
+
outputRouteDialogDescription: t('releases.detail.topology.outputRoute.dialogDescription'),
|
|
432
|
+
outputRouteNewConnector: t('releases.detail.topology.outputRoute.newConnector'),
|
|
433
|
+
outputRouteConnectorFilter: t('releases.detail.topology.outputRoute.connectorFilter'),
|
|
434
|
+
outputRouteConnectorSelect: t('releases.detail.topology.outputRoute.connectorSelect'),
|
|
435
|
+
outputRouteNoAddableConnector: t('releases.detail.topology.outputRoute.noAddableConnector'),
|
|
436
|
+
outputRouteAvailableConnectors: t('releases.detail.topology.outputRoute.availableConnectors'),
|
|
437
|
+
outputRouteSelectedConnectors: t('releases.detail.topology.outputRoute.selectedConnectors'),
|
|
438
|
+
outputRouteNoSelectedConnector: t('releases.detail.topology.outputRoute.noSelectedConnector'),
|
|
439
|
+
outputRouteAddConnector: t('releases.detail.topology.outputRoute.addConnector'),
|
|
440
|
+
outputRouteRemoveConnector: t('releases.detail.topology.outputRoute.removeConnector'),
|
|
441
|
+
outputRoutePassThrough: t('releases.detail.topology.outputRoute.passThrough'),
|
|
442
|
+
outputRouteConnectorHelp: t('releases.detail.topology.outputRoute.connectorHelp'),
|
|
443
|
+
outputRouteMappingHelp: t('releases.detail.topology.outputRoute.mappingHelp'),
|
|
444
|
+
outputRouteSource: t('releases.detail.topology.outputRoute.source'),
|
|
445
|
+
outputRouteTarget: t('releases.detail.topology.outputRoute.target'),
|
|
446
|
+
outputRouteSourceDecisionOutput: t('releases.detail.topology.outputRoute.sourceDecisionOutput'),
|
|
447
|
+
outputRouteSourceParsedOutput: t('releases.detail.topology.outputRoute.sourceParsedOutput'),
|
|
448
|
+
outputRouteSourceRawResponse: t('releases.detail.topology.outputRoute.sourceRawResponse'),
|
|
449
|
+
outputRouteSourceRunResultId: t('releases.detail.topology.outputRoute.sourceRunResultId'),
|
|
450
|
+
outputRouteSourceModelOutput: t('releases.detail.topology.outputRoute.sourceModelOutput'),
|
|
451
|
+
outputRouteSourceLegacy: t('releases.detail.topology.outputRoute.sourceLegacy'),
|
|
452
|
+
outputRouteProductionConnectors: t('releases.detail.topology.outputRoute.productionConnectors'),
|
|
453
|
+
outputRouteCanaryConnectors: t('releases.detail.topology.outputRoute.canaryConnectors'),
|
|
454
|
+
outputRouteProductionMapping: t('releases.detail.topology.outputRoute.productionMapping'),
|
|
455
|
+
outputRouteCanaryMapping: t('releases.detail.topology.outputRoute.canaryMapping'),
|
|
456
|
+
outputRouteSummaryConnectors: t('releases.detail.topology.outputRoute.summaryConnectors'),
|
|
457
|
+
outputRouteSummaryMapping: t('releases.detail.topology.outputRoute.summaryMapping'),
|
|
458
|
+
outputRouteSave: t('releases.detail.topology.outputRoute.saveRoute'),
|
|
459
|
+
outputRouteSourceSearch: t('releases.detail.topology.outputRoute.sourceSearch'),
|
|
460
|
+
outputRouteSourceEmpty: t('releases.detail.topology.outputRoute.sourceEmpty'),
|
|
461
|
+
outputRouteSourcePlaceholder: t('releases.detail.topology.outputRoute.sourcePlaceholder'),
|
|
462
|
+
outputRouteSourceGroupUpstream: t('releases.detail.topology.outputRoute.sourceGroup.upstream'),
|
|
463
|
+
outputRouteSourceGroupModel: t('releases.detail.topology.outputRoute.sourceGroup.model'),
|
|
464
|
+
outputRouteSourceGroupMeta: t('releases.detail.topology.outputRoute.sourceGroup.meta'),
|
|
465
|
+
outputRouteNewBadge: t('releases.detail.topology.outputRoute.newBadge'),
|
|
466
|
+
outputRouteConnectorPickerTitle: t('releases.detail.topology.outputRoute.connectorPickerTitle'),
|
|
467
|
+
outputRouteNoMoreConnectors: t('releases.detail.topology.outputRoute.noMoreConnectors'),
|
|
468
|
+
outputRouteLaneReadonly: t('releases.detail.topology.outputRoute.laneReadonly'),
|
|
469
|
+
outputRouteConnectorCount: (count) => formatTemplate(t('releases.detail.topology.outputRoute.connectorCount'), { count }),
|
|
470
|
+
outputRouteFooterSummary: (production, canary, mappings) => formatTemplate(t('releases.detail.topology.outputRoute.footerSummary'), { production, canary, mappings }),
|
|
471
|
+
addMapping: t('releases.detail.topology.outputRoute.addMapping'),
|
|
472
|
+
removeMapping: t('releases.detail.topology.outputRoute.removeMapping'),
|
|
395
473
|
adjustTraffic: t('releases.detail.action.adjustTraffic'),
|
|
396
474
|
trafficBox: t('releases.detail.topology.trafficBox'),
|
|
397
475
|
noCanaryToAdjust: t('releases.detail.topology.noCanaryToAdjust'),
|
|
398
476
|
trafficInvalid: t('releases.detail.trafficDialog.invalid'),
|
|
399
477
|
trafficUpdateFailed: t('releases.detail.trafficDialog.updateFailed'),
|
|
478
|
+
laneIdentityTitle: t('releases.detail.topology.identity.title'),
|
|
400
479
|
runConfigTitle: t('releases.detail.topology.runConfig.title'),
|
|
401
480
|
runConfigInvalid: t('releases.detail.topology.runConfig.invalid'),
|
|
402
481
|
runConfigUpdateFailed: t('releases.detail.topology.runConfig.updateFailed'),
|
|
403
482
|
runConfigModelUnavailable: t('releases.detail.topology.runConfig.modelUnavailable'),
|
|
404
483
|
modelLimit: (limit) => t('releases.detail.topology.runConfig.modelLimit').replace('{limit}', limit),
|
|
405
484
|
unlimited: t('releases.detail.topology.runConfig.unlimited'),
|
|
485
|
+
terminationManual: t('canaryReleases.new.field.termination.manual'),
|
|
486
|
+
terminationByCount: t('canaryReleases.new.field.termination.byCount'),
|
|
487
|
+
terminationByTime: t('canaryReleases.new.field.termination.byTime'),
|
|
488
|
+
recordModeTitle: t('releases.detail.topology.recordMode.title'),
|
|
489
|
+
recordCategories: t('releases.detail.topology.recordCategories'),
|
|
490
|
+
recordCategoriesEmpty: t('releases.detail.topology.recordCategories.empty'),
|
|
491
|
+
recordCategoriesSelected: (count) => formatTemplate(t('releases.detail.topology.recordCategories.selected'), { count }),
|
|
492
|
+
recordCategoriesSelectAll: t('releases.detail.topology.recordCategories.selectAll'),
|
|
493
|
+
recordCategoriesClear: t('releases.detail.topology.recordCategories.clear'),
|
|
494
|
+
canaryActionFailed: t('releases.detail.topology.canaryActionFailed'),
|
|
495
|
+
promoteCanary: t('releases.detail.action.promoteCanary'),
|
|
496
|
+
stopCanary: t('releases.detail.action.stopCanary'),
|
|
497
|
+
replaceCanary: t('releases.detail.action.replaceCanary'),
|
|
406
498
|
loading: t('common.loading'),
|
|
407
499
|
trafficPercentInput: t('releases.detail.trafficDialog.percentInput'),
|
|
408
500
|
trafficPercentAriaLabel: t('releases.detail.trafficDialog.percentAriaLabel'),
|
|
501
|
+
cancel: t('common.cancel'),
|
|
409
502
|
save: t('common.save'),
|
|
410
503
|
savePending: t('common.savePending'),
|
|
411
504
|
canaryMode: (mode) => mode === 'dual_run' ? t('releases.detail.topology.mode.dualRun') : t('releases.detail.topology.mode.split'),
|
|
505
|
+
filterOp: (op) => t(`canaryReleases.new.filter.op.${op}`),
|
|
412
506
|
};
|
|
413
507
|
}
|
|
508
|
+
function formatTemplate(template, values) {
|
|
509
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
510
|
+
const value = values[key];
|
|
511
|
+
return value === undefined ? `{${key}}` : String(value);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
414
514
|
function toDisplayValue(value) {
|
|
415
515
|
if (value === null || value === undefined || value === '')
|
|
416
516
|
return '—';
|
|
417
517
|
return String(value);
|
|
418
518
|
}
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (line.canary) {
|
|
432
|
-
if (line.canary.variableMapping.length === 0)
|
|
433
|
-
return emptyLabel;
|
|
434
|
-
return line.canary.variableMapping
|
|
435
|
-
.map((item) => {
|
|
436
|
-
const required = item.required ? ' *' : '';
|
|
437
|
-
const defaultValue = item.defaultValue === undefined ? '' : ` = ${stringifyConfig(item.defaultValue, '')}`;
|
|
438
|
-
return `${item.source} -> ${item.target}${required}${defaultValue}`;
|
|
439
|
-
})
|
|
440
|
-
.join('\n');
|
|
519
|
+
function positiveIntegerValue(value) {
|
|
520
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
521
|
+
}
|
|
522
|
+
function formatCanaryStopConditions(config, labels) {
|
|
523
|
+
const stopConditions = readCanaryStopConditions(readRecord(config)?.['stopConditions']);
|
|
524
|
+
if (!stopConditions)
|
|
525
|
+
return labels.terminationManual;
|
|
526
|
+
const parts = [];
|
|
527
|
+
if (stopConditions.maxSamples !== null)
|
|
528
|
+
parts.push(`${labels.terminationByCount} · ${stopConditions.maxSamples}`);
|
|
529
|
+
if (stopConditions.maxDurationSeconds !== null) {
|
|
530
|
+
parts.push(`${labels.terminationByTime} · ${stopConditions.maxDurationSeconds}s`);
|
|
441
531
|
}
|
|
442
|
-
|
|
443
|
-
const entries = Object.entries(productionMapping);
|
|
444
|
-
if (entries.length === 0)
|
|
445
|
-
return emptyLabel;
|
|
446
|
-
return entries.map(([target, source]) => `${source} -> ${target}${target === 'id' ? ' *' : ''}`).join('\n');
|
|
532
|
+
return parts.length > 0 ? parts.join(' / ') : labels.terminationManual;
|
|
447
533
|
}
|
|
448
|
-
function
|
|
449
|
-
const
|
|
450
|
-
|
|
534
|
+
function readCanaryStopConditions(value) {
|
|
535
|
+
const record = readRecord(value);
|
|
536
|
+
if (!record)
|
|
537
|
+
return null;
|
|
538
|
+
const maxSamples = positiveIntegerValue(record['maxSamples']);
|
|
539
|
+
const maxDurationSeconds = positiveIntegerValue(record['maxDurationSeconds']);
|
|
540
|
+
if (maxSamples === null && maxDurationSeconds === null)
|
|
541
|
+
return null;
|
|
542
|
+
return { maxDurationSeconds, maxSamples };
|
|
543
|
+
}
|
|
544
|
+
function normalizeOutputMapping(value) {
|
|
545
|
+
if (!Array.isArray(value))
|
|
546
|
+
return [];
|
|
547
|
+
return value
|
|
548
|
+
.filter((item) => {
|
|
549
|
+
return (Boolean(item) &&
|
|
550
|
+
typeof item === 'object' &&
|
|
551
|
+
'source' in item &&
|
|
552
|
+
'target' in item &&
|
|
553
|
+
typeof item.source === 'string' &&
|
|
554
|
+
typeof item.target === 'string');
|
|
555
|
+
})
|
|
556
|
+
.map((item) => ({ source: item.source, target: item.target }));
|
|
451
557
|
}
|
|
452
|
-
function
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
558
|
+
function normalizeConnectorOutputRoutes(value, connectorIds) {
|
|
559
|
+
const selectedIds = new Set(connectorIds);
|
|
560
|
+
const routes = new Map();
|
|
561
|
+
if (Array.isArray(value)) {
|
|
562
|
+
for (const item of value) {
|
|
563
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
564
|
+
continue;
|
|
565
|
+
const record = item;
|
|
566
|
+
const connectorId = record['connectorId'];
|
|
567
|
+
if (typeof connectorId !== 'string' || !selectedIds.has(connectorId))
|
|
568
|
+
continue;
|
|
569
|
+
routes.set(connectorId, normalizeOutputMapping(record['outputMapping']));
|
|
570
|
+
}
|
|
456
571
|
}
|
|
457
|
-
|
|
572
|
+
if (routes.size > 0) {
|
|
573
|
+
return connectorIds.map((connectorId) => ({
|
|
574
|
+
connectorId,
|
|
575
|
+
outputMapping: routes.get(connectorId) ?? [],
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
const legacyMapping = normalizeOutputMapping(value);
|
|
579
|
+
return connectorIds.map((connectorId) => ({ connectorId, outputMapping: legacyMapping }));
|
|
580
|
+
}
|
|
581
|
+
function countOutputMappingRows(value) {
|
|
582
|
+
if (!Array.isArray(value))
|
|
583
|
+
return 0;
|
|
584
|
+
const connectorRouteCount = value.reduce((total, item) => {
|
|
585
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
586
|
+
return total;
|
|
587
|
+
const record = item;
|
|
588
|
+
if (typeof record['connectorId'] !== 'string')
|
|
589
|
+
return total;
|
|
590
|
+
return total + normalizeOutputMapping(record['outputMapping']).length;
|
|
591
|
+
}, 0);
|
|
592
|
+
return connectorRouteCount > 0 ? connectorRouteCount : normalizeOutputMapping(value).length;
|
|
593
|
+
}
|
|
594
|
+
function serializeOutputRoutes(value) {
|
|
595
|
+
return JSON.stringify(value
|
|
596
|
+
.map((route) => ({
|
|
597
|
+
connectorId: route.connectorId,
|
|
598
|
+
outputMapping: cleanOutputMapping(route.outputMapping),
|
|
599
|
+
}))
|
|
600
|
+
.sort((left, right) => left.connectorId.localeCompare(right.connectorId)));
|
|
601
|
+
}
|
|
602
|
+
function cleanOutputMapping(value) {
|
|
603
|
+
return normalizeOutputMapping(value)
|
|
604
|
+
.map((item) => ({ source: item.source.trim(), target: item.target.trim() }))
|
|
605
|
+
.filter((item) => item.source.length > 0 && item.target.length > 0);
|
|
606
|
+
}
|
|
607
|
+
function cleanOutputRoutes(value) {
|
|
608
|
+
return value.map((route) => ({
|
|
609
|
+
connectorId: route.connectorId,
|
|
610
|
+
outputMapping: cleanOutputMapping(route.outputMapping),
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
function getOutputSourceGroups(snapshot, upstreamFields, labels) {
|
|
614
|
+
const upstreamOptions = dedupeOutputSourceOptions(upstreamFields.map((field) => ({
|
|
615
|
+
value: field.key,
|
|
616
|
+
label: field.key,
|
|
617
|
+
description: field.description || field.type,
|
|
618
|
+
})));
|
|
619
|
+
const modelOptions = [
|
|
620
|
+
{
|
|
621
|
+
value: 'decision_output',
|
|
622
|
+
label: labels.outputRouteSourceDecisionOutput,
|
|
623
|
+
description: 'decision_output',
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
value: 'parsed_output',
|
|
627
|
+
label: labels.outputRouteSourceParsedOutput,
|
|
628
|
+
description: 'parsed_output',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
value: 'raw_response',
|
|
632
|
+
label: labels.outputRouteSourceRawResponse,
|
|
633
|
+
description: 'raw_response',
|
|
634
|
+
},
|
|
635
|
+
];
|
|
636
|
+
const metaOptions = [
|
|
637
|
+
{
|
|
638
|
+
value: 'status',
|
|
639
|
+
label: labels.status,
|
|
640
|
+
description: 'status',
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
value: 'external_id',
|
|
644
|
+
label: labels.externalId,
|
|
645
|
+
description: 'external_id',
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
value: 'run_result_id',
|
|
649
|
+
label: labels.outputRouteSourceRunResultId,
|
|
650
|
+
description: 'run_result_id',
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
value: 'metrics.latency_ms',
|
|
654
|
+
label: labels.outputRouteSourceModelOutput,
|
|
655
|
+
description: 'metrics.latency_ms',
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
const outputSchema = readRecord(readRecord(snapshot)?.['outputSchema']);
|
|
659
|
+
const fields = Array.isArray(outputSchema?.['fields']) ? outputSchema['fields'] : [];
|
|
660
|
+
const schemaOptions = fields
|
|
661
|
+
.map((field) => {
|
|
662
|
+
const record = readRecord(field);
|
|
663
|
+
const key = typeof record?.['key'] === 'string' ? record['key'].trim() : '';
|
|
664
|
+
if (!key)
|
|
665
|
+
return null;
|
|
666
|
+
const value = typeof record?.['value'] === 'string' ? record['value'].trim() : '';
|
|
667
|
+
return {
|
|
668
|
+
value: key,
|
|
669
|
+
label: key,
|
|
670
|
+
description: value || labels.outputRouteSourceModelOutput,
|
|
671
|
+
};
|
|
672
|
+
})
|
|
673
|
+
.filter((option) => option !== null);
|
|
674
|
+
const groups = [
|
|
675
|
+
{
|
|
676
|
+
key: 'upstream',
|
|
677
|
+
label: labels.outputRouteSourceGroupUpstream,
|
|
678
|
+
badge: labels.outputRouteNewBadge,
|
|
679
|
+
options: upstreamOptions,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
key: 'model',
|
|
683
|
+
label: labels.outputRouteSourceGroupModel,
|
|
684
|
+
options: dedupeOutputSourceOptions([...schemaOptions, ...modelOptions]),
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
key: 'meta',
|
|
688
|
+
label: labels.outputRouteSourceGroupMeta,
|
|
689
|
+
options: metaOptions,
|
|
690
|
+
},
|
|
691
|
+
];
|
|
692
|
+
return groups.filter((group) => group.options.length > 0);
|
|
693
|
+
}
|
|
694
|
+
function dedupeOutputSourceOptions(options) {
|
|
695
|
+
return Array.from(new Map(options.map((option) => [option.value, option])).values());
|
|
696
|
+
}
|
|
697
|
+
function readRecord(value) {
|
|
698
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
699
|
+
}
|
|
700
|
+
function getOutputRouteConnectorItems(connectorIds, connectors) {
|
|
701
|
+
const connectorById = new Map(connectors.map((connector) => [connector.id, connector]));
|
|
702
|
+
return connectorIds.map((connectorId) => ({
|
|
703
|
+
id: connectorId,
|
|
704
|
+
name: connectorById.get(connectorId)?.name ?? connectorId,
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
function OutputRouteConnectorList({ connectorIds, connectors, emptyLabel, }) {
|
|
708
|
+
const items = getOutputRouteConnectorItems(connectorIds, connectors);
|
|
709
|
+
if (items.length === 0)
|
|
710
|
+
return _jsx("span", { className: "text-muted-foreground", children: emptyLabel });
|
|
711
|
+
return (_jsx("div", { className: "space-y-1", children: items.map((item, index) => (_jsx("div", { className: "truncate", title: item.name, children: item.name }, `${item.id}:${index}`))) }));
|
|
712
|
+
}
|
|
713
|
+
function getOutputRouteMappingSummary(outputMapping, labels) {
|
|
714
|
+
const count = countOutputMappingRows(outputMapping);
|
|
715
|
+
return count > 0 ? String(count) : labels.outputRoutePassThrough;
|
|
716
|
+
}
|
|
717
|
+
function OutputRouteSummaryCards({ line, outputConnectors, labels, }) {
|
|
718
|
+
const productionEvent = line.production?.currentEvent ?? null;
|
|
719
|
+
const productionConnectorIds = productionEvent?.outputConnectorIds ?? [];
|
|
720
|
+
const canaryConnectorIds = line.canary?.outputConnectorIds ?? [];
|
|
721
|
+
return (_jsxs("div", { className: "grid gap-3", children: [_jsx(OutputRouteSummaryCard, { title: labels.production, tone: productionEvent ? 'production' : 'muted', rows: [
|
|
722
|
+
{
|
|
723
|
+
label: labels.outputRouteSummaryConnectors,
|
|
724
|
+
valueNode: (_jsx(OutputRouteConnectorList, { connectorIds: productionConnectorIds, connectors: outputConnectors, emptyLabel: labels.noDownstream })),
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
label: labels.outputRouteSummaryMapping,
|
|
728
|
+
value: getOutputRouteMappingSummary(line.productionOutputMapping, labels),
|
|
729
|
+
},
|
|
730
|
+
] }), _jsx(OutputRouteSummaryCard, { title: labels.canary, tone: line.canary ? 'canary' : 'muted', rows: [
|
|
731
|
+
{
|
|
732
|
+
label: labels.outputRouteSummaryConnectors,
|
|
733
|
+
valueNode: line.canary ? (_jsx(OutputRouteConnectorList, { connectorIds: canaryConnectorIds, connectors: outputConnectors, emptyLabel: labels.noDownstream })) : (_jsx("span", { className: "text-muted-foreground", children: labels.noCanary })),
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
label: labels.outputRouteSummaryMapping,
|
|
737
|
+
value: line.canary ? getOutputRouteMappingSummary(line.canaryOutputMapping, labels) : labels.noCanary,
|
|
738
|
+
},
|
|
739
|
+
] })] }));
|
|
740
|
+
}
|
|
741
|
+
function OutputRouteSummaryCard({ title, tone, rows, }) {
|
|
742
|
+
const token = TONE_STYLES[tone];
|
|
743
|
+
return (_jsxs("section", { className: "rounded-lg border bg-card p-3", style: { borderColor: token.bd }, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "size-2 rounded-full", style: { background: token.dot } }), _jsx("span", { className: "text-[12px] font-semibold", children: title })] }), _jsx("div", { className: "mt-3 space-y-2", children: rows.map((row) => (_jsx(InspectorRowView, { row: { ...row, mono: false } }, `${title}:${row.label}`))) })] }));
|
|
458
744
|
}
|
|
459
745
|
function trafficPercentFromText(value, max = 100) {
|
|
460
746
|
const parsed = Number(value.trim());
|
|
@@ -470,8 +756,417 @@ function getProductionTrafficPercent(canaryPercent) {
|
|
|
470
756
|
function isAdjustableCanary(canary) {
|
|
471
757
|
return canary?.status === 'pending' || canary?.status === 'running' || canary?.status === 'stopped';
|
|
472
758
|
}
|
|
759
|
+
function normalizeInputRouteFilterRules(value) {
|
|
760
|
+
const parsed = canaryReleaseFilterRulesSchema.safeParse(value ?? null);
|
|
761
|
+
return parsed.success ? parsed.data : null;
|
|
762
|
+
}
|
|
763
|
+
function cleanInputRouteMappingRecord(mapping, externalIdField) {
|
|
764
|
+
const result = {};
|
|
765
|
+
for (const [target, source] of Object.entries(mapping)) {
|
|
766
|
+
const cleanTarget = target.trim();
|
|
767
|
+
const cleanSource = source.trim();
|
|
768
|
+
if (!cleanTarget || !cleanSource || cleanTarget === 'id')
|
|
769
|
+
continue;
|
|
770
|
+
result[cleanTarget] = cleanSource;
|
|
771
|
+
}
|
|
772
|
+
if (externalIdField.trim())
|
|
773
|
+
result.id = externalIdField.trim();
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
function buildInputRouteUpdate(laneType, mapping, filterRules, externalIdField, promptVariables) {
|
|
777
|
+
const normalizedExternalIdField = externalIdField.trim();
|
|
778
|
+
if (!normalizedExternalIdField)
|
|
779
|
+
return null;
|
|
780
|
+
if (!canaryReleaseFilterRulesSchema.safeParse(filterRules).success)
|
|
781
|
+
return null;
|
|
782
|
+
if (promptVariables.some((variable) => !mapping[variable.name]?.trim()))
|
|
783
|
+
return null;
|
|
784
|
+
if (laneType === 'production') {
|
|
785
|
+
return {
|
|
786
|
+
laneType,
|
|
787
|
+
variableMapping: cleanInputRouteMappingRecord(mapping, normalizedExternalIdField),
|
|
788
|
+
filterRules,
|
|
789
|
+
externalIdField: normalizedExternalIdField,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
laneType,
|
|
794
|
+
variableMapping: canaryInputRouteMappingFromRecord(mapping, promptVariables, normalizedExternalIdField),
|
|
795
|
+
filterRules,
|
|
796
|
+
externalIdField: normalizedExternalIdField,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function inputRouteSignature(input) {
|
|
800
|
+
return input ? JSON.stringify(input) : null;
|
|
801
|
+
}
|
|
802
|
+
function getInputRouteLane(line, labels, laneType) {
|
|
803
|
+
if (laneType === 'canary') {
|
|
804
|
+
const canary = line.canary;
|
|
805
|
+
if (!canary)
|
|
806
|
+
return null;
|
|
807
|
+
return {
|
|
808
|
+
laneType: 'canary',
|
|
809
|
+
title: labels.canary,
|
|
810
|
+
tone: 'canary',
|
|
811
|
+
canEdit: isAdjustableCanary(canary),
|
|
812
|
+
eventId: canary.id,
|
|
813
|
+
updatedAt: canary.updatedAt,
|
|
814
|
+
variableMapping: canary.variableMapping,
|
|
815
|
+
filterRules: canary.filterRules,
|
|
816
|
+
externalIdField: canary.externalIdField,
|
|
817
|
+
promptVersionSnapshot: line.canaryPromptVersionSnapshot,
|
|
818
|
+
inputConnectorSnapshot: line.inputConnectorSnapshot,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const event = line.production?.currentEvent ?? null;
|
|
822
|
+
if (!event)
|
|
823
|
+
return null;
|
|
824
|
+
return {
|
|
825
|
+
laneType: 'production',
|
|
826
|
+
title: labels.production,
|
|
827
|
+
tone: 'production',
|
|
828
|
+
canEdit: event.status === 'running',
|
|
829
|
+
eventId: event.id,
|
|
830
|
+
updatedAt: event.updatedAt,
|
|
831
|
+
variableMapping: event.variableMapping,
|
|
832
|
+
filterRules: event.filterRules,
|
|
833
|
+
externalIdField: event.externalIdField,
|
|
834
|
+
promptVersionSnapshot: event.promptVersionSnapshot,
|
|
835
|
+
inputConnectorSnapshot: line.inputConnectorSnapshot,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function formatFilterValue(value) {
|
|
839
|
+
if (value === undefined || value === null)
|
|
840
|
+
return '';
|
|
841
|
+
if (typeof value === 'string')
|
|
842
|
+
return value;
|
|
843
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
844
|
+
return String(value);
|
|
845
|
+
return JSON.stringify(value);
|
|
846
|
+
}
|
|
847
|
+
function summarizeFilterRules(value, labels, depth = 0) {
|
|
848
|
+
if (!value)
|
|
849
|
+
return labels.filterEmpty;
|
|
850
|
+
if (value.type === 'atom') {
|
|
851
|
+
const op = labels.filterOp(value.op);
|
|
852
|
+
if (value.op === 'exists')
|
|
853
|
+
return `${value.field} ${op}`;
|
|
854
|
+
const displayValue = formatFilterValue(value.value);
|
|
855
|
+
return displayValue ? `${value.field} ${op} ${displayValue}` : `${value.field} ${op}`;
|
|
856
|
+
}
|
|
857
|
+
if (value.type === 'not') {
|
|
858
|
+
return `NOT (${summarizeFilterRules(value.child, labels, depth + 1)})`;
|
|
859
|
+
}
|
|
860
|
+
const joiner = value.type.toUpperCase();
|
|
861
|
+
const summary = value.children.map((child) => summarizeFilterRules(child, labels, depth + 1)).join(` ${joiner} `);
|
|
862
|
+
return depth > 0 && value.children.length > 1 ? `(${summary})` : summary;
|
|
863
|
+
}
|
|
864
|
+
function SimpleFilterRulesView({ value, labels, }) {
|
|
865
|
+
return (_jsx("div", { className: "rounded-md border bg-muted/35 px-3 py-2 font-mono text-[11.5px] leading-5 text-muted-foreground", children: summarizeFilterRules(value, labels) }));
|
|
866
|
+
}
|
|
867
|
+
function collectLaneInputRouteKeys(lane) {
|
|
868
|
+
if (!lane)
|
|
869
|
+
return [];
|
|
870
|
+
return [
|
|
871
|
+
lane.externalIdField ?? '',
|
|
872
|
+
...Object.values(inputRouteMappingRecord(lane.variableMapping)),
|
|
873
|
+
...collectFilterRuleFields(normalizeInputRouteFilterRules(lane.filterRules)),
|
|
874
|
+
];
|
|
875
|
+
}
|
|
876
|
+
function getLineInputFieldOptions(line, labels) {
|
|
877
|
+
return mergeInputFieldOptions(extractInputFieldOptionsFromSnapshot(line.inputConnectorSnapshot), [
|
|
878
|
+
...collectLaneInputRouteKeys(getInputRouteLane(line, labels, 'production')),
|
|
879
|
+
...collectLaneInputRouteKeys(getInputRouteLane(line, labels, 'canary')),
|
|
880
|
+
]);
|
|
881
|
+
}
|
|
882
|
+
function UpstreamInputFields({ line, labels, }) {
|
|
883
|
+
const fields = getLineInputFieldOptions(line, labels);
|
|
884
|
+
return (_jsxs("section", { className: "mt-4 rounded-lg border bg-card", children: [_jsx("div", { className: "border-b px-3 py-2 text-[12px] font-semibold", children: labels.inputFields }), fields.length === 0 ? (_jsx("div", { className: "px-3 py-3 text-[12px] text-muted-foreground", children: labels.inputFieldsEmpty })) : (_jsx("div", { className: "max-h-56 overflow-auto p-2", children: _jsx("div", { className: "space-y-1.5", children: fields.map((field) => (_jsxs("div", { className: "rounded-md border bg-background px-2 py-1.5", children: [_jsx("div", { className: "break-all font-mono text-[11.5px] font-semibold", children: field.key }), _jsxs("div", { className: "mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[10.5px] text-muted-foreground", children: [_jsx("span", { children: field.type }), field.description ? _jsx("span", { className: "break-words", children: field.description }) : null] })] }, field.key))) }) }))] }));
|
|
885
|
+
}
|
|
886
|
+
function InputRouteLaneEditor({ lane, labels, pending, onUpdateInputRoute, }) {
|
|
887
|
+
const initialMapping = inputRouteMappingRecord(lane.variableMapping);
|
|
888
|
+
const initialExternalIdField = lane.externalIdField ?? initialMapping.id ?? '';
|
|
889
|
+
const initialFilterRules = normalizeInputRouteFilterRules(lane.filterRules);
|
|
890
|
+
const [mapping, setMapping] = useState(initialMapping);
|
|
891
|
+
const [externalIdField, setExternalIdField] = useState(initialExternalIdField);
|
|
892
|
+
const [filterRules, setFilterRules] = useState(initialFilterRules);
|
|
893
|
+
const [savedSignature, setSavedSignature] = useState(() => {
|
|
894
|
+
const promptVariables = extractPromptVariablesFromSnapshot(lane.promptVersionSnapshot, initialMapping);
|
|
895
|
+
return inputRouteSignature(buildInputRouteUpdate(lane.laneType, initialMapping, initialFilterRules, initialExternalIdField, promptVariables));
|
|
896
|
+
});
|
|
897
|
+
const [filterEditorOpen, setFilterEditorOpen] = useState(false);
|
|
898
|
+
const [error, setError] = useState(null);
|
|
899
|
+
const token = TONE_STYLES[lane.tone];
|
|
900
|
+
const promptVariables = useMemo(() => extractPromptVariablesFromSnapshot(lane.promptVersionSnapshot, mapping), [lane.promptVersionSnapshot, mapping]);
|
|
901
|
+
const fieldOptions = useMemo(() => mergeInputFieldOptions(extractInputFieldOptionsFromSnapshot(lane.inputConnectorSnapshot), [
|
|
902
|
+
externalIdField,
|
|
903
|
+
...Object.values(mapping),
|
|
904
|
+
...collectFilterRuleFields(filterRules),
|
|
905
|
+
]), [externalIdField, filterRules, lane.inputConnectorSnapshot, mapping]);
|
|
906
|
+
const nextUpdate = buildInputRouteUpdate(lane.laneType, mapping, filterRules, externalIdField, promptVariables);
|
|
907
|
+
const nextSignature = inputRouteSignature(nextUpdate);
|
|
908
|
+
const hasDraft = nextSignature !== savedSignature;
|
|
909
|
+
const canEdit = lane.canEdit && Boolean(onUpdateInputRoute);
|
|
910
|
+
const showSave = canEdit && hasDraft;
|
|
911
|
+
const canSave = showSave && !pending;
|
|
912
|
+
function setMappingField(target, source) {
|
|
913
|
+
setMapping((current) => ({ ...current, [target]: source }));
|
|
914
|
+
setError(null);
|
|
915
|
+
}
|
|
916
|
+
function setMappingTarget(target, nextTarget) {
|
|
917
|
+
setMapping((current) => ({
|
|
918
|
+
...current,
|
|
919
|
+
[target]: current[nextTarget] ?? '',
|
|
920
|
+
[nextTarget]: current[target] ?? '',
|
|
921
|
+
}));
|
|
922
|
+
setError(null);
|
|
923
|
+
}
|
|
924
|
+
async function saveInputRoute() {
|
|
925
|
+
if (!canEdit || pending || !onUpdateInputRoute)
|
|
926
|
+
return;
|
|
927
|
+
if (!nextUpdate || !nextSignature) {
|
|
928
|
+
setError(labels.inputRouteInvalid);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (nextSignature === savedSignature)
|
|
932
|
+
return;
|
|
933
|
+
setError(null);
|
|
934
|
+
try {
|
|
935
|
+
await onUpdateInputRoute(nextUpdate);
|
|
936
|
+
setSavedSignature(nextSignature);
|
|
937
|
+
}
|
|
938
|
+
catch (saveError) {
|
|
939
|
+
setError(getApiErrorMessage(saveError) ?? labels.inputRouteSaveFailed);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return (_jsxs("section", { className: "mt-4 rounded-lg border bg-card", style: { borderColor: token.bd }, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "size-2 rounded-full", style: { background: token.dot } }), _jsx("span", { className: "text-[12px] font-semibold", children: lane.title })] }), showSave ? (_jsxs(Button, { type: "button", size: "sm", onClick: saveInputRoute, disabled: !canSave, className: "h-7 px-2 text-[11.5px]", children: [_jsx(Save, { className: "size-3.5" }), pending ? labels.savePending : labels.save] })) : null] }), _jsxs("div", { className: "space-y-4 p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "mb-2 text-[11px] font-medium text-muted-foreground", children: labels.fieldMapping }), _jsx(FieldMappingTable, { compact: true, fields: fieldOptions, promptVariables: promptVariables, externalIdField: externalIdField, mapping: mapping, readOnly: !canEdit || pending, testIdPrefix: `release-${lane.laneType}-input-route`, onExternalIdFieldChange: (value) => {
|
|
943
|
+
setExternalIdField(value);
|
|
944
|
+
setError(null);
|
|
945
|
+
}, onMappingChange: setMappingField, onMappingTargetChange: setMappingTarget })] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: labels.filterRules }), canEdit && !pending ? (_jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "h-7 px-2 text-[11.5px]", onClick: () => setFilterEditorOpen((current) => !current), children: filterEditorOpen ? labels.inputRouteHideFilter : labels.inputRouteEditFilter })) : null] }), canEdit && !pending && filterEditorOpen ? (_jsx(FilterRulesBuilder, { compact: true, value: filterRules, fields: fieldOptions, onChange: (next) => {
|
|
946
|
+
setFilterRules(next);
|
|
947
|
+
setError(null);
|
|
948
|
+
} })) : (_jsx(SimpleFilterRulesView, { value: filterRules, labels: labels }))] }), error ? _jsx("p", { className: "text-[12px] text-destructive", children: error }) : null] })] }));
|
|
949
|
+
}
|
|
950
|
+
function IconTooltipButton({ label, icon: Icon, onClick, disabled, variant = 'outline', className, testId, }) {
|
|
951
|
+
return (_jsx(TooltipProvider, { delayDuration: 140, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", size: "icon", variant: variant, className: cn('size-8', className), onClick: onClick, disabled: disabled, "aria-label": label, "data-testid": testId, children: _jsx(Icon, { className: "size-3.5" }) }) }), _jsx(TooltipContent, { side: "top", children: label })] }) }));
|
|
952
|
+
}
|
|
953
|
+
function CanaryLaneActions({ canary, labels, pending, onAddCanary, onStopCanary, onPromoteCanary, }) {
|
|
954
|
+
const [activeAction, setActiveAction] = useState(null);
|
|
955
|
+
const [error, setError] = useState(null);
|
|
956
|
+
const canPromote = canary?.status === 'running' && Boolean(onPromoteCanary);
|
|
957
|
+
const canStop = canary?.status === 'running' && Boolean(onStopCanary);
|
|
958
|
+
const canAddCanary = Boolean(onAddCanary);
|
|
959
|
+
const busy = pending || activeAction !== null;
|
|
960
|
+
const addCanaryLabel = canary ? labels.replaceCanary : labels.addCanary;
|
|
961
|
+
async function runAction(action, handler) {
|
|
962
|
+
if (!handler || !canary || busy)
|
|
963
|
+
return;
|
|
964
|
+
setError(null);
|
|
965
|
+
setActiveAction(action);
|
|
966
|
+
try {
|
|
967
|
+
await handler(canary);
|
|
968
|
+
}
|
|
969
|
+
catch (caught) {
|
|
970
|
+
setError(getApiErrorMessage(caught) ?? labels.canaryActionFailed);
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
setActiveAction(null);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (!canPromote && !canStop && !canAddCanary)
|
|
977
|
+
return null;
|
|
978
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col items-end gap-1", "data-testid": "release-topology-canary-actions", children: [_jsxs("div", { className: "flex flex-wrap justify-end gap-1.5", children: [canPromote ? (_jsx(IconTooltipButton, { label: labels.promoteCanary, icon: Rocket, onClick: () => void runAction('promote', onPromoteCanary), disabled: busy, variant: "default", testId: "release-topology-promote-canary" })) : null, canAddCanary ? (_jsx(IconTooltipButton, { label: addCanaryLabel, icon: Plus, onClick: () => onAddCanary?.(), disabled: busy, variant: "outline", testId: "release-topology-add-canary" })) : null, canStop ? (_jsx(IconTooltipButton, { label: labels.stopCanary, icon: Square, onClick: () => void runAction('stop', onStopCanary), disabled: busy, variant: "outline", className: "text-destructive hover:text-destructive" })) : null] }), error ? _jsx("p", { className: "max-w-full truncate text-[11.5px] text-destructive", children: error }) : null] }));
|
|
979
|
+
}
|
|
980
|
+
function OutputRouteControl({ line, outputConnectors, outputConnectorsLoading, labels, pending, onUpdateOutputRoute, }) {
|
|
981
|
+
const [open, setOpen] = useState(false);
|
|
982
|
+
const upstreamFields = useMemo(() => extractInputFieldOptionsFromSnapshot(line.inputConnectorSnapshot), [line.inputConnectorSnapshot]);
|
|
983
|
+
const laneDrafts = [
|
|
984
|
+
line.production?.currentEvent
|
|
985
|
+
? {
|
|
986
|
+
key: 'production',
|
|
987
|
+
title: labels.production,
|
|
988
|
+
tone: 'production',
|
|
989
|
+
canEdit: line.production.currentEvent.status === 'running',
|
|
990
|
+
connectorIds: line.production.currentEvent.outputConnectorIds,
|
|
991
|
+
outputMapping: line.productionOutputMapping,
|
|
992
|
+
outputSourceGroups: getOutputSourceGroups(line.production.currentEvent.promptVersionSnapshot, upstreamFields, labels),
|
|
993
|
+
}
|
|
994
|
+
: null,
|
|
995
|
+
line.canary
|
|
996
|
+
? {
|
|
997
|
+
key: 'canary',
|
|
998
|
+
title: labels.canary,
|
|
999
|
+
tone: 'canary',
|
|
1000
|
+
canEdit: isAdjustableCanary(line.canary),
|
|
1001
|
+
connectorIds: line.canary.outputConnectorIds,
|
|
1002
|
+
outputMapping: line.canaryOutputMapping,
|
|
1003
|
+
outputSourceGroups: getOutputSourceGroups(line.canaryPromptVersionSnapshot, upstreamFields, labels),
|
|
1004
|
+
}
|
|
1005
|
+
: null,
|
|
1006
|
+
];
|
|
1007
|
+
const lanes = laneDrafts.filter((lane) => lane !== null);
|
|
1008
|
+
if (lanes.length === 0) {
|
|
1009
|
+
return (_jsx("section", { className: "mt-4 rounded-lg border bg-card p-3 text-[12px] text-muted-foreground", children: labels.outputRouteNoLane }));
|
|
1010
|
+
}
|
|
1011
|
+
return (_jsxs("div", { className: "mt-4", children: [_jsxs(Button, { type: "button", className: "w-full justify-center", onClick: () => setOpen(true), children: [_jsx(Split, { className: "size-4" }), labels.outputRouteEdit] }), open ? (_jsx(OutputRouteDialog, { open: open, onOpenChange: setOpen, lanes: lanes, outputConnectors: outputConnectors, outputConnectorsLoading: outputConnectorsLoading, labels: labels, pending: pending, lineLabel: line.label, onUpdateOutputRoute: onUpdateOutputRoute })) : null] }));
|
|
1012
|
+
}
|
|
1013
|
+
function OutputRouteDialog({ open, onOpenChange, lanes, outputConnectors, outputConnectorsLoading, labels, pending, lineLabel, onUpdateOutputRoute, }) {
|
|
1014
|
+
const [drafts, setDrafts] = useState(() => Object.fromEntries(lanes.map((lane) => [lane.key, normalizeConnectorOutputRoutes(lane.outputMapping, lane.connectorIds)])));
|
|
1015
|
+
const [saving, setSaving] = useState(false);
|
|
1016
|
+
const [error, setError] = useState(null);
|
|
1017
|
+
const connectorById = useMemo(() => new Map(outputConnectors.map((connector) => [connector.id, connector])), [outputConnectors]);
|
|
1018
|
+
const laneEntries = lanes.map((lane) => {
|
|
1019
|
+
const routes = drafts[lane.key] ?? [];
|
|
1020
|
+
const initialRoutes = normalizeConnectorOutputRoutes(lane.outputMapping, lane.connectorIds);
|
|
1021
|
+
const cleanRoutes = cleanOutputRoutes(routes);
|
|
1022
|
+
const hasDraft = serializeOutputRoutes(cleanRoutes) !== serializeOutputRoutes(initialRoutes);
|
|
1023
|
+
return { lane, routes, cleanRoutes, hasDraft };
|
|
1024
|
+
});
|
|
1025
|
+
const busy = pending || saving;
|
|
1026
|
+
const changedEntries = laneEntries.filter((entry) => entry.hasDraft && entry.lane.canEdit);
|
|
1027
|
+
const hasReadonlyDraft = laneEntries.some((entry) => entry.hasDraft && !entry.lane.canEdit);
|
|
1028
|
+
const canSave = Boolean(onUpdateOutputRoute) && !busy && changedEntries.length > 0 && !hasReadonlyDraft;
|
|
1029
|
+
const productionConnectorCount = drafts.production?.length ?? 0;
|
|
1030
|
+
const canaryConnectorCount = drafts.canary?.length ?? 0;
|
|
1031
|
+
const mappingCount = laneEntries.reduce((total, entry) => total + entry.cleanRoutes.reduce((sum, route) => sum + route.outputMapping.length, 0), 0);
|
|
1032
|
+
function updateLaneRoutes(laneType, updater) {
|
|
1033
|
+
setDrafts((current) => ({ ...current, [laneType]: updater(current[laneType] ?? []) }));
|
|
1034
|
+
setError(null);
|
|
1035
|
+
}
|
|
1036
|
+
async function saveOutputRoutes() {
|
|
1037
|
+
const update = onUpdateOutputRoute;
|
|
1038
|
+
if (!canSave || !update)
|
|
1039
|
+
return;
|
|
1040
|
+
setSaving(true);
|
|
1041
|
+
setError(null);
|
|
1042
|
+
try {
|
|
1043
|
+
for (const entry of changedEntries) {
|
|
1044
|
+
await update({
|
|
1045
|
+
laneType: entry.lane.key,
|
|
1046
|
+
outputConnectorIds: entry.cleanRoutes.map((route) => route.connectorId),
|
|
1047
|
+
outputMapping: entry.cleanRoutes,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
onOpenChange(false);
|
|
1051
|
+
}
|
|
1052
|
+
catch (caught) {
|
|
1053
|
+
setError(getApiErrorMessage(caught) ?? labels.outputRouteSaveFailed);
|
|
1054
|
+
}
|
|
1055
|
+
finally {
|
|
1056
|
+
setSaving(false);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[88vh] max-w-[920px] gap-0 overflow-hidden rounded-[14px] p-0", children: [_jsxs(DialogHeader, { className: "border-b px-5 py-4 pr-12 text-left", children: [_jsx(DialogTitle, { className: "text-[16px]", children: labels.outputRouteDialogTitle }), _jsxs(DialogDescription, { className: "text-[12.5px]", children: [labels.outputRouteTitle, " \u00B7 ", _jsx("span", { className: "font-mono", children: lineLabel }), " \u00B7", ' ', labels.outputRouteDialogDescription] })] }), _jsx("div", { className: "max-h-[calc(88vh-136px)] overflow-y-auto", children: _jsx("div", { className: cn('grid grid-cols-1', lanes.length > 1 && 'lg:grid-cols-2'), children: laneEntries.map((entry, index) => (_jsx(OutputRouteLaneEditor, { lane: entry.lane, routes: entry.routes, canEdit: entry.lane.canEdit && Boolean(onUpdateOutputRoute), connectorById: connectorById, outputConnectors: outputConnectors, outputConnectorsLoading: outputConnectorsLoading, labels: labels, busy: busy, className: index > 0 ? 'lg:border-l' : undefined, onRoutesChange: (updater) => updateLaneRoutes(entry.lane.key, updater) }, entry.lane.key))) }) }), _jsxs("div", { className: "flex flex-col gap-3 border-t bg-muted/40 px-5 py-3 sm:flex-row sm:items-center", children: [_jsx("div", { className: "min-w-0 text-[12px] text-muted-foreground", children: labels.outputRouteFooterSummary(productionConnectorCount, canaryConnectorCount, mappingCount) }), error ? _jsx("div", { className: "text-[12px] text-destructive", children: error }) : null, _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: labels.cancel }), _jsxs(Button, { type: "button", onClick: saveOutputRoutes, disabled: !canSave, children: [_jsx(Save, { className: "size-4" }), busy ? labels.savePending : labels.outputRouteSave] })] })] })] }) }));
|
|
1060
|
+
}
|
|
1061
|
+
function OutputRouteLaneEditor({ lane, routes, canEdit, connectorById, outputConnectors, outputConnectorsLoading, labels, busy, className, onRoutesChange, }) {
|
|
1062
|
+
const token = TONE_STYLES[lane.tone];
|
|
1063
|
+
const selectedConnectorIds = new Set(routes.map((route) => route.connectorId));
|
|
1064
|
+
const addableConnectors = outputConnectors.filter((connector) => {
|
|
1065
|
+
if (selectedConnectorIds.has(connector.id))
|
|
1066
|
+
return false;
|
|
1067
|
+
return true;
|
|
1068
|
+
});
|
|
1069
|
+
function addConnector(connectorId) {
|
|
1070
|
+
onRoutesChange((current) => current.some((route) => route.connectorId === connectorId)
|
|
1071
|
+
? current
|
|
1072
|
+
: [...current, { connectorId, outputMapping: [{ source: '', target: '' }] }]);
|
|
1073
|
+
}
|
|
1074
|
+
function removeConnector(connectorId) {
|
|
1075
|
+
onRoutesChange((current) => current.filter((route) => route.connectorId !== connectorId));
|
|
1076
|
+
}
|
|
1077
|
+
function addMapping(connectorId) {
|
|
1078
|
+
onRoutesChange((current) => current.map((route) => route.connectorId === connectorId
|
|
1079
|
+
? { ...route, outputMapping: [...route.outputMapping, { source: '', target: '' }] }
|
|
1080
|
+
: route));
|
|
1081
|
+
}
|
|
1082
|
+
function setMappingField(connectorId, index, field, value) {
|
|
1083
|
+
onRoutesChange((current) => current.map((route) => route.connectorId === connectorId
|
|
1084
|
+
? {
|
|
1085
|
+
...route,
|
|
1086
|
+
outputMapping: route.outputMapping.map((item, itemIndex) => itemIndex === index ? { ...item, [field]: value } : item),
|
|
1087
|
+
}
|
|
1088
|
+
: route));
|
|
1089
|
+
}
|
|
1090
|
+
function removeMapping(connectorId, index) {
|
|
1091
|
+
onRoutesChange((current) => current.map((route) => route.connectorId === connectorId
|
|
1092
|
+
? { ...route, outputMapping: route.outputMapping.filter((_item, itemIndex) => itemIndex !== index) }
|
|
1093
|
+
: route));
|
|
1094
|
+
}
|
|
1095
|
+
return (_jsxs("section", { className: cn('min-w-0 px-5 py-4', className), children: [_jsxs("div", { className: "mb-2 flex items-baseline gap-2 border-b-2 pb-3", style: { borderColor: token.dot }, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: token.dot } }), _jsx("span", { className: "truncate text-[15px] font-semibold", children: lane.title }), _jsx("span", { className: "font-mono text-[11.5px] text-muted-foreground", children: lane.key })] }), _jsx("span", { className: "ml-auto whitespace-nowrap text-[11.5px] text-muted-foreground", children: labels.outputRouteConnectorCount(routes.length) })] }), !canEdit ? (_jsx("div", { className: "mb-2 text-[11.5px] text-muted-foreground", children: labels.outputRouteLaneReadonly })) : null, _jsx("div", { className: "divide-y divide-dashed", children: routes.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-8 text-center text-[12px] text-muted-foreground", children: labels.outputRouteNoSelectedConnector })) : (routes.map((route) => {
|
|
1096
|
+
const connector = connectorById.get(route.connectorId);
|
|
1097
|
+
const connectorName = connector?.name ?? route.connectorId;
|
|
1098
|
+
const connectorMeta = connector
|
|
1099
|
+
? `${connector.type} · ${connector.configSummary || connector.healthStatus}`
|
|
1100
|
+
: route.connectorId;
|
|
1101
|
+
return (_jsxs("div", { className: "group py-3 first:pt-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-mono text-[13.5px] font-semibold", title: connectorName, children: connectorName }), _jsx("div", { className: "mt-0.5 truncate font-mono text-[11.5px] text-muted-foreground", title: connectorMeta, children: connectorMeta })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7 opacity-0 transition-opacity group-hover:opacity-100", title: labels.outputRouteRemoveConnector, "aria-label": labels.outputRouteRemoveConnector, disabled: !canEdit || busy, onClick: () => removeConnector(route.connectorId), children: _jsx(X, { className: "size-3.5" }) })] }), _jsx("div", { className: "mt-2 space-y-0", children: route.outputMapping.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground", children: labels.outputRoutePassThrough })) : (route.outputMapping.map((item, index) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1.18fr)_16px_minmax(0,0.82fr)_24px] items-center gap-1 border-t border-dashed py-1 first:border-t-0", children: [_jsx(OutputSourcePicker, { value: item.source, sourceGroups: lane.outputSourceGroups, labels: labels, disabled: !canEdit || busy, onChange: (value) => setMappingField(route.connectorId, index, 'source', value) }), _jsx("span", { className: "text-center text-[12px] text-muted-foreground", children: "\u2192" }), _jsx("input", { value: item.target, onChange: (event) => setMappingField(route.connectorId, index, 'target', event.target.value), placeholder: labels.outputRouteTarget, disabled: !canEdit || busy, className: "h-8 min-w-0 border-0 border-b border-dashed bg-transparent px-1 font-mono text-[12.5px] outline-none transition-colors placeholder:font-sans placeholder:italic placeholder:text-muted-foreground hover:border-muted-foreground focus:border-primary focus:border-solid disabled:cursor-not-allowed disabled:opacity-60" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6 opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100", title: labels.removeMapping, "aria-label": labels.removeMapping, disabled: !canEdit || busy, onClick: () => removeMapping(route.connectorId, index), children: _jsx(X, { className: "size-3.5" }) })] }, index)))) }), _jsxs("button", { type: "button", className: "mt-1 inline-flex items-center gap-1 rounded-md px-1 py-1 text-[12px] text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60", disabled: !canEdit || busy, onClick: () => addMapping(route.connectorId), children: [_jsx(Plus, { className: "size-3" }), labels.addMapping] })] }, route.connectorId));
|
|
1102
|
+
})) }), _jsx(OutputConnectorAddPopover, { addableConnectors: addableConnectors, disabled: !canEdit || busy, loading: outputConnectorsLoading, labels: labels, onAdd: addConnector })] }));
|
|
1103
|
+
}
|
|
1104
|
+
function OutputConnectorAddPopover({ addableConnectors, disabled, loading, labels, onAdd, }) {
|
|
1105
|
+
const [open, setOpen] = useState(false);
|
|
1106
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", disabled: disabled || loading, className: "mt-3 flex h-10 w-full items-center justify-center gap-1.5 rounded-md border border-dashed bg-transparent text-[12.5px] font-medium text-muted-foreground transition-colors hover:border-ring hover:bg-muted/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60", children: [_jsx(Plus, { className: "size-3.5" }), loading ? labels.loading : labels.outputRouteAddConnector] }) }), _jsxs(PopoverContent, { align: "center", sideOffset: 6, className: "w-[300px] p-1", onWheelCapture: (event) => event.stopPropagation(), onTouchMoveCapture: (event) => event.stopPropagation(), children: [_jsx("div", { className: "px-2 py-1.5 text-[11px] font-semibold text-muted-foreground", children: labels.outputRouteConnectorPickerTitle }), addableConnectors.length === 0 ? (_jsx("div", { className: "px-2 py-4 text-center text-[12px] text-muted-foreground", children: labels.outputRouteNoMoreConnectors })) : (_jsx("div", { className: "max-h-[280px] overflow-auto overscroll-contain", onWheelCapture: (event) => event.stopPropagation(), onTouchMoveCapture: (event) => event.stopPropagation(), children: addableConnectors.map((connector) => (_jsx("button", { type: "button", className: "flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted", onClick: () => {
|
|
1107
|
+
onAdd(connector.id);
|
|
1108
|
+
setOpen(false);
|
|
1109
|
+
}, children: _jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-[12.5px]", children: connector.name }) }, connector.id))) }))] })] }));
|
|
1110
|
+
}
|
|
1111
|
+
function OutputSourcePicker({ value, sourceGroups, labels, disabled, onChange, }) {
|
|
1112
|
+
const [open, setOpen] = useState(false);
|
|
1113
|
+
const [query, setQuery] = useState('');
|
|
1114
|
+
const groups = sourceGroupsForValue(value, sourceGroups, labels);
|
|
1115
|
+
const visibleGroups = filterOutputSourceGroups(groups, query);
|
|
1116
|
+
const groupKey = outputSourceGroupKeyForValue(value, groups);
|
|
1117
|
+
return (_jsxs(Popover, { open: open, onOpenChange: (nextOpen) => {
|
|
1118
|
+
setOpen(nextOpen);
|
|
1119
|
+
if (!nextOpen)
|
|
1120
|
+
setQuery('');
|
|
1121
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", disabled: disabled, className: cn('flex h-8 min-w-0 items-center gap-1.5 rounded-md border border-transparent px-1.5 font-mono text-[12.5px] transition-colors hover:bg-muted focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-60', !value && 'font-sans italic text-muted-foreground', groupKey === 'upstream' && 'text-[var(--status-running-fg)]'), children: [groupKey ? (_jsx("span", { className: cn('size-1.5 shrink-0 rounded-[2px]', outputSourceGroupDotClass(groupKey)) })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: value || labels.outputRouteSourcePlaceholder }), _jsx(ChevronDown, { className: cn('size-3 shrink-0 text-muted-foreground opacity-0', open && 'opacity-100') })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[300px] p-0", onWheelCapture: (event) => event.stopPropagation(), onTouchMoveCapture: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-2 py-1.5", children: [_jsx(Search, { className: "size-3.5 text-muted-foreground" }), _jsx("input", { value: query, onChange: (event) => setQuery(event.target.value), placeholder: labels.outputRouteSourceSearch, className: "h-7 min-w-0 flex-1 bg-transparent text-[12.5px] outline-none placeholder:text-muted-foreground" })] }), _jsx("div", { className: "max-h-[300px] overflow-auto overscroll-contain p-1", onWheelCapture: (event) => event.stopPropagation(), onTouchMoveCapture: (event) => event.stopPropagation(), children: visibleGroups.length === 0 ? (_jsx("div", { className: "px-2 py-4 text-center text-[12px] text-muted-foreground", children: labels.outputRouteSourceEmpty })) : (visibleGroups.map((group, groupIndex) => (_jsxs("div", { className: cn(groupIndex > 0 && 'border-t pt-1'), children: [_jsxs("div", { className: "flex items-center gap-2 px-2 py-1.5 text-[11px] font-semibold text-muted-foreground", children: [_jsx("span", { className: cn('size-1.5 rounded-[2px]', outputSourceGroupDotClass(group.key)) }), _jsx("span", { children: group.label }), group.badge ? (_jsx("span", { className: "ml-auto rounded border border-[var(--status-running-bd)] bg-[var(--status-running-bg)] px-1.5 py-0.5 font-mono text-[9.5px] font-semibold text-[var(--status-running-fg)]", children: group.badge })) : null] }), group.options.map((option) => (_jsxs("button", { type: "button", className: cn('flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left font-mono text-[12.5px] transition-colors hover:bg-muted', option.value === value && 'bg-primary/5'), onClick: () => {
|
|
1122
|
+
onChange(option.value);
|
|
1123
|
+
setOpen(false);
|
|
1124
|
+
}, children: [_jsx(Check, { className: cn('mt-0.5 size-3 shrink-0 text-primary opacity-0', option.value === value && 'opacity-100') }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: option.value })] }, option.value)))] }, group.key)))) })] })] }));
|
|
1125
|
+
}
|
|
1126
|
+
function sourceGroupsForValue(value, sourceGroups, labels) {
|
|
1127
|
+
if (!value || sourceGroups.some((group) => group.options.some((option) => option.value === value))) {
|
|
1128
|
+
return sourceGroups;
|
|
1129
|
+
}
|
|
1130
|
+
return [
|
|
1131
|
+
{
|
|
1132
|
+
key: 'legacy',
|
|
1133
|
+
label: labels.outputRouteSourceLegacy,
|
|
1134
|
+
options: [{ value, label: value, description: labels.outputRouteSourceLegacy }],
|
|
1135
|
+
},
|
|
1136
|
+
...sourceGroups,
|
|
1137
|
+
];
|
|
1138
|
+
}
|
|
1139
|
+
function filterOutputSourceGroups(sourceGroups, query) {
|
|
1140
|
+
const normalized = query.trim().toLowerCase();
|
|
1141
|
+
if (!normalized)
|
|
1142
|
+
return sourceGroups;
|
|
1143
|
+
return sourceGroups
|
|
1144
|
+
.map((group) => ({
|
|
1145
|
+
...group,
|
|
1146
|
+
options: group.options.filter((option) => [option.value, option.label, option.description].join(' ').toLowerCase().includes(normalized)),
|
|
1147
|
+
}))
|
|
1148
|
+
.filter((group) => group.options.length > 0);
|
|
1149
|
+
}
|
|
1150
|
+
function outputSourceGroupKeyForValue(value, sourceGroups) {
|
|
1151
|
+
if (!value)
|
|
1152
|
+
return null;
|
|
1153
|
+
return sourceGroups.find((group) => group.options.some((option) => option.value === value))?.key ?? null;
|
|
1154
|
+
}
|
|
1155
|
+
function outputSourceGroupDotClass(groupKey) {
|
|
1156
|
+
switch (groupKey) {
|
|
1157
|
+
case 'upstream':
|
|
1158
|
+
return 'bg-[var(--status-running-dot)]';
|
|
1159
|
+
case 'model':
|
|
1160
|
+
return 'bg-primary';
|
|
1161
|
+
case 'meta':
|
|
1162
|
+
return 'bg-[var(--status-canary-dot)]';
|
|
1163
|
+
case 'legacy':
|
|
1164
|
+
return 'bg-muted-foreground';
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
473
1167
|
function TrafficRatioControl({ line, labels, onUpdateTrafficRatio, pending, }) {
|
|
474
1168
|
const canary = line.canary;
|
|
1169
|
+
const isDualRun = canary?.trafficMode === 'dual_run';
|
|
475
1170
|
const maxTrafficPercent = 100;
|
|
476
1171
|
const currentPercent = canary ? Math.round(canary.trafficRatio * 100) : 0;
|
|
477
1172
|
const [trafficPercent, setTrafficPercent] = useState(canary ? trafficPercentFromRatio(canary.trafficRatio) : '');
|
|
@@ -523,13 +1218,16 @@ function TrafficRatioControl({ line, labels, onUpdateTrafficRatio, pending, }) {
|
|
|
523
1218
|
setTrafficPercent(parsed === null ? '' : String(100 - parsed));
|
|
524
1219
|
setTrafficError(null);
|
|
525
1220
|
}
|
|
526
|
-
return (_jsxs("section", { className: "mt-4 rounded-lg border bg-card", children: [_jsx("div", { className: "border-b px-3 py-2 text-[12px] font-semibold", children: labels.trafficBox }), _jsxs("div", { className: "space-y-3 p-3", children: [
|
|
1221
|
+
return (_jsxs("section", { className: "mt-4 rounded-lg border bg-card", children: [_jsx("div", { className: "border-b px-3 py-2 text-[12px] font-semibold", children: labels.trafficBox }), _jsxs("div", { className: "space-y-3 p-3", children: [isDualRun ? (_jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: labels.canaryTrafficLabel }), _jsxs("label", { className: "mt-1 flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, max: 100, value: trafficPercent, onChange: (event) => setCanaryPercentText(event.target.value), onKeyDown: (event) => {
|
|
1222
|
+
if (event.key === 'Enter')
|
|
1223
|
+
void saveTrafficRatio();
|
|
1224
|
+
}, disabled: !canEditTraffic || isSavingTraffic, "aria-label": labels.canaryTrafficLabel, className: "h-8 font-mono text-xs" }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: "%" })] })] })) : (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: labels.productionTrafficLabel }), _jsxs("label", { className: "mt-1 flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, max: 100, value: productionPercent, onChange: (event) => setProductionPercentText(event.target.value), onKeyDown: (event) => {
|
|
527
1225
|
if (event.key === 'Enter')
|
|
528
1226
|
void saveTrafficRatio();
|
|
529
1227
|
}, disabled: !canEditTraffic || isSavingTraffic, "aria-label": labels.productionTrafficLabel, className: "h-8 font-mono text-xs" }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: "%" })] })] }), _jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: labels.canaryTrafficLabel }), _jsxs("label", { className: "mt-1 flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, max: 100, value: trafficPercent, onChange: (event) => setCanaryPercentText(event.target.value), onKeyDown: (event) => {
|
|
530
1228
|
if (event.key === 'Enter')
|
|
531
1229
|
void saveTrafficRatio();
|
|
532
|
-
}, disabled: !canEditTraffic || isSavingTraffic, "aria-label": labels.canaryTrafficLabel, className: "h-8 font-mono text-xs" }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: "%" })] })] })] }), canary ? (_jsxs(_Fragment, { children: [_jsx("input", { type: "range", min: 0, max: maxTrafficPercent, step: 1, value: parsedTrafficPercent ?? currentPercent, "aria-label": labels.trafficPercentAriaLabel, onChange: (event) => setCanaryPercentText(event.target.value), disabled: !canEditTraffic || isSavingTraffic, className: "w-full accent-primary disabled:opacity-50" }),
|
|
1230
|
+
}, disabled: !canEditTraffic || isSavingTraffic, "aria-label": labels.canaryTrafficLabel, className: "h-8 font-mono text-xs" }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: "%" })] })] })] })), canary ? (_jsxs(_Fragment, { children: [_jsx("input", { type: "range", min: 0, max: maxTrafficPercent, step: 1, value: parsedTrafficPercent ?? currentPercent, "aria-label": labels.trafficPercentAriaLabel, onChange: (event) => setCanaryPercentText(event.target.value), disabled: !canEditTraffic || isSavingTraffic, className: "w-full accent-primary disabled:opacity-50" }), _jsx("div", { className: "flex items-center justify-between font-mono text-[11px] text-muted-foreground", children: isDualRun ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "0%" }), _jsxs("span", { children: [labels.canaryTrafficLabel, " 100%"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { children: [labels.productionTrafficLabel, " 100%"] }), _jsxs("span", { children: [labels.canaryTrafficLabel, " 100%"] })] })) }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", size: "sm", onClick: saveTrafficRatio, disabled: !canSaveTraffic, children: [_jsx(Save, { className: "size-3.5" }), isSavingTraffic ? labels.savePending : labels.save] }) })] })) : (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground", children: labels.noCanaryToAdjust })), trafficError ? _jsx("p", { className: "text-[12px] text-destructive", children: trafficError }) : null] })] }));
|
|
533
1231
|
}
|
|
534
1232
|
function numberText(value) {
|
|
535
1233
|
return typeof value === 'number' && Number.isFinite(value) ? String(value) : '';
|
|
@@ -542,6 +1240,7 @@ function runConfigDraftFromRecord(config, modelId) {
|
|
|
542
1240
|
tpmLimit: numberText(record.tpmLimit),
|
|
543
1241
|
concurrency: numberText(record.concurrency),
|
|
544
1242
|
temperature: numberText(record.temperature) || '0.3',
|
|
1243
|
+
stopConditions: readCanaryStopConditions(record.stopConditions),
|
|
545
1244
|
};
|
|
546
1245
|
}
|
|
547
1246
|
function parsePositiveInteger(value) {
|
|
@@ -552,7 +1251,7 @@ function parseTemperature(value) {
|
|
|
552
1251
|
const parsed = Number(value);
|
|
553
1252
|
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 2 ? parsed : null;
|
|
554
1253
|
}
|
|
555
|
-
function buildRunConfigUpdate(laneType, draft) {
|
|
1254
|
+
function buildRunConfigUpdate(laneType, draft, recordMode, recordCategories) {
|
|
556
1255
|
const rpmLimit = parsePositiveInteger(draft.rpmLimit);
|
|
557
1256
|
const tpmLimit = parsePositiveInteger(draft.tpmLimit);
|
|
558
1257
|
const concurrency = parsePositiveInteger(draft.concurrency);
|
|
@@ -561,19 +1260,41 @@ function buildRunConfigUpdate(laneType, draft) {
|
|
|
561
1260
|
const temperature = parseTemperature(draft.temperature);
|
|
562
1261
|
if (temperature === null)
|
|
563
1262
|
return null;
|
|
1263
|
+
const baseRunConfig = {
|
|
1264
|
+
rpmLimit,
|
|
1265
|
+
tpmLimit,
|
|
1266
|
+
concurrency,
|
|
1267
|
+
temperature,
|
|
1268
|
+
};
|
|
1269
|
+
if (laneType === 'canary') {
|
|
1270
|
+
return {
|
|
1271
|
+
laneType,
|
|
1272
|
+
modelId: draft.modelId || undefined,
|
|
1273
|
+
recordMode,
|
|
1274
|
+
recordCategories,
|
|
1275
|
+
runConfig: {
|
|
1276
|
+
...baseRunConfig,
|
|
1277
|
+
...(draft.stopConditions ? { stopConditions: draft.stopConditions } : {}),
|
|
1278
|
+
},
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
564
1281
|
return {
|
|
565
1282
|
laneType,
|
|
566
1283
|
modelId: draft.modelId || undefined,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
concurrency,
|
|
571
|
-
temperature,
|
|
572
|
-
},
|
|
1284
|
+
recordMode,
|
|
1285
|
+
recordCategories,
|
|
1286
|
+
runConfig: baseRunConfig,
|
|
573
1287
|
};
|
|
574
1288
|
}
|
|
575
1289
|
function runConfigSignature(input) {
|
|
576
|
-
return input
|
|
1290
|
+
return input
|
|
1291
|
+
? JSON.stringify({
|
|
1292
|
+
modelId: input.modelId ?? null,
|
|
1293
|
+
recordMode: input.recordMode ?? null,
|
|
1294
|
+
recordCategories: input.recordCategories ?? null,
|
|
1295
|
+
runConfig: input.runConfig,
|
|
1296
|
+
})
|
|
1297
|
+
: null;
|
|
577
1298
|
}
|
|
578
1299
|
function modelOptionFromProjectModel(model) {
|
|
579
1300
|
return {
|
|
@@ -613,16 +1334,17 @@ function formatModelLimitValue(value, labels) {
|
|
|
613
1334
|
function modelOptionLabel(option) {
|
|
614
1335
|
return [option.name, option.providerModelId].filter(Boolean).join(' · ');
|
|
615
1336
|
}
|
|
616
|
-
function RuntimeConfigEditor({ laneType, config, currentModelId, currentModelName, models, modelsLoading, labels, canEdit, pending, onUpdateRunConfig, }) {
|
|
1337
|
+
function RuntimeConfigEditor({ laneType, config, currentModelId, currentModelName, recordMode, models, modelsLoading, labels, canEdit, pending, onUpdateRunConfig, hideModel = false, className, }) {
|
|
617
1338
|
const modelOptions = useMemo(() => buildRuntimeModelOptions(models, currentModelId, currentModelName), [currentModelId, currentModelName, models]);
|
|
618
1339
|
const [draft, setDraft] = useState(() => runConfigDraftFromRecord(config, currentModelId));
|
|
619
1340
|
const [error, setError] = useState(null);
|
|
620
|
-
const initialSignature = runConfigSignature(buildRunConfigUpdate(laneType, runConfigDraftFromRecord(config, currentModelId)));
|
|
1341
|
+
const initialSignature = runConfigSignature(buildRunConfigUpdate(laneType, runConfigDraftFromRecord(config, currentModelId), recordMode));
|
|
621
1342
|
const [savedSignature, setSavedSignature] = useState(initialSignature);
|
|
622
|
-
const nextUpdate = buildRunConfigUpdate(laneType, draft);
|
|
1343
|
+
const nextUpdate = buildRunConfigUpdate(laneType, draft, recordMode);
|
|
623
1344
|
const nextSignature = runConfigSignature(nextUpdate);
|
|
624
|
-
const
|
|
625
|
-
const
|
|
1345
|
+
const hasDraftChange = nextSignature !== savedSignature;
|
|
1346
|
+
const showSave = canEdit && Boolean(onUpdateRunConfig) && hasDraftChange;
|
|
1347
|
+
const canSubmit = showSave && !pending;
|
|
626
1348
|
const selectedModel = modelOptions.find((model) => model.id === draft.modelId) ?? null;
|
|
627
1349
|
const rpmModelLimit = selectedModel ? labels.modelLimit(formatModelLimitValue(selectedModel.rpmLimit, labels)) : '—';
|
|
628
1350
|
const tpmModelLimit = selectedModel ? labels.modelLimit(formatModelLimitValue(selectedModel.tpmLimit, labels)) : '—';
|
|
@@ -648,14 +1370,125 @@ function RuntimeConfigEditor({ laneType, config, currentModelId, currentModelNam
|
|
|
648
1370
|
setError(getApiErrorMessage(saveError) ?? labels.runConfigUpdateFailed);
|
|
649
1371
|
}
|
|
650
1372
|
}
|
|
651
|
-
return (_jsxs("section", { className:
|
|
1373
|
+
return (_jsxs("section", { className: cn('rounded-lg border bg-card', className), children: [_jsxs("div", { className: "flex min-h-10 items-center justify-between gap-2 border-b px-3 py-2", children: [_jsx("div", { className: "text-[12px] font-semibold", children: labels.runConfigTitle }), showSave ? (_jsxs(Button, { type: "button", size: "sm", className: "h-7 px-2 text-[11.5px]", onClick: saveRunConfig, disabled: !canSubmit, children: [_jsx(Save, { className: "size-3.5" }), pending ? labels.savePending : labels.save] })) : null] }), _jsxs("div", { className: "space-y-3 p-3", children: [hideModel ? null : (_jsxs("div", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: labels.model }), _jsxs(Select, { value: draft.modelId, onValueChange: (value) => setField('modelId', value), disabled: !canEdit || pending || modelOptions.length === 0, children: [_jsx(SelectTrigger, { className: "mt-1 h-8 bg-background text-xs", "aria-label": labels.model, children: _jsx(SelectValue, { placeholder: modelsLoading ? labels.loading : labels.runConfigModelUnavailable }) }), _jsx(SelectContent, { children: modelOptions.map((option) => (_jsx(SelectItem, { value: option.id, children: modelOptionLabel(option) }, option.id))) })] })] })), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(RuntimeNumberField, { label: labels.tpmLimit, hint: tpmModelLimit, value: draft.tpmLimit, min: 1, step: 1, disabled: !canEdit || pending, onChange: (value) => setField('tpmLimit', value), onCommit: saveRunConfig }), _jsx(RuntimeNumberField, { label: labels.rpmLimit, hint: rpmModelLimit, value: draft.rpmLimit, min: 1, step: 1, disabled: !canEdit || pending, onChange: (value) => setField('rpmLimit', value), onCommit: saveRunConfig }), _jsx(RuntimeNumberField, { label: labels.concurrency, value: draft.concurrency, min: 1, step: 1, disabled: !canEdit || pending, onChange: (value) => setField('concurrency', value), onCommit: saveRunConfig }), _jsx(RuntimeNumberField, { label: labels.temperature, value: draft.temperature, min: 0, max: 2, step: 0.1, disabled: !canEdit || pending, onChange: (value) => setField('temperature', value), onCommit: saveRunConfig })] }), error ? _jsx("p", { className: "text-[12px] text-destructive", children: error }) : null] })] }));
|
|
652
1374
|
}
|
|
653
1375
|
function RuntimeNumberField({ label, hint, value, min, max, step, disabled, onChange, onCommit, }) {
|
|
654
|
-
return (_jsxs("label", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsxs("span", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: label }), hint ? _jsx("span", { className: "truncate text-right text-[10.5px] text-muted-foreground", children: hint }) : null] }), _jsx(Input, { type: "number", min: min, max: max, step: step, value: value, onChange: (event) => onChange(event.target.value),
|
|
1376
|
+
return (_jsxs("label", { className: "rounded-md border bg-muted/40 px-3 py-2", children: [_jsxs("span", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: label }), hint ? _jsx("span", { className: "truncate text-right text-[10.5px] text-muted-foreground", children: hint }) : null] }), _jsx(Input, { type: "number", min: min, max: max, step: step, value: value, onChange: (event) => onChange(event.target.value), onKeyDown: (event) => {
|
|
655
1377
|
if (event.key === 'Enter')
|
|
656
1378
|
onCommit();
|
|
657
1379
|
}, disabled: disabled, "aria-label": label, className: "mt-1 h-8 font-mono text-xs" })] }));
|
|
658
1380
|
}
|
|
1381
|
+
function resourceDetailHref(resource, id, versionId) {
|
|
1382
|
+
if (!id)
|
|
1383
|
+
return null;
|
|
1384
|
+
const encodedId = encodeURIComponent(id);
|
|
1385
|
+
if (resource === 'model')
|
|
1386
|
+
return `/models/${encodedId}/edit`;
|
|
1387
|
+
const query = versionId ? `?version=${encodeURIComponent(versionId)}` : '';
|
|
1388
|
+
return `/prompts/${encodedId}${query}`;
|
|
1389
|
+
}
|
|
1390
|
+
function LinkedInspectorValue({ href, value }) {
|
|
1391
|
+
const display = toDisplayValue(value);
|
|
1392
|
+
if (!href || !value) {
|
|
1393
|
+
return (_jsx("span", { className: "block truncate", title: display, children: display }));
|
|
1394
|
+
}
|
|
1395
|
+
return (_jsx(Link, { href: href, className: "block truncate text-primary underline-offset-2 hover:underline", title: display, children: display }));
|
|
1396
|
+
}
|
|
1397
|
+
function editableRecordMode(mode) {
|
|
1398
|
+
return mode === 'correct_only' ? 'selected_categories' : mode;
|
|
1399
|
+
}
|
|
1400
|
+
function normalizeRecordCategories(categories, options) {
|
|
1401
|
+
const allowed = new Set(options);
|
|
1402
|
+
const seen = new Set();
|
|
1403
|
+
const normalized = [];
|
|
1404
|
+
for (const category of categories) {
|
|
1405
|
+
const trimmed = category.trim();
|
|
1406
|
+
if (!trimmed || seen.has(trimmed))
|
|
1407
|
+
continue;
|
|
1408
|
+
if (allowed.size > 0 && !allowed.has(trimmed))
|
|
1409
|
+
continue;
|
|
1410
|
+
seen.add(trimmed);
|
|
1411
|
+
normalized.push(trimmed);
|
|
1412
|
+
}
|
|
1413
|
+
return normalized;
|
|
1414
|
+
}
|
|
1415
|
+
function recordSettingsSignature(mode, categories) {
|
|
1416
|
+
return JSON.stringify({
|
|
1417
|
+
mode: editableRecordMode(mode),
|
|
1418
|
+
categories: editableRecordMode(mode) === 'selected_categories' ? [...categories].sort() : [],
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
function recordCategoriesDraftFromState(mode, categories, options) {
|
|
1422
|
+
if (options.length === 0)
|
|
1423
|
+
return [];
|
|
1424
|
+
const normalized = normalizeRecordCategories(categories, options);
|
|
1425
|
+
return editableRecordMode(mode) === 'selected_categories' && normalized.length > 0 ? normalized : options;
|
|
1426
|
+
}
|
|
1427
|
+
function recordModeFromDraftCategories(categories, options) {
|
|
1428
|
+
if (options.length === 0 || categories.length === options.length)
|
|
1429
|
+
return 'all';
|
|
1430
|
+
return 'selected_categories';
|
|
1431
|
+
}
|
|
1432
|
+
function recordCategoryOptionsFromSnapshot(snapshot, savedCategories) {
|
|
1433
|
+
const outputSchema = snapshot['outputSchema'] ?? null;
|
|
1434
|
+
const derived = deriveClassificationOptionsFromPromptOutputSchema(outputSchema);
|
|
1435
|
+
return Array.from(new Set([...derived, ...savedCategories].map((category) => category.trim()).filter(Boolean)));
|
|
1436
|
+
}
|
|
1437
|
+
function RecordCategorySelector({ value, options, disabled, labels, onChange, }) {
|
|
1438
|
+
const allSelected = options.length > 0 && options.every((option) => value.includes(option));
|
|
1439
|
+
if (options.length === 0) {
|
|
1440
|
+
return (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground", children: labels.recordCategoriesEmpty }));
|
|
1441
|
+
}
|
|
1442
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex min-h-7 items-center justify-between gap-2", children: [_jsx("span", { className: "text-[11.5px] text-muted-foreground", children: labels.recordCategoriesSelected(value.length) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 px-2 text-[11.5px]", disabled: disabled, onClick: () => onChange(allSelected ? [] : options), children: allSelected ? labels.recordCategoriesClear : labels.recordCategoriesSelectAll })] }), _jsx("div", { className: "flex flex-wrap gap-1.5 rounded-md border bg-muted/30 p-2", children: options.map((option) => {
|
|
1443
|
+
const selected = value.includes(option);
|
|
1444
|
+
return (_jsxs("button", { type: "button", disabled: disabled, "aria-pressed": selected, onClick: () => onChange(selected ? value.filter((item) => item !== option) : [...value, option]), className: cn('inline-flex h-7 min-w-0 items-center gap-1 rounded-full border px-2.5 text-[11.5px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60', selected
|
|
1445
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
1446
|
+
: 'border-border bg-background text-muted-foreground hover:bg-muted'), children: [selected ? _jsx(Check, { className: "size-3", strokeWidth: 3 }) : null, _jsx("span", { className: "max-w-[140px] truncate", children: option })] }, option));
|
|
1447
|
+
}) })] }));
|
|
1448
|
+
}
|
|
1449
|
+
function RecordModeMetadataCard({ laneType, config, modelId, recordMode, recordCategories, promptVersionSnapshot, createdAt, updatedAt, labels, formatDateTimeOrDash, canEdit, pending, onUpdateRunConfig, }) {
|
|
1450
|
+
const recordCategoryOptions = useMemo(() => recordCategoryOptionsFromSnapshot(promptVersionSnapshot, recordCategories), [promptVersionSnapshot, recordCategories]);
|
|
1451
|
+
const initialCategories = recordCategoriesDraftFromState(recordMode, recordCategories, recordCategoryOptions);
|
|
1452
|
+
const [draftRecordCategories, setDraftRecordCategories] = useState(initialCategories);
|
|
1453
|
+
const initialMode = recordModeFromDraftCategories(initialCategories, recordCategoryOptions);
|
|
1454
|
+
const initialRecordCategories = initialMode === 'selected_categories' ? initialCategories : [];
|
|
1455
|
+
const [savedSignature, setSavedSignature] = useState(recordSettingsSignature(initialMode, initialRecordCategories));
|
|
1456
|
+
const [error, setError] = useState(null);
|
|
1457
|
+
const normalizedDraftCategories = normalizeRecordCategories(draftRecordCategories, recordCategoryOptions);
|
|
1458
|
+
const draftRecordMode = recordModeFromDraftCategories(normalizedDraftCategories, recordCategoryOptions);
|
|
1459
|
+
const draftRecordCategoriesForSave = draftRecordMode === 'selected_categories' ? normalizedDraftCategories : [];
|
|
1460
|
+
const nextUpdate = buildRunConfigUpdate(laneType, runConfigDraftFromRecord(config, modelId), draftRecordMode, draftRecordCategoriesForSave);
|
|
1461
|
+
const nextSignature = recordSettingsSignature(draftRecordMode, draftRecordCategoriesForSave);
|
|
1462
|
+
const hasRecordModeDraft = nextSignature !== savedSignature;
|
|
1463
|
+
const showSave = canEdit && Boolean(onUpdateRunConfig) && hasRecordModeDraft;
|
|
1464
|
+
const canSubmit = showSave && !pending;
|
|
1465
|
+
async function saveRecordMode() {
|
|
1466
|
+
if (!canEdit || !onUpdateRunConfig || pending || !hasRecordModeDraft)
|
|
1467
|
+
return;
|
|
1468
|
+
if (!nextUpdate) {
|
|
1469
|
+
setError(labels.runConfigInvalid);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
setError(null);
|
|
1473
|
+
try {
|
|
1474
|
+
await onUpdateRunConfig(nextUpdate);
|
|
1475
|
+
setSavedSignature(nextSignature);
|
|
1476
|
+
}
|
|
1477
|
+
catch (saveError) {
|
|
1478
|
+
setError(getApiErrorMessage(saveError) ?? labels.runConfigUpdateFailed);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsxs("div", { className: "flex min-h-10 items-center justify-between gap-2 border-b px-3 py-2", children: [_jsx("div", { className: "text-[12px] font-semibold", children: labels.recordModeTitle }), showSave ? (_jsxs(Button, { type: "button", size: "sm", className: "h-7 px-2 text-[11.5px]", onClick: saveRecordMode, disabled: !canSubmit, children: [_jsx(Save, { className: "size-3.5" }), pending ? labels.savePending : labels.save] })) : null] }), _jsxs("div", { className: "space-y-2 p-3", children: [_jsxs("div", { className: "grid grid-cols-[112px_minmax(0,1fr)] gap-2 text-[12px]", children: [_jsx("span", { className: "pt-1.5 text-muted-foreground", children: labels.recordCategories }), _jsx(RecordCategorySelector, { value: normalizedDraftCategories, options: recordCategoryOptions, disabled: !canEdit || pending, labels: labels, onChange: (next) => {
|
|
1482
|
+
setDraftRecordCategories(next);
|
|
1483
|
+
setError(null);
|
|
1484
|
+
} })] }), _jsx(InspectorRowView, { row: { label: labels.createdAt, value: formatDateTimeOrDash(createdAt) } }), _jsx(InspectorRowView, { row: { label: labels.updatedAt, value: formatDateTimeOrDash(updatedAt) } }), error ? _jsx("p", { className: "text-[12px] text-destructive", children: error }) : null] })] }));
|
|
1485
|
+
}
|
|
1486
|
+
function formatPromptNameWithVersion(promptName, versionLabel) {
|
|
1487
|
+
return [promptName, versionLabel].filter((part) => Boolean(part)).join(' - ');
|
|
1488
|
+
}
|
|
1489
|
+
function ReleaseLaneDetailCards({ tone, labels, identityRows, runtimeEditor, metadataCard, routeEditor, }) {
|
|
1490
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mt-4 space-y-3", children: [_jsx(InspectorRowsCard, { rows: identityRows, tone: tone, title: labels.laneIdentityTitle }), runtimeEditor, metadataCard] }), routeEditor] }));
|
|
1491
|
+
}
|
|
659
1492
|
function getOutputConnector(line, nodeId) {
|
|
660
1493
|
if (!nodeId.startsWith('output-'))
|
|
661
1494
|
return null;
|
|
@@ -670,7 +1503,7 @@ function getOutputScope(line, connectorId, labels) {
|
|
|
670
1503
|
return ([inProduction ? labels.productionScope : null, inCanary ? labels.canaryScope : null].filter(Boolean).join(' + ') ||
|
|
671
1504
|
null);
|
|
672
1505
|
}
|
|
673
|
-
function buildInspectorDetail({ line, selectedNodeId, labels, formatDateTimeOrDash, onUpdateTrafficRatio, trafficRatioPending, onUpdateRunConfig, runConfigPending, models, modelsLoading, onAddCanary, }) {
|
|
1506
|
+
function buildInspectorDetail({ line, selectedNodeId, labels, formatDateTimeOrDash, onUpdateTrafficRatio, trafficRatioPending, onUpdateRunConfig, runConfigPending, onUpdateOutputRoute, outputRoutePending, onUpdateInputRoute, inputRoutePending, models, modelsLoading, outputConnectors, outputConnectorsLoading, onAddCanary, onStopCanary, onPromoteCanary, canaryActionPending, }) {
|
|
674
1507
|
if (selectedNodeId === 'upstream') {
|
|
675
1508
|
return {
|
|
676
1509
|
icon: 'upstream',
|
|
@@ -684,6 +1517,7 @@ function buildInspectorDetail({ line, selectedNodeId, labels, formatDateTimeOrDa
|
|
|
684
1517
|
{ label: labels.direction, value: labels.inputDirection, mono: false },
|
|
685
1518
|
{ label: labels.prompt, value: line.promptName },
|
|
686
1519
|
],
|
|
1520
|
+
content: _jsx(UpstreamInputFields, { line: line, labels: labels }),
|
|
687
1521
|
};
|
|
688
1522
|
}
|
|
689
1523
|
if (selectedNodeId === 'input-route') {
|
|
@@ -693,94 +1527,79 @@ function buildInspectorDetail({ line, selectedNodeId, labels, formatDateTimeOrDa
|
|
|
693
1527
|
tone: 'neutral',
|
|
694
1528
|
rows: [],
|
|
695
1529
|
hideSummary: true,
|
|
696
|
-
content: (_jsx(TrafficRatioControl, { line: line, labels: labels, onUpdateTrafficRatio: onUpdateTrafficRatio, pending: trafficRatioPending }, `${line.canary?.id ?? 'no-canary'}:${line.canary?.trafficRatio ?? 0}`)),
|
|
697
|
-
blocks: [
|
|
698
|
-
{
|
|
699
|
-
title: labels.fieldMapping,
|
|
700
|
-
body: getVariableMappingBody(line, labels.fieldMappingEmpty),
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
title: labels.filterRules,
|
|
704
|
-
body: getFilterRulesBody(line, labels),
|
|
705
|
-
},
|
|
706
|
-
],
|
|
1530
|
+
content: (_jsx(_Fragment, { children: _jsx(TrafficRatioControl, { line: line, labels: labels, onUpdateTrafficRatio: onUpdateTrafficRatio, pending: trafficRatioPending }, `${line.canary?.id ?? 'no-canary'}:${line.canary?.trafficRatio ?? 0}`) })),
|
|
707
1531
|
};
|
|
708
1532
|
}
|
|
709
1533
|
if (selectedNodeId === 'output-route') {
|
|
710
1534
|
return {
|
|
711
1535
|
icon: 'router',
|
|
712
1536
|
label: labels.outputRoute,
|
|
713
|
-
title: labels.outputRouteTitle,
|
|
714
|
-
subtitle: labels.outputMapping,
|
|
715
1537
|
tone: 'neutral',
|
|
716
|
-
rows: [
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
value: line.outputConnectors.map((connector) => connector.name).join(', ') || null,
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
label: labels.productionScope,
|
|
723
|
-
value: line.production?.outputConnectors?.length ? String(line.production.outputConnectors.length) : '0',
|
|
724
|
-
},
|
|
725
|
-
{
|
|
726
|
-
label: labels.canaryScope,
|
|
727
|
-
value: line.canary?.outputConnectors.length ? String(line.canary.outputConnectors.length) : '0',
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
|
-
blocks: [
|
|
731
|
-
{
|
|
732
|
-
title: labels.outputMapping,
|
|
733
|
-
body: getOutputMappingBody(line, labels),
|
|
734
|
-
},
|
|
735
|
-
],
|
|
1538
|
+
rows: [],
|
|
1539
|
+
hideSummary: true,
|
|
1540
|
+
content: (_jsxs(_Fragment, { children: [_jsx(OutputRouteSummaryCards, { line: line, outputConnectors: outputConnectors, labels: labels }), _jsx(OutputRouteControl, { line: line, outputConnectors: outputConnectors, outputConnectorsLoading: outputConnectorsLoading, labels: labels, pending: outputRoutePending, onUpdateOutputRoute: onUpdateOutputRoute }, `${line.production?.currentEvent?.id ?? 'no-production'}:${line.canary?.id ?? 'no-canary'}`)] })),
|
|
736
1541
|
};
|
|
737
1542
|
}
|
|
738
1543
|
if (selectedNodeId === 'production') {
|
|
739
1544
|
const event = line.production?.currentEvent ?? null;
|
|
1545
|
+
const inputRouteLane = getInputRouteLane(line, labels, 'production');
|
|
1546
|
+
const productionCanEditConfig = event?.status === 'running';
|
|
1547
|
+
const routeEditor = inputRouteLane ? (_jsx(InputRouteLaneEditor, { lane: inputRouteLane, labels: labels, pending: inputRoutePending, onUpdateInputRoute: onUpdateInputRoute }, `input-route:${inputRouteLane.laneType}:${inputRouteLane.eventId}:${inputRouteLane.updatedAt ?? ''}`)) : undefined;
|
|
740
1548
|
return {
|
|
741
1549
|
icon: 'production',
|
|
742
1550
|
label: labels.production,
|
|
743
1551
|
title: line.productionVersionLabel ?? labels.noProduction,
|
|
744
1552
|
subtitle: line.productionModelName ?? labels.noModel,
|
|
745
1553
|
tone: event ? 'production' : 'muted',
|
|
746
|
-
rows: [
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1554
|
+
rows: [],
|
|
1555
|
+
hideSummary: true,
|
|
1556
|
+
content: event ? (_jsx(ReleaseLaneDetailCards, { tone: "production", labels: labels, identityRows: [
|
|
1557
|
+
{
|
|
1558
|
+
label: labels.model,
|
|
1559
|
+
valueNode: (_jsx(LinkedInspectorValue, { href: resourceDetailHref('model', event.modelId), value: line.productionModelName })),
|
|
1560
|
+
mono: false,
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
label: labels.prompt,
|
|
1564
|
+
valueNode: (_jsx(LinkedInspectorValue, { href: resourceDetailHref('prompt', event.promptId ?? line.promptId, event.promptVersionId), value: formatPromptNameWithVersion(line.promptName, line.productionVersionLabel) })),
|
|
1565
|
+
mono: false,
|
|
1566
|
+
},
|
|
1567
|
+
], runtimeEditor: _jsx(RuntimeConfigEditor, { laneType: "production", config: event.runConfig, currentModelId: event.modelId, currentModelName: line.productionModelName, recordMode: event.recordMode, models: models, modelsLoading: modelsLoading, labels: labels, canEdit: productionCanEditConfig, pending: runConfigPending, onUpdateRunConfig: onUpdateRunConfig, hideModel: true }, `production:${event.id}:${event.updatedAt}`), metadataCard: _jsx(RecordModeMetadataCard, { laneType: "production", config: event.runConfig, modelId: event.modelId, recordMode: event.recordMode, recordCategories: event.recordCategories, promptVersionSnapshot: event.promptVersionSnapshot, createdAt: event.createdAt, updatedAt: event.updatedAt, labels: labels, formatDateTimeOrDash: formatDateTimeOrDash, canEdit: productionCanEditConfig, pending: runConfigPending, onUpdateRunConfig: onUpdateRunConfig }, `production-meta:${event.id}:${event.recordMode}:${event.recordCategories.join('|')}:${event.updatedAt}`), routeEditor: routeEditor })) : (_jsx(InspectorRowsCard, { className: "mt-4", rows: [{ label: labels.status, value: labels.noProduction }] })),
|
|
758
1568
|
};
|
|
759
1569
|
}
|
|
760
1570
|
if (selectedNodeId === 'canary') {
|
|
761
1571
|
const canary = line.canary;
|
|
762
|
-
const
|
|
1572
|
+
const canCreateOrReplaceCanary = line.production?.currentEvent?.status === 'running' && Boolean(onAddCanary);
|
|
1573
|
+
const canShowAddCanarySlot = !canary && canCreateOrReplaceCanary;
|
|
1574
|
+
const canaryCanEditConfig = isAdjustableCanary(canary);
|
|
1575
|
+
const inputRouteLane = getInputRouteLane(line, labels, 'canary');
|
|
1576
|
+
const routeEditor = inputRouteLane ? (_jsx(InputRouteLaneEditor, { lane: inputRouteLane, labels: labels, pending: inputRoutePending, onUpdateInputRoute: onUpdateInputRoute }, `input-route:${inputRouteLane.laneType}:${inputRouteLane.eventId}:${inputRouteLane.updatedAt ?? ''}`)) : undefined;
|
|
763
1577
|
return {
|
|
764
|
-
icon:
|
|
1578
|
+
icon: canShowAddCanarySlot ? 'addCanary' : canaryTopologyIcon(canary),
|
|
765
1579
|
label: labels.canary,
|
|
766
|
-
title:
|
|
1580
|
+
title: canShowAddCanarySlot ? labels.addCanary : (line.canaryVersionLabel ?? labels.noCanary),
|
|
767
1581
|
subtitle: line.canaryModelName ?? labels.readyForCandidate,
|
|
768
1582
|
tone: canary ? 'canary' : 'muted',
|
|
769
|
-
rows: [
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1583
|
+
rows: [],
|
|
1584
|
+
hideSummary: true,
|
|
1585
|
+
content: canary ? (_jsx(ReleaseLaneDetailCards, { tone: "canary", labels: labels, identityRows: [
|
|
1586
|
+
{
|
|
1587
|
+
label: labels.model,
|
|
1588
|
+
valueNode: (_jsx(LinkedInspectorValue, { href: resourceDetailHref('model', canary.modelId), value: line.canaryModelName })),
|
|
1589
|
+
mono: false,
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
label: labels.prompt,
|
|
1593
|
+
valueNode: (_jsx(LinkedInspectorValue, { href: resourceDetailHref('prompt', canary.promptId ?? line.promptId, canary.promptVersionId), value: formatPromptNameWithVersion(line.promptName, line.canaryVersionLabel) })),
|
|
1594
|
+
mono: false,
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
label: labels.termination,
|
|
1598
|
+
value: formatCanaryStopConditions(canary.runConfig, labels),
|
|
1599
|
+
mono: false,
|
|
1600
|
+
},
|
|
1601
|
+
], runtimeEditor: _jsx(RuntimeConfigEditor, { laneType: "canary", config: canary.runConfig, currentModelId: canary.modelId, currentModelName: canary.modelName, recordMode: canary.recordMode, models: models, modelsLoading: modelsLoading, labels: labels, canEdit: canaryCanEditConfig, pending: runConfigPending, onUpdateRunConfig: onUpdateRunConfig, hideModel: true }, `canary:${canary.id}:${canary.updatedAt}`), metadataCard: _jsx(RecordModeMetadataCard, { laneType: "canary", config: canary.runConfig, modelId: canary.modelId, recordMode: canary.recordMode, recordCategories: canary.recordCategories, promptVersionSnapshot: readRecord(line.canaryPromptVersionSnapshot) ?? EMPTY_RECORD, createdAt: canary.createdAt, updatedAt: canary.updatedAt, labels: labels, formatDateTimeOrDash: formatDateTimeOrDash, canEdit: canaryCanEditConfig, pending: runConfigPending, onUpdateRunConfig: onUpdateRunConfig }, `canary-meta:${canary.id}:${canary.recordMode}:${canary.recordCategories.join('|')}:${canary.updatedAt}`), routeEditor: routeEditor })) : (_jsx(InspectorRowsCard, { className: "mt-4", rows: [{ label: labels.status, value: labels.noCanary }] })),
|
|
1602
|
+
headerAction: canary || canCreateOrReplaceCanary ? (_jsx(CanaryLaneActions, { canary: canary, labels: labels, pending: canaryActionPending || trafficRatioPending, onAddCanary: canCreateOrReplaceCanary ? onAddCanary : undefined, onStopCanary: onStopCanary, onPromoteCanary: onPromoteCanary })) : undefined,
|
|
784
1603
|
};
|
|
785
1604
|
}
|
|
786
1605
|
const connector = getOutputConnector(line, selectedNodeId);
|
|
@@ -800,14 +1619,18 @@ function buildInspectorDetail({ line, selectedNodeId, labels, formatDateTimeOrDa
|
|
|
800
1619
|
};
|
|
801
1620
|
}
|
|
802
1621
|
function InspectorRowView({ row }) {
|
|
803
|
-
return (_jsxs("div", { className: "grid grid-cols-[112px_minmax(0,1fr)] gap-2 text-[12px]", children: [_jsx("span", { className: "text-muted-foreground", children: row.label }), _jsx("span", { className: cn('min-w-0 truncate', row.mono !== false && 'font-mono'), title: toDisplayValue(row.value), children: toDisplayValue(row.value) })] }));
|
|
1622
|
+
return (_jsxs("div", { className: "grid grid-cols-[112px_minmax(0,1fr)] gap-2 text-[12px]", children: [_jsx("span", { className: "text-muted-foreground", children: row.label }), row.valueNode ? (_jsx("span", { className: cn('min-w-0', row.mono !== false && 'font-mono'), children: row.valueNode })) : (_jsx("span", { className: cn('min-w-0 truncate', row.mono !== false && 'font-mono'), title: toDisplayValue(row.value), children: toDisplayValue(row.value) }))] }));
|
|
1623
|
+
}
|
|
1624
|
+
function InspectorRowsCard({ rows, tone, title, action, className, }) {
|
|
1625
|
+
const token = tone ? TONE_STYLES[tone] : null;
|
|
1626
|
+
return (_jsxs("div", { className: cn('rounded-lg border bg-card', className), style: token ? { borderColor: token.bd } : undefined, children: [title || action ? (_jsxs("div", { className: "flex min-h-10 items-center justify-between gap-2 border-b px-3 py-2", children: [title ? _jsx("div", { className: "text-[12px] font-semibold", children: title }) : _jsx("span", {}), action] })) : null, _jsx("div", { className: "space-y-2 p-3", children: rows.map((row, index) => (_jsx(InspectorRowView, { row: row }, `${row.label}:${index}`))) })] }));
|
|
804
1627
|
}
|
|
805
1628
|
function TopologyInspector({ detail, labels, }) {
|
|
806
1629
|
const token = TONE_STYLES[detail.tone];
|
|
807
1630
|
const Icon = NODE_ICONS[detail.icon];
|
|
808
|
-
return (_jsxs("aside", { className: "flex min-h-[360px] flex-col bg-background/60 p-4", children: [_jsxs("div", { className: "mb-3 flex items-
|
|
1631
|
+
return (_jsxs("aside", { className: "flex h-[500px] min-h-[360px] max-h-[calc(100vh-180px)] flex-col overflow-hidden bg-background/60 p-4", children: [_jsxs("div", { className: "mb-3 flex min-h-7 shrink-0 items-start justify-between gap-3", children: [_jsx("h3", { className: "shrink-0 text-[14px] font-semibold", children: labels.inspector }), detail.headerAction] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto pr-1", children: [!detail.hideSummary ? (_jsx("div", { className: "rounded-lg border bg-card p-3", style: { borderColor: token.bd }, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-md border", style: { background: token.bg, borderColor: token.bd, color: token.dot }, children: _jsx(Icon, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: detail.label }), detail.title ? (_jsx("div", { className: "mt-1 truncate font-mono text-[13px] font-semibold", title: detail.title, children: detail.title })) : null, detail.subtitle ? (_jsx("div", { className: "mt-1 truncate text-[12px] text-muted-foreground", title: detail.subtitle, children: detail.subtitle })) : null] })] }) })) : null, detail.rows.length ? _jsx(InspectorRowsCard, { rows: detail.rows, className: "mt-4" }) : null, detail.content, detail.runtimeEditor, detail.blocks?.length ? (_jsx("div", { className: "mt-4 space-y-3", children: detail.blocks.map((block) => (_jsxs("section", { className: "rounded-lg border bg-card", children: [_jsx("div", { className: "border-b px-3 py-2 text-[12px] font-semibold", children: block.title }), _jsx("pre", { className: "max-h-36 overflow-auto whitespace-pre-wrap break-words p-3 font-mono text-[11.5px] leading-5 text-muted-foreground", children: block.body })] }, `${detail.label}:${block.title}`))) })) : null, detail.action ? _jsx("div", { className: "mt-4 flex justify-end", children: detail.action }) : null] })] }));
|
|
809
1632
|
}
|
|
810
|
-
export function ReleaseTopologyCanvas({ line, models = [], modelsLoading = false, onUpdateTrafficRatio, onUpdateRunConfig, onAddCanary, trafficRatioPending = false, runConfigPending = false, }) {
|
|
1633
|
+
export function ReleaseTopologyCanvas({ line, models = [], modelsLoading = false, outputConnectors = [], outputConnectorsLoading = false, onUpdateTrafficRatio, onUpdateRunConfig, onUpdateOutputRoute, onUpdateInputRoute, onAddCanary, onStopCanary, onPromoteCanary, trafficRatioPending = false, runConfigPending = false, outputRoutePending = false, inputRoutePending = false, canaryActionPending = false, }) {
|
|
811
1634
|
const labels = useTopologyLabels(line);
|
|
812
1635
|
const { formatDateTime } = useDateTimeFormatter();
|
|
813
1636
|
const formatDateTimeOrDash = useMemo(() => (value) => (value ? formatDateTime(value, { fallback: '—' }) : '—'), [formatDateTime]);
|
|
@@ -829,23 +1652,41 @@ export function ReleaseTopologyCanvas({ line, models = [], modelsLoading = false
|
|
|
829
1652
|
trafficRatioPending,
|
|
830
1653
|
onUpdateRunConfig,
|
|
831
1654
|
runConfigPending,
|
|
1655
|
+
onUpdateOutputRoute,
|
|
1656
|
+
outputRoutePending,
|
|
1657
|
+
onUpdateInputRoute,
|
|
1658
|
+
inputRoutePending,
|
|
832
1659
|
models,
|
|
833
1660
|
modelsLoading,
|
|
1661
|
+
outputConnectors,
|
|
1662
|
+
outputConnectorsLoading,
|
|
834
1663
|
onAddCanary,
|
|
1664
|
+
onStopCanary,
|
|
1665
|
+
onPromoteCanary,
|
|
1666
|
+
canaryActionPending,
|
|
835
1667
|
}), [
|
|
1668
|
+
canaryActionPending,
|
|
836
1669
|
labels,
|
|
837
1670
|
formatDateTimeOrDash,
|
|
838
1671
|
line,
|
|
839
1672
|
onAddCanary,
|
|
1673
|
+
onPromoteCanary,
|
|
1674
|
+
onStopCanary,
|
|
840
1675
|
onUpdateRunConfig,
|
|
841
1676
|
onUpdateTrafficRatio,
|
|
842
1677
|
models,
|
|
843
1678
|
modelsLoading,
|
|
844
1679
|
runConfigPending,
|
|
1680
|
+
onUpdateOutputRoute,
|
|
1681
|
+
onUpdateInputRoute,
|
|
1682
|
+
outputConnectors,
|
|
1683
|
+
outputConnectorsLoading,
|
|
1684
|
+
outputRoutePending,
|
|
1685
|
+
inputRoutePending,
|
|
845
1686
|
selectedNodeId,
|
|
846
1687
|
trafficRatioPending,
|
|
847
1688
|
]);
|
|
848
|
-
return (_jsxs("div", { className: "release-topology-canvas rounded-lg border bg-card", "data-testid": "release-topology-canvas", children: [_jsx("div", { className: "border-b px-4 py-3", children: _jsx("h2", { className: "text-[14px] font-semibold", children: labels.routeMeta }) }), _jsxs("div", { className: "grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_380px]", children: [_jsx("div", { className: "h-[500px] min-h-[380px] w-full border-b xl:border-b-0 xl:border-r", children: _jsxs(ReactFlow, { nodes: nodes, edges: topology.edges, nodeTypes: nodeTypes, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, onNodeClick: (_, node) => {
|
|
1689
|
+
return (_jsxs("div", { className: "release-topology-canvas rounded-lg border bg-card", "data-testid": "release-topology-canvas", children: [_jsx("div", { className: "border-b px-4 py-3", children: _jsx("h2", { className: "text-[14px] font-semibold", children: labels.routeMeta }) }), _jsxs("div", { className: "grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_380px]", children: [_jsx("div", { className: "h-[500px] min-h-[380px] w-full border-b xl:border-b-0 xl:border-r", children: _jsxs(ReactFlow, { nodes: nodes, edges: topology.edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, onNodeClick: (_, node) => {
|
|
849
1690
|
if (node.data.action === 'addCanary' && onAddCanary) {
|
|
850
1691
|
onAddCanary();
|
|
851
1692
|
return;
|