@proofhound/web-ui 0.1.8 → 0.1.10
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/components/navigation/link.d.ts +15 -0
- package/dist/components/navigation/link.d.ts.map +1 -0
- package/dist/components/navigation/link.js +23 -0
- package/dist/components/navigation/link.js.map +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js.map +1 -1
- 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/hooks/use-router.d.ts +17 -0
- package/dist/hooks/use-router.d.ts.map +1 -0
- package/dist/hooks/use-router.js +30 -0
- package/dist/hooks/use-router.js.map +1 -0
- 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/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/navigation-provider.d.ts +27 -0
- package/dist/providers/navigation-provider.d.ts.map +1 -0
- package/dist/providers/navigation-provider.js +17 -0
- package/dist/providers/navigation-provider.js.map +1 -0
- package/dist/providers/proofhound-web-provider.d.ts.map +1 -1
- package/dist/providers/proofhound-web-provider.js +2 -1
- package/dist/providers/proofhound-web-provider.js.map +1 -1
- package/dist/screens/annotations/annotation-detail-page.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-detail-page.js +4 -3
- package/dist/screens/annotations/annotation-detail-page.js.map +1 -1
- package/dist/screens/annotations/annotation-new-page.d.ts.map +1 -1
- package/dist/screens/annotations/annotation-new-page.js +216 -51
- 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 +3 -3
- package/dist/screens/annotations/annotations-list-page.js.map +1 -1
- package/dist/screens/connectors/connector-detail-page.d.ts.map +1 -1
- package/dist/screens/connectors/connector-detail-page.js +8 -4
- package/dist/screens/connectors/connector-detail-page.js.map +1 -1
- package/dist/screens/connectors/connector-form-page.js +3 -3
- 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 +8 -7
- 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 +29 -17
- package/dist/screens/dashboard/dashboard-screen.js.map +1 -1
- package/dist/screens/datasets/dataset-detail-page.js +1 -1
- package/dist/screens/datasets/dataset-detail-page.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/dataset-upload-page.js +2 -2
- package/dist/screens/datasets/dataset-upload-page.js.map +1 -1
- package/dist/screens/datasets/datasets-list-page.d.ts.map +1 -1
- package/dist/screens/datasets/datasets-list-page.js +37 -26
- package/dist/screens/datasets/datasets-list-page.js.map +1 -1
- package/dist/screens/experiments/experiment-detail-page.js +3 -3
- package/dist/screens/experiments/experiment-detail-page.js.map +1 -1
- package/dist/screens/experiments/experiment-new-page.js +2 -2
- package/dist/screens/experiments/experiment-new-page.js.map +1 -1
- package/dist/screens/experiments/experiments-list-page.d.ts.map +1 -1
- package/dist/screens/experiments/experiments-list-page.js +3 -2
- package/dist/screens/experiments/experiments-list-page.js.map +1 -1
- package/dist/screens/experiments/experiments-table.js +1 -1
- package/dist/screens/experiments/experiments-table.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/models/model-form-page.js +2 -2
- package/dist/screens/models/model-form-page.js.map +1 -1
- package/dist/screens/models/models-list-page.d.ts.map +1 -1
- package/dist/screens/models/models-list-page.js +3 -2
- package/dist/screens/models/models-list-page.js.map +1 -1
- package/dist/screens/optimizations/optimization-detail-page.js +2 -2
- package/dist/screens/optimizations/optimization-detail-page.js.map +1 -1
- package/dist/screens/optimizations/optimization-new-page.js +2 -2
- package/dist/screens/optimizations/optimization-new-page.js.map +1 -1
- package/dist/screens/optimizations/optimizations-list-page.d.ts.map +1 -1
- package/dist/screens/optimizations/optimizations-list-page.js +3 -2
- package/dist/screens/optimizations/optimizations-list-page.js.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.d.ts.map +1 -1
- package/dist/screens/prompts/prompt-detail-page.js +10 -10
- 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 +46 -21
- package/dist/screens/prompts/prompts-list-page.js.map +1 -1
- package/dist/screens/quick-start/quick-start-screen.js +2 -2
- package/dist/screens/quick-start/quick-start-screen.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 +1880 -325
- 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 +104 -68
- 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 +82 -33
- package/dist/screens/releases/releases-list-page.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,44 +1,50 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import Link from '
|
|
5
|
-
import { usePathname,
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Link } from '../../components/navigation/link';
|
|
5
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
6
|
+
import { useRouter } from '../../hooks/use-router';
|
|
6
7
|
import { useQueryClient } from '@tanstack/react-query';
|
|
7
|
-
import { Activity, AlertTriangle,
|
|
8
|
-
import
|
|
8
|
+
import { Activity, AlertTriangle, Archive, Check, ChevronDown, CircleDollarSign, Gauge, MoreHorizontal, Play, Plus, RotateCcw, ScrollText, Search, SlidersHorizontal, Square, Tag, Trash2, Timer, } from 'lucide-react';
|
|
9
|
+
import * as echarts from 'echarts/core';
|
|
10
|
+
import { LineChart } from 'echarts/charts';
|
|
11
|
+
import { DataZoomComponent, GridComponent, ToolboxComponent, TooltipComponent, } from 'echarts/components';
|
|
12
|
+
import { SVGRenderer } from 'echarts/renderers';
|
|
9
13
|
import { Main } from '@proofhound/ui/layout';
|
|
10
|
-
import { Button, DateRangeSegmented, resolveDateRangePreset, resolveRollingDateRangeValue, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, PlatformLoader, DetailPageSkeleton, ResourcePaginationFooter, Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow, cn, } from '@proofhound/ui';
|
|
11
|
-
import { useAnnotationTaskList } from '../../hooks';
|
|
14
|
+
import { Button, DateRangeSegmented, resolveDateRangePreset, resolveRollingDateRangeValue, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, PlatformLoader, Popover, PopoverContent, PopoverTrigger, DetailPageSkeleton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, ResourcePaginationFooter, Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow, cn, } from '@proofhound/ui';
|
|
12
15
|
import { useDelayedLoading } from '../../hooks';
|
|
13
16
|
import { useProjectModels } from '../../hooks';
|
|
14
|
-
import { useProductionReleaseHistory,
|
|
17
|
+
import { useConnectors, useProductionReleaseHistory, useStopCanaryRelease } from '../../hooks';
|
|
15
18
|
import { useProjectMonitoringStats, useProjectMonitoringTimeseries } from '../../hooks';
|
|
16
|
-
import { useReleaseLineEvents, useReleaseLineList, useUpdateReleaseLineRunConfig, useUpdateReleaseLineTrafficRatio, } from '../../hooks';
|
|
19
|
+
import { useArchiveReleaseLine, usePromoteReleaseLineCanary, useDeleteReleaseLine, useReleaseLineDeleteImpact, useReleaseLineEvents, useReleaseLineList, useRestoreReleaseLineHistoryToCanary, useRestoreReleaseLineHistoryToProduction, useStartReleaseLine, useStopReleaseLine, useUnarchiveReleaseLine, useUpdateReleaseLineInputRoute, useUpdateReleaseLineOutputRoute, useUpdateReleaseLineRunConfig, useUpdateReleaseLineTrafficRatio, } from '../../hooks';
|
|
17
20
|
import { useReleaseRunResults } from '../../hooks';
|
|
18
21
|
import { AUTO_REFRESH_INTERVAL_MS, useAutoRefresh, useDateTimeFormatter } from '../../hooks';
|
|
19
22
|
import { useI18n } from '../../i18n';
|
|
20
|
-
import { getReleaseLineId, getReleaseStopConfirmationName } from '../../lib';
|
|
23
|
+
import { getApiErrorMessage, getReleaseLineId, getReleaseStopConfirmationName } from '../../lib';
|
|
21
24
|
import { BigChartCard } from '../monitoring/big-chart-card';
|
|
22
|
-
import {
|
|
25
|
+
import { formatCount, formatPercent } from './release-line-ui';
|
|
23
26
|
import { ReleaseTopologyCanvas } from './release-topology-canvas';
|
|
27
|
+
echarts.use([LineChart, GridComponent, TooltipComponent, DataZoomComponent, ToolboxComponent, SVGRenderer]);
|
|
24
28
|
const DETAIL_TABS = [
|
|
25
29
|
{ value: 'monitoring', key: 'releases.detail.tab.monitoring' },
|
|
26
|
-
{ value: 'variants', key: 'releases.detail.tab.variants' },
|
|
27
30
|
{ value: 'results', key: 'releases.detail.tab.results' },
|
|
28
31
|
{ value: 'quality', key: 'releases.detail.tab.quality' },
|
|
29
32
|
{ value: 'history', key: 'releases.detail.tab.history' },
|
|
33
|
+
{ value: 'settings', key: 'releases.detail.tab.settings' },
|
|
30
34
|
];
|
|
31
35
|
const RESULT_COLUMNS = [
|
|
32
36
|
{ key: 'externalId', width: 'normal' },
|
|
33
37
|
{ key: 'input', width: 'wide' },
|
|
34
38
|
{ key: 'output', width: 'wide' },
|
|
35
39
|
{ key: 'source', width: 'compact' },
|
|
36
|
-
{ key: '
|
|
40
|
+
{ key: 'version', width: 'normal' },
|
|
37
41
|
{ key: 'latency', width: 'compact' },
|
|
38
42
|
{ key: 'tokens', width: 'compact' },
|
|
39
43
|
{ key: 'createdAt', width: 'normal' },
|
|
40
44
|
];
|
|
41
45
|
const RESULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
|
46
|
+
const HISTORY_INITIAL_GROUP_LIMIT = 8;
|
|
47
|
+
const HISTORY_GROUP_PAGE_SIZE = 8;
|
|
42
48
|
const EMPTY_BY_SOURCE = { prod: 0, canary: 0, iter: 0, exp: 0 };
|
|
43
49
|
const EMPTY_TIMESERIES_POINTS = [];
|
|
44
50
|
const RELEASE_MONITORING_SOURCE_KEYS = ['prod', 'canary'];
|
|
@@ -46,6 +52,21 @@ function useDateTimeOrDash() {
|
|
|
46
52
|
const { formatDateTime } = useDateTimeFormatter();
|
|
47
53
|
return useCallback((value) => (value ? formatDateTime(value, { fallback: '—' }) : '—'), [formatDateTime]);
|
|
48
54
|
}
|
|
55
|
+
const QUALITY_OVERALL_SCOPE = '__overall__';
|
|
56
|
+
const QUALITY_METRIC_OPTIONS = [
|
|
57
|
+
{ key: 'recall', labelKey: 'releases.detail.quality.metric.recall' },
|
|
58
|
+
{ key: 'precision', labelKey: 'releases.detail.quality.metric.precision' },
|
|
59
|
+
{ key: 'f1', labelKey: 'releases.detail.quality.metric.f1' },
|
|
60
|
+
{ key: 'accuracy', labelKey: 'releases.detail.quality.metric.accuracy' },
|
|
61
|
+
];
|
|
62
|
+
const QUALITY_SERIES_COLORS = [
|
|
63
|
+
'var(--primary)',
|
|
64
|
+
'var(--src-iter)',
|
|
65
|
+
'var(--status-pending-dot)',
|
|
66
|
+
'var(--destructive)',
|
|
67
|
+
'var(--foreground)',
|
|
68
|
+
'var(--muted-foreground)',
|
|
69
|
+
];
|
|
49
70
|
const COMPACT_METRIC_DOT_CLASS = {
|
|
50
71
|
default: 'bg-muted-foreground',
|
|
51
72
|
production: 'bg-[var(--src-prod-fg)]',
|
|
@@ -53,9 +74,6 @@ const COMPACT_METRIC_DOT_CLASS = {
|
|
|
53
74
|
success: 'bg-[var(--status-running-fg)]',
|
|
54
75
|
danger: 'bg-destructive',
|
|
55
76
|
};
|
|
56
|
-
const QUALITY_LINE_COLORS = {
|
|
57
|
-
score: 'var(--src-canary)',
|
|
58
|
-
};
|
|
59
77
|
function normalizeLineId(value) {
|
|
60
78
|
try {
|
|
61
79
|
return decodeURIComponent(value);
|
|
@@ -67,13 +85,17 @@ function normalizeLineId(value) {
|
|
|
67
85
|
function resolveTab(value) {
|
|
68
86
|
if (value === 'annotation')
|
|
69
87
|
return 'quality';
|
|
88
|
+
if (value === 'variants' || value === 'versions')
|
|
89
|
+
return 'history';
|
|
70
90
|
if (value === 'monitoring' ||
|
|
71
|
-
value === 'variants' ||
|
|
72
91
|
value === 'results' ||
|
|
73
92
|
value === 'quality' ||
|
|
74
|
-
value === 'history'
|
|
93
|
+
value === 'history' ||
|
|
94
|
+
value === 'settings') {
|
|
75
95
|
return value;
|
|
76
96
|
}
|
|
97
|
+
if (value === 'delete')
|
|
98
|
+
return 'settings';
|
|
77
99
|
return 'monitoring';
|
|
78
100
|
}
|
|
79
101
|
function createDefaultMonitoringRange() {
|
|
@@ -87,6 +109,17 @@ function createDefaultMonitoringRange() {
|
|
|
87
109
|
to: now.toISOString(),
|
|
88
110
|
};
|
|
89
111
|
}
|
|
112
|
+
function createDefaultResultDateRange() {
|
|
113
|
+
const preset = resolveDateRangePreset('d7');
|
|
114
|
+
if (preset)
|
|
115
|
+
return { preset: 'all', ...preset };
|
|
116
|
+
const now = new Date();
|
|
117
|
+
return {
|
|
118
|
+
preset: 'all',
|
|
119
|
+
from: new Date(now.getTime() - 7 * 24 * 60 * 60_000).toISOString(),
|
|
120
|
+
to: now.toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
90
123
|
function getMonitoringRefreshInterval(preset) {
|
|
91
124
|
if (preset === 'h1')
|
|
92
125
|
return AUTO_REFRESH_INTERVAL_MS;
|
|
@@ -96,6 +129,12 @@ function getMonitoringRefreshInterval(preset) {
|
|
|
96
129
|
return 60_000;
|
|
97
130
|
return false;
|
|
98
131
|
}
|
|
132
|
+
function isResultDateRangeApplied(value) {
|
|
133
|
+
return value.preset !== 'all';
|
|
134
|
+
}
|
|
135
|
+
function isResultDateRangeRolling(value) {
|
|
136
|
+
return value.preset !== 'all' && value.preset !== 'custom';
|
|
137
|
+
}
|
|
99
138
|
function hasRunningRelease(line) {
|
|
100
139
|
return line?.production?.currentEvent?.status === 'running' || line?.canary?.status === 'running';
|
|
101
140
|
}
|
|
@@ -156,29 +195,261 @@ function timeValue(value) {
|
|
|
156
195
|
const parsed = Date.parse(value);
|
|
157
196
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
158
197
|
}
|
|
159
|
-
function
|
|
198
|
+
function latestTime(values) {
|
|
199
|
+
const latest = values
|
|
200
|
+
.filter((value) => Boolean(value))
|
|
201
|
+
.sort((left, right) => timeValue(right) - timeValue(left))[0];
|
|
202
|
+
return latest ?? null;
|
|
203
|
+
}
|
|
204
|
+
function qualityVersionLane(kind) {
|
|
205
|
+
return kind === 'production' ? 'production' : 'canary';
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
function stringFromQualityRecord(record, key) {
|
|
211
|
+
const value = record[key];
|
|
212
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
213
|
+
}
|
|
214
|
+
function numberFromQualityRecord(record, key) {
|
|
215
|
+
const value = record[key];
|
|
216
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
217
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
218
|
+
return null;
|
|
219
|
+
if (parsed <= 1)
|
|
220
|
+
return toPercentPoint(parsed);
|
|
221
|
+
if (parsed <= 100)
|
|
222
|
+
return parsed;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function countFromQualityRecord(record, key) {
|
|
226
|
+
const value = record[key];
|
|
227
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
228
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
229
|
+
return null;
|
|
230
|
+
return Math.round(parsed);
|
|
231
|
+
}
|
|
232
|
+
function readReleaseQualityMetricSet(value) {
|
|
233
|
+
if (!isRecord(value))
|
|
234
|
+
return null;
|
|
235
|
+
const recall = numberFromQualityRecord(value, 'recall');
|
|
236
|
+
const precision = numberFromQualityRecord(value, 'precision');
|
|
237
|
+
const f1 = numberFromQualityRecord(value, 'f1');
|
|
238
|
+
const accuracy = numberFromQualityRecord(value, 'accuracy');
|
|
239
|
+
if (recall === null || precision === null || f1 === null || accuracy === null)
|
|
240
|
+
return null;
|
|
241
|
+
const sampleCount = countFromQualityRecord(value, 'sampleCount');
|
|
242
|
+
return {
|
|
243
|
+
recall,
|
|
244
|
+
precision,
|
|
245
|
+
f1,
|
|
246
|
+
accuracy,
|
|
247
|
+
sampleCount,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function readReleaseQualityScopes(metrics, overallLabel) {
|
|
251
|
+
const quality = isRecord(metrics?.['quality']) ? metrics['quality'] : null;
|
|
252
|
+
if (!quality)
|
|
253
|
+
return [];
|
|
254
|
+
const scopes = [];
|
|
255
|
+
const overallMetrics = readReleaseQualityMetricSet(quality['overall']);
|
|
256
|
+
if (overallMetrics) {
|
|
257
|
+
scopes.push({ scope: QUALITY_OVERALL_SCOPE, label: overallLabel, metrics: overallMetrics });
|
|
258
|
+
}
|
|
259
|
+
const rawScopes = quality['scopes'];
|
|
260
|
+
if (Array.isArray(rawScopes)) {
|
|
261
|
+
for (const item of rawScopes) {
|
|
262
|
+
if (!isRecord(item))
|
|
263
|
+
continue;
|
|
264
|
+
const scope = stringFromQualityRecord(item, 'key') ?? stringFromQualityRecord(item, 'label');
|
|
265
|
+
if (!scope)
|
|
266
|
+
continue;
|
|
267
|
+
const metricSet = readReleaseQualityMetricSet(item['metrics']) ?? readReleaseQualityMetricSet(item);
|
|
268
|
+
if (!metricSet)
|
|
269
|
+
continue;
|
|
270
|
+
scopes.push({
|
|
271
|
+
scope,
|
|
272
|
+
label: stringFromQualityRecord(item, 'label') ?? scope,
|
|
273
|
+
metrics: metricSet,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (isRecord(rawScopes)) {
|
|
278
|
+
for (const [scope, value] of Object.entries(rawScopes)) {
|
|
279
|
+
if (!isRecord(value))
|
|
280
|
+
continue;
|
|
281
|
+
const metricSet = readReleaseQualityMetricSet(value['metrics']) ?? readReleaseQualityMetricSet(value);
|
|
282
|
+
if (!metricSet)
|
|
283
|
+
continue;
|
|
284
|
+
scopes.push({
|
|
285
|
+
scope,
|
|
286
|
+
label: stringFromQualityRecord(value, 'label') ?? scope,
|
|
287
|
+
metrics: metricSet,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return scopes;
|
|
292
|
+
}
|
|
293
|
+
function buildReleaseQualityPoints(releaseEvents, overallLabel) {
|
|
160
294
|
const points = [];
|
|
161
|
-
for (const
|
|
162
|
-
if (
|
|
295
|
+
for (const event of releaseEvents) {
|
|
296
|
+
if (!event.releaseVersionId)
|
|
297
|
+
continue;
|
|
298
|
+
const releaseVersionKind = event.releaseVersionKind ?? (event.laneType === 'production' ? 'production' : 'candidate');
|
|
299
|
+
for (const scope of readReleaseQualityScopes(event.metrics, overallLabel)) {
|
|
163
300
|
points.push({
|
|
164
|
-
id:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
301
|
+
id: `${event.id}:${scope.scope}`,
|
|
302
|
+
eventId: event.id,
|
|
303
|
+
eventLabel: event.operation,
|
|
304
|
+
scope: scope.scope,
|
|
305
|
+
scopeLabel: scope.label,
|
|
306
|
+
releaseVersionId: event.releaseVersionId,
|
|
307
|
+
releaseVersionKind,
|
|
308
|
+
lane: qualityVersionLane(releaseVersionKind),
|
|
309
|
+
promptVersionLabel: event.promptVersionLabel ?? formatShortId(event.promptVersionId),
|
|
310
|
+
modelName: event.modelName ?? formatShortId(event.modelId),
|
|
311
|
+
releaseVersionLabel: event.releaseVersionLabel ?? formatShortId(event.releaseVersionId),
|
|
312
|
+
sampleCount: scope.metrics.sampleCount,
|
|
313
|
+
createdAt: event.createdAt,
|
|
314
|
+
updatedAt: event.updatedAt,
|
|
315
|
+
recall: scope.metrics.recall,
|
|
316
|
+
precision: scope.metrics.precision,
|
|
317
|
+
f1: scope.metrics.f1,
|
|
318
|
+
accuracy: scope.metrics.accuracy,
|
|
176
319
|
});
|
|
177
320
|
}
|
|
178
321
|
}
|
|
179
|
-
return points
|
|
180
|
-
|
|
181
|
-
|
|
322
|
+
return points.sort((left, right) => timeValue(left.createdAt) - timeValue(right.createdAt));
|
|
323
|
+
}
|
|
324
|
+
function buildQualityChartSeries(points, metrics, scopes) {
|
|
325
|
+
const sortedPoints = [...points].sort((left, right) => timeValue(left.createdAt) - timeValue(right.createdAt));
|
|
326
|
+
const eventOrder = new Map();
|
|
327
|
+
for (const point of sortedPoints) {
|
|
328
|
+
if (!eventOrder.has(point.eventId))
|
|
329
|
+
eventOrder.set(point.eventId, eventOrder.size + 1);
|
|
330
|
+
}
|
|
331
|
+
const series = [];
|
|
332
|
+
for (const scope of scopes) {
|
|
333
|
+
const scopedPoints = sortedPoints.filter((point) => point.scope === scope.id);
|
|
334
|
+
if (scopedPoints.length === 0)
|
|
335
|
+
continue;
|
|
336
|
+
for (const metric of metrics) {
|
|
337
|
+
const seriesId = `${scope.id}:${metric.id}`;
|
|
338
|
+
const seriesLabel = `${scope.label} · ${metric.label}`;
|
|
339
|
+
const color = QUALITY_SERIES_COLORS[series.length % QUALITY_SERIES_COLORS.length] ?? 'var(--primary)';
|
|
340
|
+
series.push({
|
|
341
|
+
id: seriesId,
|
|
342
|
+
label: seriesLabel,
|
|
343
|
+
color,
|
|
344
|
+
metric: metric.id,
|
|
345
|
+
metricLabel: metric.label,
|
|
346
|
+
scope: scope.id,
|
|
347
|
+
scopeLabel: scope.label,
|
|
348
|
+
points: scopedPoints.map((point) => ({
|
|
349
|
+
...point,
|
|
350
|
+
xIndex: eventOrder.get(point.eventId) ?? 0,
|
|
351
|
+
xLabel: `#${eventOrder.get(point.eventId) ?? 0}`,
|
|
352
|
+
metric: metric.id,
|
|
353
|
+
metricLabel: metric.label,
|
|
354
|
+
seriesId,
|
|
355
|
+
seriesLabel,
|
|
356
|
+
seriesColor: color,
|
|
357
|
+
value: point[metric.id],
|
|
358
|
+
})),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return series;
|
|
363
|
+
}
|
|
364
|
+
function buildQualityChartAxisData(series) {
|
|
365
|
+
const points = new Map();
|
|
366
|
+
for (const item of series) {
|
|
367
|
+
for (const point of item.points) {
|
|
368
|
+
points.set(point.eventId, point);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return [...points.values()].sort((left, right) => left.xIndex - right.xIndex);
|
|
372
|
+
}
|
|
373
|
+
function buildQualityVersionOptions(points) {
|
|
374
|
+
const versions = new Map();
|
|
375
|
+
for (const point of points) {
|
|
376
|
+
const existing = versions.get(point.releaseVersionId);
|
|
377
|
+
const latestAt = latestTime([existing?.latestAt, point.updatedAt ?? point.createdAt]);
|
|
378
|
+
if (existing) {
|
|
379
|
+
versions.set(point.releaseVersionId, {
|
|
380
|
+
...existing,
|
|
381
|
+
pointCount: existing.pointCount + 1,
|
|
382
|
+
latestAt,
|
|
383
|
+
});
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
versions.set(point.releaseVersionId, {
|
|
387
|
+
id: point.releaseVersionId,
|
|
388
|
+
label: point.releaseVersionLabel,
|
|
389
|
+
kind: point.releaseVersionKind,
|
|
390
|
+
promptVersion: point.promptVersionLabel,
|
|
391
|
+
model: point.modelName,
|
|
392
|
+
pointCount: 1,
|
|
393
|
+
latestAt,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return [...versions.values()].sort((left, right) => left.label.localeCompare(right.label, undefined, { numeric: true }));
|
|
397
|
+
}
|
|
398
|
+
function buildQualityScopeOptions(points) {
|
|
399
|
+
const scopes = new Map();
|
|
400
|
+
for (const point of points) {
|
|
401
|
+
scopes.set(point.scope, { id: point.scope, label: point.scopeLabel });
|
|
402
|
+
}
|
|
403
|
+
return [...scopes.values()].sort((left, right) => {
|
|
404
|
+
if (left.id === QUALITY_OVERALL_SCOPE)
|
|
405
|
+
return -1;
|
|
406
|
+
if (right.id === QUALITY_OVERALL_SCOPE)
|
|
407
|
+
return 1;
|
|
408
|
+
return left.label.localeCompare(right.label, undefined, { numeric: true });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function resolveActiveQualityScopes(selectedScopes, options) {
|
|
412
|
+
if (selectedScopes) {
|
|
413
|
+
const selectedSet = new Set(selectedScopes);
|
|
414
|
+
const activeOptions = options.filter((option) => selectedSet.has(option.id));
|
|
415
|
+
if (activeOptions.length > 0)
|
|
416
|
+
return activeOptions;
|
|
417
|
+
}
|
|
418
|
+
const fallback = options.find((option) => option.id === QUALITY_OVERALL_SCOPE) ?? options[0] ?? null;
|
|
419
|
+
return fallback ? [fallback] : [];
|
|
420
|
+
}
|
|
421
|
+
function resolveActiveQualityMetrics(selectedMetrics, options) {
|
|
422
|
+
if (selectedMetrics) {
|
|
423
|
+
const selectedSet = new Set(selectedMetrics);
|
|
424
|
+
const activeOptions = options.filter((option) => selectedSet.has(option.id));
|
|
425
|
+
if (activeOptions.length > 0)
|
|
426
|
+
return activeOptions;
|
|
427
|
+
}
|
|
428
|
+
return options.filter((option) => option.id === 'f1');
|
|
429
|
+
}
|
|
430
|
+
function filterQualityPoints(points, versionIds, scopes) {
|
|
431
|
+
if (scopes.length === 0)
|
|
432
|
+
return [];
|
|
433
|
+
const versionSet = new Set(versionIds);
|
|
434
|
+
const scopeSet = new Set(scopes.map((scope) => scope.id));
|
|
435
|
+
return points.filter((point) => versionSet.has(point.releaseVersionId) && scopeSet.has(point.scope));
|
|
436
|
+
}
|
|
437
|
+
function toggleQualityFilterValue(values, value) {
|
|
438
|
+
if (!values.includes(value))
|
|
439
|
+
return [...values, value];
|
|
440
|
+
if (values.length <= 1)
|
|
441
|
+
return values;
|
|
442
|
+
return values.filter((item) => item !== value);
|
|
443
|
+
}
|
|
444
|
+
function normalizeQualitySearch(value) {
|
|
445
|
+
return value.trim().toLowerCase();
|
|
446
|
+
}
|
|
447
|
+
function qualitySearchIncludes(query, parts) {
|
|
448
|
+
return parts
|
|
449
|
+
.filter((part) => part !== null && part !== undefined)
|
|
450
|
+
.join(' ')
|
|
451
|
+
.toLowerCase()
|
|
452
|
+
.includes(query);
|
|
182
453
|
}
|
|
183
454
|
function comparisonFromDelta(current, previous, formatter, label, unit) {
|
|
184
455
|
const delta = current - previous;
|
|
@@ -282,6 +553,41 @@ function CompactMetricGroup({ title, items, className, }) {
|
|
|
282
553
|
return (_jsxs("div", { className: "min-w-0", children: [_jsxs("dt", { className: "flex items-center gap-1.5 text-[12px] text-muted-foreground", children: [_jsx("span", { className: cn('size-1.5 shrink-0 rounded-full', COMPACT_METRIC_DOT_CLASS[tone]) }), _jsx("span", { className: "truncate", children: item.label })] }), _jsx("dd", { className: cn('mt-1 truncate text-[20px] font-semibold leading-none text-foreground', tone === 'danger' && 'text-destructive'), children: item.value })] }, item.label));
|
|
283
554
|
}) })] }));
|
|
284
555
|
}
|
|
556
|
+
function ReleaseLineDeleteImpactPanel({ impact, loading, }) {
|
|
557
|
+
const { t } = useI18n();
|
|
558
|
+
if (loading && !impact) {
|
|
559
|
+
return (_jsx("div", { className: "rounded-lg border bg-muted/35 px-3 py-2 text-[12px] text-muted-foreground", "data-testid": "release-line-delete-impact", children: t('releases.deleteImpact.loading') }));
|
|
560
|
+
}
|
|
561
|
+
if (!impact)
|
|
562
|
+
return null;
|
|
563
|
+
const items = [
|
|
564
|
+
{
|
|
565
|
+
key: 'events',
|
|
566
|
+
label: t('releases.deleteImpact.events'),
|
|
567
|
+
hint: t('releases.deleteImpact.eventsHint'),
|
|
568
|
+
count: impact.events.length,
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
key: 'versions',
|
|
572
|
+
label: t('releases.deleteImpact.versions'),
|
|
573
|
+
hint: t('releases.deleteImpact.versionsHint'),
|
|
574
|
+
count: impact.versions.length,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
key: 'run-results',
|
|
578
|
+
label: t('releases.deleteImpact.runResults'),
|
|
579
|
+
hint: t('releases.deleteImpact.runResultsHint'),
|
|
580
|
+
count: impact.runResults,
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
key: 'annotation-tasks',
|
|
584
|
+
label: t('releases.deleteImpact.annotationTasks'),
|
|
585
|
+
hint: t('releases.deleteImpact.annotationTasksHint'),
|
|
586
|
+
count: impact.annotationTasks.length,
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
return (_jsx("div", { className: "rounded-lg border border-destructive/25 bg-background p-3", "data-testid": "release-line-delete-impact", children: impact.total === 0 ? (_jsx("div", { className: "text-[12px] text-muted-foreground", children: t('releases.deleteImpact.empty') })) : (_jsx("div", { className: "grid gap-2 sm:grid-cols-4", children: items.map((item) => (_jsxs("div", { className: "rounded-md border bg-muted/30 px-3 py-2", "data-testid": `release-line-delete-impact-${item.key}`, children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: item.label }), _jsx("div", { className: "mt-1 font-mono text-[18px] font-semibold leading-none", children: formatCount(item.count) }), _jsx("div", { className: "mt-1 truncate text-[10.5px] text-muted-foreground", children: item.hint })] }, item.key))) })) }));
|
|
590
|
+
}
|
|
285
591
|
export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
286
592
|
const router = useRouter();
|
|
287
593
|
const pathname = usePathname();
|
|
@@ -293,17 +599,35 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
293
599
|
const line = useMemo(() => listQuery.data.find((item) => item.id === lineId || getReleaseLineId(item.promptId, item.inputConnectorId) === lineId) ?? null, [lineId, listQuery.data]);
|
|
294
600
|
const historyQuery = useProductionReleaseHistory(projectId, line?.promptId ?? '');
|
|
295
601
|
const releaseLineEventsQuery = useReleaseLineEvents(projectId, line?.id ?? '');
|
|
296
|
-
const
|
|
602
|
+
const stopLineMutation = useStopReleaseLine(projectId);
|
|
603
|
+
const startLineMutation = useStartReleaseLine(projectId);
|
|
604
|
+
const archiveLineMutation = useArchiveReleaseLine(projectId);
|
|
605
|
+
const unarchiveLineMutation = useUnarchiveReleaseLine(projectId);
|
|
606
|
+
const deleteLineMutation = useDeleteReleaseLine(projectId);
|
|
607
|
+
const stopCanaryMutation = useStopCanaryRelease(projectId);
|
|
297
608
|
const updateTrafficRatioMutation = useUpdateReleaseLineTrafficRatio(projectId);
|
|
609
|
+
const promoteCanaryMutation = usePromoteReleaseLineCanary(projectId);
|
|
298
610
|
const updateRunConfigMutation = useUpdateReleaseLineRunConfig(projectId);
|
|
611
|
+
const updateOutputRouteMutation = useUpdateReleaseLineOutputRoute(projectId);
|
|
612
|
+
const updateInputRouteMutation = useUpdateReleaseLineInputRoute(projectId);
|
|
299
613
|
const modelQuery = useProjectModels(projectId);
|
|
614
|
+
const outputConnectorsQuery = useConnectors(projectId, { direction: 'output' });
|
|
300
615
|
const tab = resolveTab(searchParams.get('tab'));
|
|
301
|
-
const
|
|
616
|
+
const selectedReleaseVersionId = searchParams.get('version') ?? undefined;
|
|
302
617
|
const [stopDialogOpen, setStopDialogOpen] = useState(false);
|
|
303
618
|
const [stopConfirmationText, setStopConfirmationText] = useState('');
|
|
619
|
+
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
|
620
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
621
|
+
const [deleteState, setDeleteState] = useState({ lineId: '', confirmationText: '', error: null });
|
|
622
|
+
const activeDeleteLineId = line?.id ?? '';
|
|
623
|
+
const deleteConfirmationText = deleteState.lineId === activeDeleteLineId ? deleteState.confirmationText : '';
|
|
624
|
+
const deleteError = deleteState.lineId === activeDeleteLineId ? deleteState.error : null;
|
|
625
|
+
const deleteImpactQuery = useReleaseLineDeleteImpact(projectId, deleteDialogOpen ? activeDeleteLineId : '');
|
|
304
626
|
const productionReleaseName = useMemo(() => getReleaseStopConfirmationName(line), [line]);
|
|
305
627
|
const canConfirmStopProduction = stopConfirmationText === productionReleaseName && productionReleaseName.length > 0;
|
|
306
|
-
const
|
|
628
|
+
const canConfirmDelete = Boolean(line && deleteConfirmationText === line.label);
|
|
629
|
+
const canAddCanary = Boolean(line && line.production?.currentEvent?.status === 'running');
|
|
630
|
+
const canaryActionPending = stopCanaryMutation.isPending || promoteCanaryMutation.isPending;
|
|
307
631
|
const isLive = hasRunningRelease(line);
|
|
308
632
|
const onAutoRefreshTick = useCallback(async () => {
|
|
309
633
|
await Promise.all([
|
|
@@ -318,10 +642,18 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
318
642
|
onTick: onAutoRefreshTick,
|
|
319
643
|
});
|
|
320
644
|
useEffect(() => {
|
|
321
|
-
|
|
645
|
+
const rawTab = searchParams.get('tab');
|
|
646
|
+
const normalizedTab = rawTab === 'annotation'
|
|
647
|
+
? 'quality'
|
|
648
|
+
: rawTab === 'variants' || rawTab === 'versions'
|
|
649
|
+
? 'history'
|
|
650
|
+
: rawTab === 'delete'
|
|
651
|
+
? 'settings'
|
|
652
|
+
: null;
|
|
653
|
+
if (!normalizedTab)
|
|
322
654
|
return;
|
|
323
655
|
const params = new URLSearchParams(searchParams.toString());
|
|
324
|
-
params.set('tab',
|
|
656
|
+
params.set('tab', normalizedTab);
|
|
325
657
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
326
658
|
}, [pathname, router, searchParams]);
|
|
327
659
|
const selectTab = useCallback((next) => {
|
|
@@ -331,7 +663,7 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
331
663
|
else
|
|
332
664
|
params.set('tab', next);
|
|
333
665
|
if (next !== 'results')
|
|
334
|
-
params.delete('
|
|
666
|
+
params.delete('version');
|
|
335
667
|
const query = params.toString();
|
|
336
668
|
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
337
669
|
}, [pathname, router, searchParams]);
|
|
@@ -343,22 +675,22 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
343
675
|
return (_jsx(Main, { fixed: true, className: "bg-muted/35", children: _jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.notFound') }) }));
|
|
344
676
|
}
|
|
345
677
|
function openStopProductionDialog() {
|
|
346
|
-
if (!line
|
|
678
|
+
if (!line || line.status !== 'running')
|
|
347
679
|
return;
|
|
348
680
|
setStopConfirmationText('');
|
|
349
681
|
setStopDialogOpen(true);
|
|
350
682
|
}
|
|
351
683
|
function closeStopProductionDialog() {
|
|
352
|
-
if (
|
|
684
|
+
if (stopLineMutation.isPending)
|
|
353
685
|
return;
|
|
354
686
|
setStopDialogOpen(false);
|
|
355
687
|
setStopConfirmationText('');
|
|
356
688
|
}
|
|
357
689
|
function confirmStopProduction() {
|
|
358
|
-
if (!line
|
|
690
|
+
if (!line || !canConfirmStopProduction)
|
|
359
691
|
return;
|
|
360
|
-
|
|
361
|
-
|
|
692
|
+
stopLineMutation.mutate({
|
|
693
|
+
releaseLineId: line.id,
|
|
362
694
|
body: { reason: t('releases.detail.stopReason') },
|
|
363
695
|
}, {
|
|
364
696
|
onSuccess: () => {
|
|
@@ -367,22 +699,116 @@ export function ReleaseLineDetailPage({ projectId, releaseLineId }) {
|
|
|
367
699
|
},
|
|
368
700
|
});
|
|
369
701
|
}
|
|
702
|
+
function startReleaseLine() {
|
|
703
|
+
if (!line || line.status !== 'stopped')
|
|
704
|
+
return;
|
|
705
|
+
startLineMutation.mutate({
|
|
706
|
+
releaseLineId: line.id,
|
|
707
|
+
body: { reason: t('releases.detail.startReason') },
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function openArchiveDialog() {
|
|
711
|
+
if (!line || line.status !== 'stopped')
|
|
712
|
+
return;
|
|
713
|
+
setArchiveDialogOpen(true);
|
|
714
|
+
}
|
|
715
|
+
function closeArchiveDialog() {
|
|
716
|
+
if (archiveLineMutation.isPending)
|
|
717
|
+
return;
|
|
718
|
+
setArchiveDialogOpen(false);
|
|
719
|
+
}
|
|
720
|
+
function confirmArchiveReleaseLine() {
|
|
721
|
+
if (!line || line.status !== 'stopped')
|
|
722
|
+
return;
|
|
723
|
+
archiveLineMutation.mutate({
|
|
724
|
+
releaseLineId: line.id,
|
|
725
|
+
body: { reason: t('releases.detail.archiveReason') },
|
|
726
|
+
}, {
|
|
727
|
+
onSuccess: () => setArchiveDialogOpen(false),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function unarchiveReleaseLine() {
|
|
731
|
+
if (!line || line.status !== 'archived')
|
|
732
|
+
return;
|
|
733
|
+
unarchiveLineMutation.mutate({
|
|
734
|
+
releaseLineId: line.id,
|
|
735
|
+
body: { reason: t('releases.detail.unarchiveReason') },
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function openDeleteDialog() {
|
|
739
|
+
if (!line)
|
|
740
|
+
return;
|
|
741
|
+
setDeleteState({ lineId: line.id, confirmationText: '', error: null });
|
|
742
|
+
setDeleteDialogOpen(true);
|
|
743
|
+
}
|
|
744
|
+
function closeDeleteDialog() {
|
|
745
|
+
if (deleteLineMutation.isPending)
|
|
746
|
+
return;
|
|
747
|
+
setDeleteDialogOpen(false);
|
|
748
|
+
if (line)
|
|
749
|
+
setDeleteState({ lineId: line.id, confirmationText: '', error: null });
|
|
750
|
+
}
|
|
751
|
+
async function confirmDeleteReleaseLine() {
|
|
752
|
+
if (!line || !canConfirmDelete)
|
|
753
|
+
return;
|
|
754
|
+
setDeleteState((current) => ({
|
|
755
|
+
lineId: line.id,
|
|
756
|
+
confirmationText: current.lineId === line.id ? current.confirmationText : '',
|
|
757
|
+
error: null,
|
|
758
|
+
}));
|
|
759
|
+
try {
|
|
760
|
+
await deleteLineMutation.mutateAsync({
|
|
761
|
+
releaseLineId: line.id,
|
|
762
|
+
body: {
|
|
763
|
+
confirmationName: line.label,
|
|
764
|
+
reason: t('releases.detail.deleteReason'),
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
setDeleteDialogOpen(false);
|
|
768
|
+
router.push('/releases');
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
setDeleteState((current) => ({
|
|
772
|
+
lineId: line.id,
|
|
773
|
+
confirmationText: current.lineId === line.id ? current.confirmationText : '',
|
|
774
|
+
error: getApiErrorMessage(error) ?? t('releases.detail.deleteFailed'),
|
|
775
|
+
}));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
370
778
|
function openAddCanaryPage() {
|
|
371
779
|
if (!line || !canAddCanary)
|
|
372
780
|
return;
|
|
373
781
|
router.push(`/releases/new?mode=canary&line=${encodeURIComponent(line.id)}`);
|
|
374
782
|
}
|
|
375
|
-
return (_jsxs(Main, { fixed: true, className: "gap-5 overflow-auto bg-muted/35 pb-8", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-[1760px] flex-col gap-5", "data-testid": "release-line-detail-page", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("h1", { className: "truncate text-[22px] font-semibold leading-tight", children: line.promptName }), _jsx("span", { "data-testid": "release-line-detail-status", className: "sr-only", children: line.
|
|
783
|
+
return (_jsxs(Main, { fixed: true, className: "gap-5 overflow-auto bg-muted/35 pb-8", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-[1760px] flex-col gap-5", "data-testid": "release-line-detail-page", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsx("div", { className: "min-w-0", children: _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("h1", { className: "truncate text-[22px] font-semibold leading-tight", children: line.promptName }), _jsx("span", { "data-testid": "release-line-detail-status", className: "sr-only", children: line.status })] }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [line.status === 'running' ? (_jsxs(Button, { variant: "outline", onClick: openStopProductionDialog, disabled: stopLineMutation.isPending, className: "text-destructive hover:text-destructive", "data-testid": "release-line-detail-stop", children: [_jsx(Square, { className: "size-4" }), t('releases.detail.action.stopProduction')] })) : null, line.status === 'stopped' ? (_jsxs(Button, { variant: "outline", onClick: startReleaseLine, disabled: startLineMutation.isPending, "data-testid": "release-line-detail-start", children: [_jsx(Play, { className: "size-4" }), startLineMutation.isPending ? t('releases.detail.action.starting') : t('releases.detail.action.start')] })) : null, line.status === 'stopped' ? (_jsxs(Button, { variant: "outline", onClick: openArchiveDialog, disabled: archiveLineMutation.isPending, "data-testid": "release-line-detail-archive", children: [_jsx(Archive, { className: "size-4" }), archiveLineMutation.isPending
|
|
784
|
+
? t('releases.detail.action.archiving')
|
|
785
|
+
: t('releases.detail.action.archive')] })) : null, line.status === 'archived' ? (_jsxs(Button, { variant: "outline", onClick: unarchiveReleaseLine, disabled: unarchiveLineMutation.isPending, "data-testid": "release-line-detail-unarchive", children: [_jsx(RotateCcw, { className: "size-4" }), unarchiveLineMutation.isPending
|
|
786
|
+
? t('releases.detail.action.unarchiving')
|
|
787
|
+
: t('releases.detail.action.unarchive')] })) : null, canAddCanary ? (_jsxs(Button, { onClick: openAddCanaryPage, children: [_jsx(Plus, { className: "size-4" }), line.canary ? t('releases.detail.action.replaceCanary') : t('releases.detail.action.addCanary')] })) : null] })] }), _jsx(ReleaseTopologyCanvas, { line: line, models: modelQuery.data?.data ?? [], modelsLoading: modelQuery.isLoading, outputConnectors: outputConnectorsQuery.data?.data ?? [], outputConnectorsLoading: outputConnectorsQuery.isLoading, onUpdateTrafficRatio: (_canary, trafficRatio) => updateTrafficRatioMutation.mutateAsync({
|
|
376
788
|
releaseLineId: line.id,
|
|
377
789
|
body: { trafficRatio },
|
|
378
790
|
}), trafficRatioPending: updateTrafficRatioMutation.isPending, onUpdateRunConfig: (body) => updateRunConfigMutation.mutateAsync({
|
|
379
791
|
releaseLineId: line.id,
|
|
380
792
|
body,
|
|
381
|
-
}), runConfigPending: updateRunConfigMutation.isPending,
|
|
793
|
+
}), runConfigPending: updateRunConfigMutation.isPending, onUpdateOutputRoute: (body) => updateOutputRouteMutation.mutateAsync({
|
|
794
|
+
releaseLineId: line.id,
|
|
795
|
+
body,
|
|
796
|
+
}), outputRoutePending: updateOutputRouteMutation.isPending, onUpdateInputRoute: (body) => updateInputRouteMutation.mutateAsync({
|
|
797
|
+
releaseLineId: line.id,
|
|
798
|
+
body,
|
|
799
|
+
}), inputRoutePending: updateInputRouteMutation.isPending, onAddCanary: canAddCanary ? openAddCanaryPage : undefined, onStopCanary: (canary) => stopCanaryMutation.mutateAsync(canary.id), onPromoteCanary: () => promoteCanaryMutation.mutateAsync(line.id), canaryActionPending: canaryActionPending }), _jsx("div", { className: "inline-flex w-fit flex-wrap gap-0.5 rounded-lg border bg-card p-1", children: DETAIL_TABS.map((item) => (_jsx("button", { type: "button", onClick: () => selectTab(item.value), className: cn('rounded-md px-3.5 py-1.5 text-[13px] font-medium transition-colors', tab === item.value
|
|
382
800
|
? 'bg-muted font-semibold text-foreground'
|
|
383
|
-
: 'text-muted-foreground hover:text-foreground'), children: t(item.key) }, item.value))) }), tab === 'monitoring' ? (_jsx(MonitoringPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === '
|
|
801
|
+
: 'text-muted-foreground hover:text-foreground'), children: t(item.key) }, item.value))) }), tab === 'monitoring' ? (_jsx(MonitoringPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === 'results' ? (_jsx(ResultsPane, { projectId: projectId, line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [], initialReleaseVersionId: selectedReleaseVersionId })) : null, tab === 'quality' ? (_jsx(QualityMetricsPane, { line: line, releaseEvents: releaseLineEventsQuery.data?.data ?? [] })) : null, tab === 'history' ? (_jsx(HistoryPane, { projectId: projectId, line: line, productionHistory: historyQuery.data?.data ?? [], releaseEvents: releaseLineEventsQuery.data?.data ?? [], loading: historyQuery.isLoading || releaseLineEventsQuery.isLoading })) : null, tab === 'settings' ? (_jsxs("section", { className: "rounded-lg border bg-card", "data-testid": "release-line-settings-tab", children: [_jsxs("div", { className: "border-b px-4 py-3", children: [_jsx("div", { className: "text-[13px] font-semibold", children: t('releases.detail.settings.title') }), _jsx("p", { className: "mt-1 text-[12px] text-muted-foreground", children: t('releases.detail.settings.description') })] }), _jsx("div", { className: "p-4", children: _jsxs("div", { className: "flex flex-col gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 text-[13px] font-semibold text-destructive", children: [_jsx(AlertTriangle, { className: "size-4" }), t('releases.detail.delete.dangerTitle')] }), _jsx("p", { className: "mt-1 max-w-3xl text-[12px] text-muted-foreground", children: t('releases.detail.delete.description') })] }), _jsxs(Button, { type: "button", variant: "destructive", onClick: openDeleteDialog, "data-testid": "release-line-delete-open", children: [_jsx(Trash2, { className: "size-4" }), t('releases.detail.delete.open')] })] }) })] })) : null] }), _jsx(Dialog, { open: stopDialogOpen, onOpenChange: (open) => (open ? setStopDialogOpen(true) : closeStopProductionDialog()), children: _jsxs(DialogContent, { "data-testid": "release-stop-production-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.detail.stopDialog.title') }), _jsx(DialogDescription, { children: t('releases.detail.stopDialog.description') })] }), _jsxs("div", { className: "rounded-lg border bg-muted/40 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.stopDialog.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: productionReleaseName || '—' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "release-stop-production-name", className: "text-[12.5px] font-medium", children: t('releases.detail.stopDialog.inputLabel') }), _jsx(Input, { id: "release-stop-production-name", value: stopConfirmationText, onChange: (event) => setStopConfirmationText(event.target.value), placeholder: t('releases.detail.stopDialog.inputPlaceholder').replace('{name}', productionReleaseName), autoComplete: "off" }), stopConfirmationText.length > 0 && !canConfirmStopProduction ? (_jsx("p", { className: "text-[12px] text-destructive", children: t('releases.detail.stopDialog.mismatch') })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeStopProductionDialog, disabled: stopLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", variant: "destructive", onClick: confirmStopProduction, disabled: !canConfirmStopProduction || stopLineMutation.isPending, "data-testid": "release-stop-production-confirm", children: [_jsx(Square, { className: "size-4" }), stopLineMutation.isPending
|
|
384
802
|
? t('releases.detail.stopDialog.stopping')
|
|
385
|
-
: t('releases.detail.stopDialog.confirm')] })] })] }) })] }))
|
|
803
|
+
: t('releases.detail.stopDialog.confirm')] })] })] }) }), _jsx(Dialog, { open: archiveDialogOpen, onOpenChange: (open) => (open ? setArchiveDialogOpen(true) : closeArchiveDialog()), children: _jsxs(DialogContent, { "data-testid": "release-line-detail-archive-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.archiveDialog.title') }), _jsx(DialogDescription, { children: t('releases.archiveDialog.description') })] }), _jsxs("div", { className: "rounded-lg border bg-muted/40 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.stopDialog.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: line.label })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeArchiveDialog, disabled: archiveLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", onClick: confirmArchiveReleaseLine, disabled: archiveLineMutation.isPending, "data-testid": "release-line-detail-archive-confirm", children: [_jsx(Archive, { className: "size-4" }), archiveLineMutation.isPending
|
|
804
|
+
? t('releases.archiveDialog.archiving')
|
|
805
|
+
: t('releases.archiveDialog.confirm')] })] })] }) }), _jsx(Dialog, { open: deleteDialogOpen, onOpenChange: (open) => (open ? openDeleteDialog() : closeDeleteDialog()), children: _jsxs(DialogContent, { "data-testid": "release-line-delete-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('releases.detail.delete.title') }), _jsx(DialogDescription, { children: t('releases.detail.delete.dialogDescription') })] }), _jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3", children: [_jsx("div", { className: "text-[11.5px] font-medium text-muted-foreground", children: t('releases.detail.delete.releaseName') }), _jsx("div", { className: "mt-1 break-all font-mono text-[13px] font-semibold", children: line.label })] }), _jsx(ReleaseLineDeleteImpactPanel, { impact: deleteImpactQuery.data, loading: deleteImpactQuery.isLoading || deleteImpactQuery.isFetching }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "release-line-delete-name", className: "text-[12.5px] font-medium", children: t('releases.detail.delete.inputLabel') }), _jsx(Input, { id: "release-line-delete-name", value: deleteConfirmationText, onChange: (event) => setDeleteState({
|
|
806
|
+
lineId: line.id,
|
|
807
|
+
confirmationText: event.target.value,
|
|
808
|
+
error: null,
|
|
809
|
+
}), placeholder: t('releases.detail.delete.inputPlaceholder').replace('{name}', line.label), autoComplete: "off" }), deleteConfirmationText.length > 0 && !canConfirmDelete ? (_jsx("p", { className: "text-[12px] text-destructive", children: t('releases.detail.delete.mismatch') })) : null] }), deleteError ? (_jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: deleteError })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: closeDeleteDialog, disabled: deleteLineMutation.isPending, children: t('common.cancel') }), _jsxs(Button, { type: "button", variant: "destructive", onClick: () => void confirmDeleteReleaseLine(), disabled: !canConfirmDelete || deleteLineMutation.isPending, "data-testid": "release-line-delete-confirm", children: [_jsx(Trash2, { className: "size-4" }), deleteLineMutation.isPending
|
|
810
|
+
? t('releases.detail.delete.deleting')
|
|
811
|
+
: t('releases.detail.delete.confirm')] })] })] }) })] }));
|
|
386
812
|
}
|
|
387
813
|
function MonitoringPane({ projectId, line, releaseEvents, }) {
|
|
388
814
|
const { t, language } = useI18n();
|
|
@@ -505,58 +931,57 @@ function MonitoringPane({ projectId, line, releaseEvents, }) {
|
|
|
505
931
|
},
|
|
506
932
|
] })] }) }), _jsxs("section", { className: "space-y-3", "aria-label": t('releases.detail.metric.engineering'), children: [_jsx("h3", { className: "text-[14px] font-semibold", children: t('releases.detail.metric.engineering') }), _jsxs("div", { className: "grid grid-cols-1 gap-4 xl:grid-cols-2 2xl:grid-cols-4", children: [_jsx(BigChartCard, { title: t('releases.detail.metric.rpm'), icon: _jsx(Gauge, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: formatRateValue(timeseriesMax.rpm), comparison: comparisonFromDelta(timeseriesMax.rpm, stats?.rpmPeak.previous ?? 0, formatRateValue, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.rpmSubtitle'), data: pickTimeseries('rpm'), yTickFormatter: formatRateValue, legendFormatter: formatRateValue, bySource: stats?.rpmPeak.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.tpm'), icon: _jsx(Activity, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-iter-soft)", iconFg: "var(--src-iter-fg)", total: formatBigNumber(timeseriesMax.tpm), comparison: comparisonFromDelta(timeseriesMax.tpm, stats?.tpmPeak.previous ?? 0, formatBigNumber, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.tpmSubtitle'), data: pickTimeseries('tpm'), yTickFormatter: formatBigNumber, legendFormatter: formatBigNumber, bySource: stats?.tpmPeak.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.averageLatency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-canary-soft)", iconFg: "var(--src-canary-fg)", total: formatLatencyMs(timeseriesMax.latencyAverageMs), comparison: comparisonFromDelta(timeseriesMax.latencyAverageMs, stats?.latencyAverageMs.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyAverageMs'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyAverageMs.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p50Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--src-prod-soft)", iconFg: "var(--src-prod-fg)", total: formatLatencyMs(timeseriesMax.latencyP50Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP50Ms, stats?.latencyP50Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP50Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP50Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p95Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-running-bg)", iconFg: "var(--status-running-fg)", total: formatLatencyMs(timeseriesMax.latencyP95Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP95Ms, stats?.latencyP95Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP95Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP95Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.p99Latency'), icon: _jsx(Timer, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: formatLatencyMs(timeseriesMax.latencyP99Ms), comparison: comparisonFromDelta(timeseriesMax.latencyP99Ms, stats?.latencyP99Ms.previous ?? 0, formatLatencyMs, vsPreviousPeriodLabel), data: pickTimeseries('latencyP99Ms'), yTickFormatter: formatLatencyMs, legendFormatter: formatLatencyMs, bySource: stats?.latencyP99Ms.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.cost'), icon: _jsx(CircleDollarSign, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-running-bg)", iconFg: "var(--status-running-fg)", total: formatCostValue(timeseriesMax.cost), comparison: comparisonFromDelta(timeseriesMax.cost, stats?.cost.previous ?? 0, formatCostValue, vsPreviousPeriodLabel), subtitle: t('monitoring.delta.costSubtitle'), data: pickTimeseries('cost'), yTickFormatter: formatCostValue, legendFormatter: formatCostValue, bySource: stats?.cost.bySource ?? EMPTY_BY_SOURCE, sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, ...chartLabels }), _jsx(BigChartCard, { title: t('releases.detail.metric.failureRate'), icon: _jsx(AlertTriangle, { className: "size-4", strokeWidth: 2.2 }), iconBg: "var(--status-pending-bg)", iconFg: "var(--status-pending-fg)", total: timeseriesMax.failureRatePercent.toFixed(2), unit: "%", comparison: comparisonFromDelta(timeseriesMax.failureRatePercent, failureRatePercent(stats, 'previous'), (value) => value.toFixed(2), vsPreviousPeriodLabel, '%'), subtitle: t('monitoring.delta.failureRateSubtitle'), data: pickFailureRateTimeseries(), yTickFormatter: formatPercentValue, legendFormatter: formatPercentValue, bySource: failureRateBySourcePercent(stats), sourceLabels: sourceLabels, sourceKeys: RELEASE_MONITORING_SOURCE_KEYS, sourceDistributionLabel: chartLabels.sourceDistributionLabel, totalLabel: chartLabels.failureRateTotalLabel })] })] })] }));
|
|
507
933
|
}
|
|
508
|
-
function
|
|
509
|
-
const { t } = useI18n();
|
|
510
|
-
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
511
|
-
const details = useMemo(() => buildReleaseVariantDetails(line, releaseEvents), [line, releaseEvents]);
|
|
512
|
-
const showLoader = useDelayedLoading(loading);
|
|
513
|
-
if (loading && details.length === 0) {
|
|
514
|
-
return showLoader ? _jsx(PlatformLoader, { className: "py-8", size: "sm" }) : null;
|
|
515
|
-
}
|
|
516
|
-
if (details.length === 0) {
|
|
517
|
-
return (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.variants.empty') }));
|
|
518
|
-
}
|
|
519
|
-
return (_jsx("section", { className: "space-y-3", "data-testid": "release-variants-pane", children: _jsx("div", { className: "grid grid-cols-1 gap-3 xl:grid-cols-2", children: details.map((detail) => (_jsxs("article", { className: "rounded-lg border bg-card p-4", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-[17px] font-semibold", children: detail.label }), _jsx(ReleaseVariantStageBadge, { stage: detail.stage })] }), _jsxs("div", { className: "mt-1 max-w-full truncate text-[12px] text-muted-foreground", children: [detail.promptName, " \u00B7 ", detail.promptVersionLabel ?? formatShortId(detail.promptVersionId), " \u00B7", ' ', detail.modelName ?? formatShortId(detail.modelId)] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void navigator.clipboard?.writeText(detail.id), children: [_jsx(Copy, { className: "size-3.5" }), t('releases.detail.variants.copyId')] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", asChild: true, children: _jsxs(Link, { href: `/releases/${encodeURIComponent(line.id)}?tab=results&variant=${encodeURIComponent(detail.id)}`, children: [_jsx(Activity, { className: "size-3.5" }), t('releases.detail.variants.viewResults')] }) }), _jsx(Button, { type: "button", size: "sm", asChild: true, children: _jsxs(Link, { href: `/annotations/new?line=${encodeURIComponent(line.id)}&variant=${encodeURIComponent(detail.id)}`, children: [_jsx(ClipboardCheck, { className: "size-3.5" }), t('releases.detail.variants.newAnnotation')] }) })] })] }), _jsxs("dl", { className: "mt-4 grid grid-cols-2 gap-3 border-t pt-4 md:grid-cols-4", children: [_jsx(VariantMeta, { label: t('releases.detail.variants.promptVersion'), value: detail.promptVersionLabel ?? formatShortId(detail.promptVersionId) }), _jsx(VariantMeta, { label: t('releases.detail.variants.model'), value: detail.modelName ?? formatShortId(detail.modelId) }), _jsx(VariantMeta, { label: t('releases.detail.variants.provider'), value: detail.modelProvider ?? '—' }), _jsx(VariantMeta, { label: t('releases.detail.variants.updatedAt'), value: formatDateTimeOrDash(detail.updatedAt) }), _jsx(VariantMeta, { label: t('releases.detail.variants.productionEvents'), value: formatCount(detail.productionEventCount) }), _jsx(VariantMeta, { label: t('releases.detail.variants.canaryEvents'), value: formatCount(detail.canaryEventCount) }), _jsx(VariantMeta, { label: t('releases.detail.variants.processed'), value: formatCount(detail.totalProcessed) }), _jsx(VariantMeta, { label: t('releases.detail.variants.errors'), value: formatCount(detail.totalErrors) })] }), _jsxs("div", { className: "mt-4 border-t pt-4", children: [_jsx("div", { className: "mb-2 text-[12px] font-medium text-muted-foreground", children: t('releases.detail.variants.events') }), detail.events.length === 0 ? (_jsx("div", { className: "text-[12px] text-muted-foreground", children: t('releases.detail.variants.noEvents') })) : (_jsx("div", { className: "space-y-2", children: detail.events.slice(0, 5).map((event) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2 text-[12px]", children: [_jsx(ReleaseEventPill, { event: event.operation }), _jsx("span", { className: "font-mono text-muted-foreground", children: t(event.laneType === 'production'
|
|
520
|
-
? 'releases.detail.history.productionLane'
|
|
521
|
-
: 'releases.detail.history.canaryLane') }), _jsx("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", children: event.submitReason || event.status }), _jsx("span", { className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(event.createdAt) })] }, event.id))) }))] }), _jsx("div", { className: "mt-4 border-t pt-3", children: _jsx("div", { className: "font-mono text-[11px] text-muted-foreground", children: detail.id }) })] }, detail.id))) }) }));
|
|
522
|
-
}
|
|
523
|
-
function VariantMeta({ label, value }) {
|
|
524
|
-
return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "truncate text-[11.5px] text-muted-foreground", children: label }), _jsx("dd", { className: "mt-1 truncate font-mono text-[12.5px] font-medium text-foreground", children: value })] }));
|
|
525
|
-
}
|
|
526
|
-
function ReleaseVariantStageBadge({ stage }) {
|
|
527
|
-
const { t } = useI18n();
|
|
528
|
-
const isProduction = stage === 'production' || stage === 'production_canary';
|
|
529
|
-
const isCanary = stage === 'canary' || stage === 'production_canary';
|
|
530
|
-
return (_jsx("span", { className: "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium", style: {
|
|
531
|
-
background: isProduction ? 'var(--src-prod-soft)' : isCanary ? 'var(--src-canary-soft)' : 'var(--muted)',
|
|
532
|
-
color: isProduction ? 'var(--src-prod-fg)' : isCanary ? 'var(--src-canary-fg)' : 'var(--muted-foreground)',
|
|
533
|
-
borderColor: isProduction
|
|
534
|
-
? 'color-mix(in srgb, var(--src-prod) 30%, transparent)'
|
|
535
|
-
: isCanary
|
|
536
|
-
? 'color-mix(in srgb, var(--src-canary) 30%, transparent)'
|
|
537
|
-
: 'var(--border)',
|
|
538
|
-
}, children: t(`releases.detail.variants.stage.${stage}`) }));
|
|
539
|
-
}
|
|
540
|
-
function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId, }) {
|
|
541
|
-
const { t } = useI18n();
|
|
934
|
+
function ResultsPane({ projectId, line, releaseEvents, initialReleaseVersionId, }) {
|
|
935
|
+
const { t, language } = useI18n();
|
|
542
936
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
543
|
-
const [
|
|
544
|
-
const [
|
|
545
|
-
const [promptVersionFilter, setPromptVersionFilter] = useState('all');
|
|
937
|
+
const [dateRange, setDateRange] = useState(() => createDefaultResultDateRange());
|
|
938
|
+
const [releaseVersionFilter, setReleaseVersionFilter] = useState(initialReleaseVersionId ?? 'all');
|
|
546
939
|
const [pageIndex, setPageIndex] = useState(0);
|
|
547
940
|
const [pageSize, setPageSize] = useState(20);
|
|
548
941
|
const sourceIds = useMemo(() => getReleaseResultSourceIds(line, releaseEvents), [line, releaseEvents]);
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
? releaseVariantFilter
|
|
553
|
-
: 'all';
|
|
554
|
-
const activePromptVersionFilter = promptVersionFilter === 'all' || promptVersionOptions.some((option) => option.id === promptVersionFilter)
|
|
555
|
-
? promptVersionFilter
|
|
942
|
+
const releaseVersionOptions = useMemo(() => getReleaseResultVersionOptions(line, releaseEvents), [line, releaseEvents]);
|
|
943
|
+
const activeReleaseVersionFilter = releaseVersionFilter === 'all' || releaseVersionOptions.some((option) => option.id === releaseVersionFilter)
|
|
944
|
+
? releaseVersionFilter
|
|
556
945
|
: 'all';
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
946
|
+
const releaseVersionIds = activeReleaseVersionFilter === 'all' ? undefined : [activeReleaseVersionFilter];
|
|
947
|
+
const applyDateRange = isResultDateRangeApplied(dateRange);
|
|
948
|
+
const handleDateRangeChange = useCallback((next) => {
|
|
949
|
+
setDateRange(next);
|
|
950
|
+
setPageIndex(0);
|
|
951
|
+
}, []);
|
|
952
|
+
const refreshResultDateRange = useCallback(() => {
|
|
953
|
+
const nextDateRange = resolveRollingDateRangeValue(dateRange);
|
|
954
|
+
if (nextDateRange.preset !== dateRange.preset ||
|
|
955
|
+
nextDateRange.from !== dateRange.from ||
|
|
956
|
+
nextDateRange.to !== dateRange.to) {
|
|
957
|
+
setDateRange(nextDateRange);
|
|
958
|
+
}
|
|
959
|
+
}, [dateRange]);
|
|
960
|
+
useAutoRefresh({
|
|
961
|
+
intervalMs: AUTO_REFRESH_INTERVAL_MS,
|
|
962
|
+
enabled: sourceIds.length > 0 && isResultDateRangeRolling(dateRange),
|
|
963
|
+
onTick: refreshResultDateRange,
|
|
964
|
+
});
|
|
965
|
+
const dateRangePresetLabels = useMemo(() => [
|
|
966
|
+
{ value: 'all', label: t('releases.detail.results.dateFilter.all') },
|
|
967
|
+
{ value: 'h24', label: t('monitoring.timeRange.preset.h24') },
|
|
968
|
+
{ value: 'd7', label: t('monitoring.timeRange.preset.d7') },
|
|
969
|
+
{ value: 'd30', label: t('monitoring.timeRange.preset.d30') },
|
|
970
|
+
{ value: 'custom', label: t('monitoring.timeRange.preset.custom') },
|
|
971
|
+
], [t]);
|
|
972
|
+
const dateRangeLabels = useMemo(() => ({
|
|
973
|
+
ariaLabel: t('releases.detail.results.dateFilter.ariaLabel'),
|
|
974
|
+
customRangeAriaLabel: t('releases.detail.results.dateFilter.customRangeAriaLabel'),
|
|
975
|
+
fromLabel: t('monitoring.timeRange.from'),
|
|
976
|
+
toLabel: t('monitoring.timeRange.to'),
|
|
977
|
+
dateLabel: t('monitoring.timeRange.date'),
|
|
978
|
+
timeLabel: t('monitoring.timeRange.time'),
|
|
979
|
+
previousMonth: t('monitoring.timeRange.previousMonth'),
|
|
980
|
+
nextMonth: t('monitoring.timeRange.nextMonth'),
|
|
981
|
+
cancel: t('common.cancel'),
|
|
982
|
+
apply: t('common.apply'),
|
|
983
|
+
invalidRange: t('monitoring.timeRange.invalidRange'),
|
|
984
|
+
}), [t]);
|
|
560
985
|
const resultsQuery = useReleaseRunResults(projectId, {
|
|
561
986
|
page: pageIndex + 1,
|
|
562
987
|
pageSize,
|
|
@@ -565,9 +990,10 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
565
990
|
judgmentStatus: undefined,
|
|
566
991
|
isCorrect: undefined,
|
|
567
992
|
sourceIds,
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
993
|
+
releaseVersionIds,
|
|
994
|
+
releaseVersionScope: 'exact',
|
|
995
|
+
from: applyDateRange ? dateRange.from : undefined,
|
|
996
|
+
to: applyDateRange ? dateRange.to : undefined,
|
|
571
997
|
}, sourceIds.length > 0);
|
|
572
998
|
const rows = resultsQuery.data?.data ?? [];
|
|
573
999
|
const resultsLoading = useDelayedLoading(resultsQuery.isLoading);
|
|
@@ -575,16 +1001,10 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
575
1001
|
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
576
1002
|
const from = total === 0 ? 0 : pageIndex * pageSize + 1;
|
|
577
1003
|
const to = Math.min((pageIndex + 1) * pageSize, total);
|
|
578
|
-
return (_jsxs("div", { className: "overflow-hidden rounded-lg border bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3", children: [_jsx("div", { children: _jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.tab.results') }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("label", { className: "sr-only", htmlFor: "release-result-
|
|
579
|
-
|
|
1004
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-lg border bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3", children: [_jsx("div", { children: _jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.tab.results') }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(DateRangeSegmented, { value: dateRange, onChange: handleDateRangeChange, presetLabels: dateRangePresetLabels, labels: dateRangeLabels, locale: language }), _jsx("label", { className: "sr-only", htmlFor: "release-result-version-filter", children: t('releases.detail.results.version') }), _jsx(ResultReleaseVersionSelect, { id: "release-result-version-filter", options: releaseVersionOptions, value: activeReleaseVersionFilter, onChange: (next) => {
|
|
1005
|
+
setReleaseVersionFilter(next);
|
|
580
1006
|
setPageIndex(0);
|
|
581
|
-
},
|
|
582
|
-
setPromptVersionFilter(event.currentTarget.value);
|
|
583
|
-
setPageIndex(0);
|
|
584
|
-
}, className: "h-9 rounded-md border bg-background px-3 text-[12px] font-medium text-foreground shadow-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", disabled: promptVersionOptions.length === 0, children: [_jsx("option", { value: "all", children: t('releases.detail.results.promptVersionFilter.all') }), promptVersionOptions.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id)))] }), _jsx("div", { className: "inline-flex rounded-lg border bg-background p-1", children: ['all', 'production', 'canary'].map((value) => (_jsx("button", { type: "button", onClick: () => {
|
|
585
|
-
setSourceFilter(value);
|
|
586
|
-
setPageIndex(0);
|
|
587
|
-
}, className: cn('h-7 rounded-md px-3 text-[12px] font-medium transition-colors', sourceFilter === value ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground'), children: t(`releases.detail.results.sourceFilter.${value}`) }, value))) })] })] }), _jsxs(Table, { columns: RESULT_COLUMNS, children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { column: "externalId", children: t('releases.detail.results.externalId') }), _jsx(TableHead, { column: "input", children: t('releases.detail.results.input') }), _jsx(TableHead, { column: "output", children: t('releases.detail.results.output') }), _jsx(TableHead, { column: "source", children: t('releases.detail.results.source') }), _jsx(TableHead, { column: "variant", children: t('releases.detail.results.variant') }), _jsx(TableHead, { column: "latency", children: t('releases.detail.results.latency') }), _jsx(TableHead, { column: "tokens", children: t('releases.detail.results.tokens') }), _jsx(TableHead, { column: "createdAt", children: t('releases.detail.results.createdAt') })] }) }), _jsxs(TableBody, { children: [resultsLoading && rows.length === 0 ? (_jsx(TableEmpty, { children: _jsx(PlatformLoader, { className: "py-1", size: "sm" }) })) : null, resultsQuery.isError ? _jsx(TableEmpty, { children: t('releases.detail.results.loadFailed') }) : null, !resultsQuery.isLoading && !resultsQuery.isError && rows.length === 0 ? (_jsx(TableEmpty, { children: t('releases.detail.results.empty') })) : null, rows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { column: "externalId", truncate: true, className: "font-mono text-[11.5px] text-muted-foreground", children: _jsx("span", { title: row.externalId ?? undefined, children: row.externalId ?? '—' }) }), _jsx(TableCell, { column: "input", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultInput(row, 1000), children: formatReleaseRunResultInput(row, 220) }) }), _jsx(TableCell, { column: "output", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultOutput(row, 1000), children: formatReleaseRunResultOutput(row, 220) }) }), _jsx(TableCell, { column: "source", children: _jsx(ReleaseRunResultLaneBadge, { lane: row.lane }) }), _jsx(TableCell, { column: "variant", className: "text-[12px]", children: _jsx(ReleaseRunResultVariant, { value: row }) }), _jsx(TableCell, { column: "latency", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultLatency(row.latencyMs) }), _jsx(TableCell, { column: "tokens", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultTokens(row) }), _jsx(TableCell, { column: "createdAt", className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(row.createdAt) })] }, `${row.id}:${row.createdAt}`)))] })] }), _jsx(ResourcePaginationFooter, { summary: _jsx("span", { children: t('releases.detail.results.summary')
|
|
1007
|
+
}, disabled: releaseVersionOptions.length === 0 })] })] }), _jsxs(Table, { columns: RESULT_COLUMNS, children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { column: "externalId", children: t('releases.detail.results.externalId') }), _jsx(TableHead, { column: "input", children: t('releases.detail.results.input') }), _jsx(TableHead, { column: "output", children: t('releases.detail.results.output') }), _jsx(TableHead, { column: "source", children: t('releases.detail.results.source') }), _jsx(TableHead, { column: "version", children: t('releases.detail.results.version') }), _jsx(TableHead, { column: "latency", children: t('releases.detail.results.latency') }), _jsx(TableHead, { column: "tokens", children: t('releases.detail.results.tokens') }), _jsx(TableHead, { column: "createdAt", children: t('releases.detail.results.createdAt') })] }) }), _jsxs(TableBody, { children: [resultsLoading && rows.length === 0 ? (_jsx(TableEmpty, { children: _jsx(PlatformLoader, { className: "py-1", size: "sm" }) })) : null, resultsQuery.isError ? _jsx(TableEmpty, { children: t('releases.detail.results.loadFailed') }) : null, !resultsQuery.isLoading && !resultsQuery.isError && rows.length === 0 ? (_jsx(TableEmpty, { children: t('releases.detail.results.empty') })) : null, rows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { column: "externalId", truncate: true, className: "font-mono text-[11.5px] text-muted-foreground", children: _jsx("span", { title: row.externalId ?? undefined, children: row.externalId ?? '—' }) }), _jsx(TableCell, { column: "input", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultInput(row, 1000), children: formatReleaseRunResultInput(row, 220) }) }), _jsx(TableCell, { column: "output", truncate: 2, className: "text-[12px]", children: _jsx("span", { title: formatReleaseRunResultOutput(row, 1000), children: formatReleaseRunResultOutput(row, 220) }) }), _jsx(TableCell, { column: "source", children: _jsx(ReleaseRunResultLaneBadge, { lane: row.lane }) }), _jsx(TableCell, { column: "version", className: "text-[12px]", children: _jsx(ReleaseRunResultVersion, { value: row }) }), _jsx(TableCell, { column: "latency", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultLatency(row.latencyMs) }), _jsx(TableCell, { column: "tokens", className: "font-mono text-[11.5px] text-muted-foreground", children: formatResultTokens(row) }), _jsx(TableCell, { column: "createdAt", className: "font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(row.createdAt) })] }, `${row.id}:${row.createdAt}`)))] })] }), _jsx(ResourcePaginationFooter, { summary: _jsx("span", { children: t('releases.detail.results.summary')
|
|
588
1008
|
.replace('{from}', String(from))
|
|
589
1009
|
.replace('{to}', String(to))
|
|
590
1010
|
.replace('{total}', formatCount(total)) }), pageIndex: pageIndex, pageCount: pageCount, pageSize: pageSize, pageSizeOptions: RESULT_PAGE_SIZE_OPTIONS, previousPageLabel: t('common.previousPage'), nextPageLabel: t('common.nextPage'), onPageChange: setPageIndex, onPageSizeChange: (nextPageSize) => {
|
|
@@ -592,6 +1012,60 @@ function ResultsPane({ projectId, line, releaseEvents, initialReleaseVariantId,
|
|
|
592
1012
|
setPageIndex(0);
|
|
593
1013
|
} })] }));
|
|
594
1014
|
}
|
|
1015
|
+
function ResultReleaseVersionSelect({ id, options, value, onChange, disabled, }) {
|
|
1016
|
+
const { t } = useI18n();
|
|
1017
|
+
const [open, setOpen] = useState(false);
|
|
1018
|
+
const [query, setQuery] = useState('');
|
|
1019
|
+
const selectedOption = value === 'all' ? null : (options.find((option) => option.id === value) ?? null);
|
|
1020
|
+
const allLabel = t('releases.detail.results.versionFilter.all');
|
|
1021
|
+
const triggerLabel = selectedOption?.label ?? allLabel;
|
|
1022
|
+
const triggerDetail = selectedOption?.detail ?? null;
|
|
1023
|
+
const normalizedQuery = normalizeResultVersionSearch(query);
|
|
1024
|
+
const allOptionVisible = !normalizedQuery || resultVersionSearchIncludes(normalizedQuery, [allLabel, 'all']);
|
|
1025
|
+
const filteredOptions = useMemo(() => {
|
|
1026
|
+
if (!normalizedQuery)
|
|
1027
|
+
return options;
|
|
1028
|
+
return options.filter((option) => resultVersionSearchIncludes(normalizedQuery, [
|
|
1029
|
+
option.id,
|
|
1030
|
+
option.label,
|
|
1031
|
+
option.promptVersion,
|
|
1032
|
+
option.model,
|
|
1033
|
+
option.detail,
|
|
1034
|
+
]));
|
|
1035
|
+
}, [normalizedQuery, options]);
|
|
1036
|
+
function select(next) {
|
|
1037
|
+
onChange(next);
|
|
1038
|
+
setOpen(false);
|
|
1039
|
+
setQuery('');
|
|
1040
|
+
}
|
|
1041
|
+
return (_jsxs(Popover, { open: open, onOpenChange: (next) => {
|
|
1042
|
+
setOpen(next);
|
|
1043
|
+
if (!next)
|
|
1044
|
+
setQuery('');
|
|
1045
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "data-testid": "release-result-version-filter", className: "h-auto min-h-10 w-full justify-between px-3 py-2 text-left sm:w-[340px]", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-mono text-[13px] font-semibold", children: triggerLabel }), triggerDetail ? (_jsx("span", { className: "mt-0.5 block truncate text-[11px] font-normal text-muted-foreground", children: triggerDetail })) : null] }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 6, className: "w-[calc(100vw-2rem)] p-0 sm:w-[720px]", children: [_jsx(ResultDropdownSearchInput, { value: query, onChange: setQuery, placeholder: t('releases.detail.results.versionDropdown.search') }), _jsxs("div", { className: "max-h-[360px] overflow-y-auto p-1.5", children: [allOptionVisible ? (_jsxs("button", { type: "button", "data-testid": "release-result-version-option-all", onClick: () => select('all'), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', value === 'all' && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: value === 'all' }), _jsx("span", { className: "min-w-0 flex-1", children: _jsx("span", { className: "block min-w-0 truncate font-mono text-[13px] font-semibold", children: allLabel }) })] })) : null, filteredOptions.length === 0 && !allOptionVisible ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t('releases.detail.results.versionDropdown.noMatches') })) : (filteredOptions.map((option) => {
|
|
1046
|
+
const selected = option.id === value;
|
|
1047
|
+
return (_jsxs("button", { type: "button", "data-testid": `release-result-version-option-${option.id}`, onClick: () => select(option.id), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block min-w-0 truncate font-mono text-[13px] font-semibold", children: option.label }), _jsxs("span", { className: "mt-1 grid gap-x-4 gap-y-1 text-[11.5px] text-muted-foreground sm:grid-cols-3", children: [_jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.promptVersion'), value: option.promptVersion }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.model'), value: option.model }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.versionId'), value: formatShortId(option.id) }) })] })] })] }, option.id));
|
|
1048
|
+
}))] })] })] }));
|
|
1049
|
+
}
|
|
1050
|
+
function ResultDropdownSearchInput({ value, onChange, placeholder, }) {
|
|
1051
|
+
return (_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Search, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: value, onChange: (event) => onChange(event.target.value), onKeyDown: (event) => event.stopPropagation(), placeholder: placeholder, "data-testid": "release-result-version-search", className: "h-8 border-0 bg-transparent px-0 text-[13px] shadow-none focus-visible:ring-0 focus-visible:ring-offset-0" })] }));
|
|
1052
|
+
}
|
|
1053
|
+
function ResultVersionSelectionCheck({ selected }) {
|
|
1054
|
+
return (_jsx("span", { className: cn('mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-full border', selected ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/35 bg-background'), "aria-hidden": "true", children: _jsx(Check, { className: cn('size-3', selected ? 'opacity-100' : 'opacity-0') }) }));
|
|
1055
|
+
}
|
|
1056
|
+
function ResultDropdownFieldLabel({ label, value }) {
|
|
1057
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "mx-1 text-muted-foreground/60", children: "-" }), _jsx("span", { className: "text-foreground", children: value })] }));
|
|
1058
|
+
}
|
|
1059
|
+
function normalizeResultVersionSearch(value) {
|
|
1060
|
+
return value.trim().toLowerCase();
|
|
1061
|
+
}
|
|
1062
|
+
function resultVersionSearchIncludes(query, parts) {
|
|
1063
|
+
return parts
|
|
1064
|
+
.filter((part) => part !== null && part !== undefined)
|
|
1065
|
+
.join(' ')
|
|
1066
|
+
.toLowerCase()
|
|
1067
|
+
.includes(query);
|
|
1068
|
+
}
|
|
595
1069
|
function getReleaseLineEventSourceIds(line, releaseEvents) {
|
|
596
1070
|
const ids = [
|
|
597
1071
|
...releaseEvents.flatMap((event) => [
|
|
@@ -628,103 +1102,7 @@ function getReleaseLineEventSources(line, releaseEvents) {
|
|
|
628
1102
|
function getReleaseResultSourceIds(line, releaseEvents) {
|
|
629
1103
|
return getReleaseLineEventSourceIds(line, releaseEvents);
|
|
630
1104
|
}
|
|
631
|
-
function
|
|
632
|
-
const baseById = new Map();
|
|
633
|
-
const eventsByVariant = new Map();
|
|
634
|
-
const addVariant = (variant) => {
|
|
635
|
-
baseById.set(variant.id, {
|
|
636
|
-
id: variant.id,
|
|
637
|
-
variantNumber: variant.variantNumber,
|
|
638
|
-
label: variant.label,
|
|
639
|
-
promptName: variant.promptName,
|
|
640
|
-
promptVersionId: variant.promptVersionId,
|
|
641
|
-
promptVersionLabel: variant.promptVersionLabel,
|
|
642
|
-
modelId: variant.modelId,
|
|
643
|
-
modelName: variant.modelName,
|
|
644
|
-
modelProvider: variant.modelProvider,
|
|
645
|
-
createdAt: variant.createdAt,
|
|
646
|
-
updatedAt: variant.updatedAt,
|
|
647
|
-
});
|
|
648
|
-
};
|
|
649
|
-
for (const variant of line.variants)
|
|
650
|
-
addVariant(variant);
|
|
651
|
-
for (const event of releaseEvents) {
|
|
652
|
-
if (!event.releaseVariantId)
|
|
653
|
-
continue;
|
|
654
|
-
const events = eventsByVariant.get(event.releaseVariantId) ?? [];
|
|
655
|
-
events.push(event);
|
|
656
|
-
eventsByVariant.set(event.releaseVariantId, events);
|
|
657
|
-
if (!baseById.has(event.releaseVariantId)) {
|
|
658
|
-
baseById.set(event.releaseVariantId, {
|
|
659
|
-
id: event.releaseVariantId,
|
|
660
|
-
variantNumber: event.releaseVariantNumber,
|
|
661
|
-
label: event.releaseVariantLabel ?? formatShortId(event.releaseVariantId),
|
|
662
|
-
promptName: event.promptName,
|
|
663
|
-
promptVersionId: event.promptVersionId,
|
|
664
|
-
promptVersionLabel: event.promptVersionLabel,
|
|
665
|
-
modelId: event.modelId,
|
|
666
|
-
modelName: event.modelName,
|
|
667
|
-
modelProvider: event.modelProvider,
|
|
668
|
-
createdAt: event.createdAt,
|
|
669
|
-
updatedAt: event.updatedAt,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
const currentProductionVariantId = releaseEvents.find((event) => event.id === line.production?.currentEvent?.id)?.releaseVariantId ?? null;
|
|
674
|
-
const activeCanaryVariantId = line.canary?.releaseVariantId ??
|
|
675
|
-
releaseEvents.find((event) => event.id === line.canary?.id)?.releaseVariantId ??
|
|
676
|
-
null;
|
|
677
|
-
return [...baseById.values()]
|
|
678
|
-
.map((base) => {
|
|
679
|
-
const events = (eventsByVariant.get(base.id) ?? []).sort((left, right) => timeValue(right.createdAt) - timeValue(left.createdAt));
|
|
680
|
-
return {
|
|
681
|
-
...base,
|
|
682
|
-
createdAt: minDateString([base.createdAt, ...events.map((event) => event.createdAt)]),
|
|
683
|
-
updatedAt: maxDateString([base.updatedAt, ...events.map((event) => event.updatedAt ?? event.createdAt)]),
|
|
684
|
-
stage: resolveReleaseVariantStage(base.id, currentProductionVariantId, activeCanaryVariantId, events),
|
|
685
|
-
events,
|
|
686
|
-
productionEventCount: events.filter((event) => event.laneType === 'production').length,
|
|
687
|
-
canaryEventCount: events.filter((event) => event.laneType === 'canary').length,
|
|
688
|
-
totalProcessed: events.reduce((sum, event) => sum + event.totalProcessed, 0),
|
|
689
|
-
totalErrors: events.reduce((sum, event) => sum + event.totalErrors, 0),
|
|
690
|
-
};
|
|
691
|
-
})
|
|
692
|
-
.sort((left, right) => {
|
|
693
|
-
if (left.variantNumber !== null && right.variantNumber !== null)
|
|
694
|
-
return left.variantNumber - right.variantNumber;
|
|
695
|
-
if (left.variantNumber !== null)
|
|
696
|
-
return -1;
|
|
697
|
-
if (right.variantNumber !== null)
|
|
698
|
-
return 1;
|
|
699
|
-
return left.label.localeCompare(right.label, undefined, { numeric: true });
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
function resolveReleaseVariantStage(releaseVariantId, currentProductionVariantId, activeCanaryVariantId, events) {
|
|
703
|
-
const isProduction = currentProductionVariantId === releaseVariantId ||
|
|
704
|
-
events.some((event) => event.laneType === 'production' && event.status === 'running');
|
|
705
|
-
const isCanary = activeCanaryVariantId === releaseVariantId ||
|
|
706
|
-
events.some((event) => event.laneType === 'canary' && (event.status === 'running' || event.status === 'stopped'));
|
|
707
|
-
if (isProduction && isCanary)
|
|
708
|
-
return 'production_canary';
|
|
709
|
-
if (isProduction)
|
|
710
|
-
return 'production';
|
|
711
|
-
if (isCanary)
|
|
712
|
-
return 'canary';
|
|
713
|
-
return 'history';
|
|
714
|
-
}
|
|
715
|
-
function minDateString(values) {
|
|
716
|
-
const dates = values.filter((value) => Boolean(value));
|
|
717
|
-
if (dates.length === 0)
|
|
718
|
-
return null;
|
|
719
|
-
return dates.reduce((min, value) => (timeValue(value) < timeValue(min) ? value : min));
|
|
720
|
-
}
|
|
721
|
-
function maxDateString(values) {
|
|
722
|
-
const dates = values.filter((value) => Boolean(value));
|
|
723
|
-
if (dates.length === 0)
|
|
724
|
-
return null;
|
|
725
|
-
return dates.reduce((max, value) => (timeValue(value) > timeValue(max) ? value : max));
|
|
726
|
-
}
|
|
727
|
-
function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
1105
|
+
function getReleaseResultVersionOptions(line, releaseEvents) {
|
|
728
1106
|
const options = new Map();
|
|
729
1107
|
const add = (input) => {
|
|
730
1108
|
if (!input.id)
|
|
@@ -734,23 +1112,25 @@ function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
|
734
1112
|
options.set(input.id, {
|
|
735
1113
|
id: input.id,
|
|
736
1114
|
label: input.label?.trim() || formatShortId(input.id),
|
|
1115
|
+
promptVersion,
|
|
1116
|
+
model,
|
|
737
1117
|
detail: `${promptVersion} · ${model}`,
|
|
738
1118
|
});
|
|
739
1119
|
};
|
|
740
|
-
for (const
|
|
1120
|
+
for (const version of line.versions) {
|
|
741
1121
|
add({
|
|
742
|
-
id:
|
|
743
|
-
label:
|
|
744
|
-
promptVersionLabel:
|
|
745
|
-
promptVersionId:
|
|
746
|
-
modelName:
|
|
747
|
-
modelId:
|
|
1122
|
+
id: version.id,
|
|
1123
|
+
label: version.label,
|
|
1124
|
+
promptVersionLabel: version.promptVersionLabel,
|
|
1125
|
+
promptVersionId: version.promptVersionId,
|
|
1126
|
+
modelName: version.modelName,
|
|
1127
|
+
modelId: version.modelId,
|
|
748
1128
|
});
|
|
749
1129
|
}
|
|
750
1130
|
for (const event of releaseEvents) {
|
|
751
1131
|
add({
|
|
752
|
-
id: event.
|
|
753
|
-
label: event.
|
|
1132
|
+
id: event.releaseVersionId,
|
|
1133
|
+
label: event.releaseVersionLabel,
|
|
754
1134
|
promptVersionLabel: event.promptVersionLabel,
|
|
755
1135
|
promptVersionId: event.promptVersionId,
|
|
756
1136
|
modelName: event.modelName,
|
|
@@ -759,20 +1139,6 @@ function getReleaseResultVariantOptions(line, releaseEvents) {
|
|
|
759
1139
|
}
|
|
760
1140
|
return [...options.values()].sort((left, right) => left.label.localeCompare(right.label, undefined, { numeric: true }));
|
|
761
1141
|
}
|
|
762
|
-
function getReleaseResultPromptVersionOptions(line, releaseEvents) {
|
|
763
|
-
const options = new Map();
|
|
764
|
-
const add = (id, label) => {
|
|
765
|
-
if (!id)
|
|
766
|
-
return;
|
|
767
|
-
options.set(id, label?.trim() || formatShortId(id));
|
|
768
|
-
};
|
|
769
|
-
add(line.production?.currentEvent?.promptVersionId, line.productionVersionLabel);
|
|
770
|
-
add(line.canary?.promptVersionId, line.canaryVersionLabel);
|
|
771
|
-
for (const event of releaseEvents) {
|
|
772
|
-
add(event.promptVersionId, event.promptVersionLabel);
|
|
773
|
-
}
|
|
774
|
-
return [...options.entries()].map(([id, label]) => ({ id, label }));
|
|
775
|
-
}
|
|
776
1142
|
function formatReleaseRunResultInput(row, maxLength) {
|
|
777
1143
|
return compactReleaseRunResultValue(row.inputVariables, maxLength);
|
|
778
1144
|
}
|
|
@@ -849,125 +1215,1314 @@ function ReleaseRunResultLaneBadge({ lane }) {
|
|
|
849
1215
|
: 'color-mix(in srgb, var(--src-canary) 30%, transparent)',
|
|
850
1216
|
}, children: t(isProduction ? 'releases.detail.results.lane.production' : 'releases.detail.results.lane.canary') }));
|
|
851
1217
|
}
|
|
852
|
-
function
|
|
853
|
-
const label = value.
|
|
1218
|
+
function ReleaseRunResultVersion({ value }) {
|
|
1219
|
+
const label = value.releaseVersionLabel ?? formatShortId(value.releaseVersionId);
|
|
854
1220
|
const promptVersion = formatReleaseRunResultPromptVersion(value);
|
|
855
1221
|
const model = value.modelName ?? formatShortId(value.modelId);
|
|
856
1222
|
return (_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-mono text-[11.5px] font-semibold", children: label }), _jsxs("div", { className: "mt-0.5 truncate text-[11.5px] text-muted-foreground", children: [promptVersion, " \u00B7 ", model] })] }));
|
|
857
1223
|
}
|
|
858
|
-
function QualityMetricsPane({
|
|
1224
|
+
function QualityMetricsPane({ line, releaseEvents }) {
|
|
859
1225
|
const { t } = useI18n();
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1226
|
+
const [selectedVersionIds, setSelectedVersionIds] = useState(null);
|
|
1227
|
+
const [selectedScopeIds, setSelectedScopeIds] = useState(null);
|
|
1228
|
+
const [selectedMetricIds, setSelectedMetricIds] = useState(['f1']);
|
|
1229
|
+
const overallLabel = t('releases.detail.quality.scope.overall');
|
|
1230
|
+
const qualityPoints = useMemo(() => buildReleaseQualityPoints(releaseEvents, overallLabel), [overallLabel, releaseEvents]);
|
|
1231
|
+
const versionOptions = useMemo(() => buildQualityVersionOptions(qualityPoints), [qualityPoints]);
|
|
1232
|
+
const scopeOptions = useMemo(() => buildQualityScopeOptions(qualityPoints), [qualityPoints]);
|
|
1233
|
+
const metricOptions = useMemo(() => QUALITY_METRIC_OPTIONS.map((option) => ({ id: option.key, label: t(option.labelKey) })), [t]);
|
|
1234
|
+
const activeVersionIds = useMemo(() => {
|
|
1235
|
+
const available = versionOptions.map((option) => option.id);
|
|
1236
|
+
if (selectedVersionIds === null)
|
|
1237
|
+
return available;
|
|
1238
|
+
return selectedVersionIds.filter((id) => available.includes(id));
|
|
1239
|
+
}, [selectedVersionIds, versionOptions]);
|
|
1240
|
+
const activeScopes = resolveActiveQualityScopes(selectedScopeIds, scopeOptions);
|
|
1241
|
+
const activeMetrics = resolveActiveQualityMetrics(selectedMetricIds, metricOptions);
|
|
1242
|
+
const visiblePoints = useMemo(() => filterQualityPoints(qualityPoints, activeVersionIds, activeScopes), [activeScopes, activeVersionIds, qualityPoints]);
|
|
1243
|
+
const chartSeries = useMemo(() => buildQualityChartSeries(visiblePoints, activeMetrics, activeScopes), [activeMetrics, activeScopes, visiblePoints]);
|
|
1244
|
+
const chartAxisData = useMemo(() => buildQualityChartAxisData(chartSeries), [chartSeries]);
|
|
1245
|
+
const annotationHref = buildQualityAnnotationHref(line, releaseEvents);
|
|
1246
|
+
return (_jsxs("section", { className: "space-y-4", "data-testid": "release-quality-metrics-pane", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-[14px] font-semibold", children: t('releases.detail.quality.title') }), _jsx("p", { className: "mt-1 max-w-3xl text-[12px] leading-5 text-muted-foreground", children: t('releases.detail.quality.description') })] }), _jsxs("div", { className: "overflow-hidden rounded-lg border bg-card shadow-sm", children: [qualityPoints.length > 0 ? (_jsxs("div", { className: "flex flex-wrap items-end gap-3 border-b px-4 py-3", children: [_jsx(QualityVersionFilter, { options: versionOptions, activeIds: activeVersionIds, onChange: setSelectedVersionIds }), _jsx(QualityScopeFilter, { options: scopeOptions, activeIds: activeScopes.map((scope) => scope.id), onChange: setSelectedScopeIds }), _jsx(QualityMetricFilter, { options: metricOptions, activeIds: activeMetrics.map((metric) => metric.id), onChange: setSelectedMetricIds })] })) : null, _jsx("div", { className: "px-4 pb-2 pt-4", children: _jsx(QualityMetricsChart, { axisData: chartAxisData, series: chartSeries, children: chartSeries.length === 0 ? (qualityPoints.length === 0 ? (_jsx(QualityEmptyChartMessage, { annotationHref: annotationHref })) : (_jsx(QualityFilteredEmptyChartMessage, {}))) : null }) }), chartSeries.length > 0 ? _jsx(QualityLegend, { series: chartSeries }) : null] })] }));
|
|
1247
|
+
}
|
|
1248
|
+
function QualityVersionFilter({ options, activeIds, onChange, }) {
|
|
874
1249
|
const { t } = useI18n();
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
];
|
|
878
|
-
|
|
1250
|
+
const [open, setOpen] = useState(false);
|
|
1251
|
+
const [query, setQuery] = useState('');
|
|
1252
|
+
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
1253
|
+
const normalizedQuery = normalizeQualitySearch(query);
|
|
1254
|
+
const latestOption = useMemo(() => [...options].sort((left, right) => timeValue(right.latestAt) - timeValue(left.latestAt))[0] ?? null, [options]);
|
|
1255
|
+
const productionVersionIds = useMemo(() => options.filter((option) => option.kind === 'production').map((option) => option.id), [options]);
|
|
1256
|
+
const canaryVersionIds = useMemo(() => options.filter((option) => option.kind !== 'production').map((option) => option.id), [options]);
|
|
1257
|
+
const selectedOptions = options.filter((option) => activeSet.has(option.id));
|
|
1258
|
+
const allSelected = options.length > 0 && selectedOptions.length === options.length;
|
|
1259
|
+
const productionSelected = productionVersionIds.length > 0 &&
|
|
1260
|
+
selectedOptions.length === productionVersionIds.length &&
|
|
1261
|
+
productionVersionIds.every((id) => activeSet.has(id));
|
|
1262
|
+
const canarySelected = canaryVersionIds.length > 0 &&
|
|
1263
|
+
selectedOptions.length === canaryVersionIds.length &&
|
|
1264
|
+
canaryVersionIds.every((id) => activeSet.has(id));
|
|
1265
|
+
let triggerLabel = formatTemplate(t('releases.detail.quality.filter.selectedVersions'), {
|
|
1266
|
+
count: formatCount(selectedOptions.length),
|
|
1267
|
+
});
|
|
1268
|
+
if (allSelected) {
|
|
1269
|
+
triggerLabel = t('releases.detail.quality.filter.allVersions');
|
|
1270
|
+
}
|
|
1271
|
+
else if (productionSelected) {
|
|
1272
|
+
triggerLabel = t('releases.detail.quality.filter.allProductionVersions');
|
|
1273
|
+
}
|
|
1274
|
+
else if (canarySelected) {
|
|
1275
|
+
triggerLabel = t('releases.detail.quality.filter.allCanaryVersions');
|
|
1276
|
+
}
|
|
1277
|
+
else if (selectedOptions.length === 1) {
|
|
1278
|
+
triggerLabel = selectedOptions[0]?.label ?? triggerLabel;
|
|
1279
|
+
}
|
|
1280
|
+
const filteredOptions = useMemo(() => {
|
|
1281
|
+
if (!normalizedQuery)
|
|
1282
|
+
return options;
|
|
1283
|
+
return options.filter((option) => qualitySearchIncludes(normalizedQuery, [option.label, option.promptVersion, option.model, option.id]));
|
|
1284
|
+
}, [normalizedQuery, options]);
|
|
1285
|
+
function commit(next) {
|
|
1286
|
+
onChange(next.length === options.length ? null : next);
|
|
1287
|
+
}
|
|
1288
|
+
return (_jsxs("div", { className: "flex min-w-[220px] flex-col gap-1.5", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('releases.detail.quality.filter.version') }), _jsxs(Popover, { open: open, onOpenChange: (next) => {
|
|
1289
|
+
setOpen(next);
|
|
1290
|
+
if (!next)
|
|
1291
|
+
setQuery('');
|
|
1292
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: options.length === 0, className: "h-10 justify-between gap-2 px-3 text-left", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-[13px] font-semibold", children: triggerLabel }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[calc(100vw-2rem)] p-0 sm:w-[560px]", children: [_jsx(ResultDropdownSearchInput, { value: query, onChange: setQuery, placeholder: t('releases.detail.quality.filter.versionSearch') }), _jsxs("div", { className: "flex flex-wrap gap-x-3 gap-y-1.5 border-b px-3 py-2 text-[12px]", children: [_jsx("button", { type: "button", className: "font-medium text-primary", onClick: () => onChange(null), children: t('releases.detail.quality.filter.selectAll') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: !latestOption, onClick: () => {
|
|
1293
|
+
if (latestOption)
|
|
1294
|
+
onChange([latestOption.id]);
|
|
1295
|
+
}, children: t('releases.detail.quality.filter.latestOnly') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: productionVersionIds.length === 0, onClick: () => commit(productionVersionIds), children: t('releases.detail.quality.filter.allProductionVersions') }), _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: canaryVersionIds.length === 0, onClick: () => commit(canaryVersionIds), children: t('releases.detail.quality.filter.allCanaryVersions') })] }), _jsx("div", { className: "max-h-[320px] overflow-y-auto p-1.5", children: filteredOptions.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t('releases.detail.quality.filter.noVersions') })) : (filteredOptions.map((option) => {
|
|
1296
|
+
const selected = activeSet.has(option.id);
|
|
1297
|
+
return (_jsxs("button", { type: "button", onClick: () => commit(toggleQualityFilterValue(activeIds, option.id)), className: cn('flex w-full items-start gap-3 rounded-md px-3 py-2.5 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-mono text-[13px] font-semibold", children: option.label }), _jsx(QualityVersionKindBadge, { kind: option.kind })] }), _jsxs("span", { className: "mt-1 grid gap-x-4 gap-y-1 text-[11.5px] text-muted-foreground sm:grid-cols-3", children: [_jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.promptVersion'), value: option.promptVersion }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.results.versionDropdown.model'), value: option.model }) }), _jsx("span", { className: "min-w-0 truncate", children: _jsx(ResultDropdownFieldLabel, { label: t('releases.detail.quality.filter.points'), value: formatCount(option.pointCount) }) })] })] })] }, option.id));
|
|
1298
|
+
})) })] })] })] }));
|
|
1299
|
+
}
|
|
1300
|
+
function QualityScopeFilter({ options, activeIds, onChange, }) {
|
|
1301
|
+
return (_jsx(QualityMultiSelectFilter, { labelKey: "releases.detail.quality.filter.scope", options: options, activeIds: activeIds, onChange: onChange, allLabelKey: "releases.detail.quality.filter.allScopes", selectedLabelKey: "releases.detail.quality.filter.selectedScopes", emptyLabelKey: "releases.detail.quality.filter.scopeEmpty", minWidthClassName: "min-w-[190px]" }));
|
|
879
1302
|
}
|
|
880
|
-
function
|
|
1303
|
+
function QualityMetricFilter({ options, activeIds, onChange, }) {
|
|
1304
|
+
return (_jsx(QualityMultiSelectFilter, { labelKey: "releases.detail.quality.filter.metric", options: options, activeIds: activeIds, onChange: onChange, allLabelKey: "releases.detail.quality.filter.allMetrics", selectedLabelKey: "releases.detail.quality.filter.selectedMetrics", emptyLabelKey: "releases.detail.quality.filter.metricEmpty", minWidthClassName: "min-w-[170px]" }));
|
|
1305
|
+
}
|
|
1306
|
+
function QualityMultiSelectFilter({ labelKey, options, activeIds, onChange, allLabelKey, selectedLabelKey, emptyLabelKey, minWidthClassName, }) {
|
|
881
1307
|
const { t } = useI18n();
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1308
|
+
const [open, setOpen] = useState(false);
|
|
1309
|
+
const activeSet = useMemo(() => new Set(activeIds), [activeIds]);
|
|
1310
|
+
const selectedOptions = options.filter((option) => activeSet.has(option.id));
|
|
1311
|
+
const allSelected = options.length > 0 && selectedOptions.length === options.length;
|
|
1312
|
+
const triggerLabel = allSelected
|
|
1313
|
+
? t(allLabelKey)
|
|
1314
|
+
: selectedOptions.length === 1
|
|
1315
|
+
? selectedOptions[0]?.label
|
|
1316
|
+
: selectedOptions.length > 1
|
|
1317
|
+
? formatTemplate(t(selectedLabelKey), { count: formatCount(selectedOptions.length) })
|
|
1318
|
+
: t(emptyLabelKey);
|
|
1319
|
+
function commit(next) {
|
|
1320
|
+
if (next.length === 0)
|
|
1321
|
+
return;
|
|
1322
|
+
onChange(next);
|
|
1323
|
+
}
|
|
1324
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-1.5', minWidthClassName), children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t(labelKey) }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: options.length === 0, className: "h-10 justify-between gap-2 px-3 text-left", children: [_jsx("span", { className: "min-w-0 truncate text-[13px] font-semibold", children: triggerLabel }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-64 p-0", children: [_jsx("div", { className: "flex gap-3 border-b px-3 py-2 text-[12px]", children: _jsx("button", { type: "button", className: "font-medium text-primary disabled:text-muted-foreground", disabled: allSelected, onClick: () => onChange(options.map((option) => option.id)), children: t('releases.detail.quality.filter.selectAll') }) }), _jsx("div", { className: "max-h-[260px] overflow-y-auto p-1.5", children: options.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-[12px] text-muted-foreground", children: t(emptyLabelKey) })) : (options.map((option) => {
|
|
1325
|
+
const selected = activeSet.has(option.id);
|
|
1326
|
+
return (_jsxs("button", { type: "button", onClick: () => commit(toggleQualityFilterValue(activeIds, option.id)), className: cn('flex w-full items-center gap-3 rounded-md px-3 py-2 text-left hover:bg-accent', selected && 'bg-primary/5'), children: [_jsx(ResultVersionSelectionCheck, { selected: selected }), _jsx("span", { className: "min-w-0 flex-1 truncate text-[13px] font-medium", children: option.label }), option.meta ? (_jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: option.meta })) : null] }, option.id));
|
|
1327
|
+
})) })] })] })] }));
|
|
1328
|
+
}
|
|
1329
|
+
function QualityVersionKindBadge({ kind }) {
|
|
1330
|
+
const { t } = useI18n();
|
|
1331
|
+
const isProduction = kind === 'production';
|
|
1332
|
+
return (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-[10.5px] font-medium leading-4", style: {
|
|
1333
|
+
background: isProduction ? 'var(--src-prod-soft)' : 'var(--src-canary-soft)',
|
|
1334
|
+
color: isProduction ? 'var(--src-prod-fg)' : 'var(--src-canary-fg)',
|
|
1335
|
+
borderColor: isProduction
|
|
1336
|
+
? 'color-mix(in srgb, var(--src-prod) 30%, transparent)'
|
|
1337
|
+
: 'color-mix(in srgb, var(--src-canary) 30%, transparent)',
|
|
1338
|
+
}, children: t(isProduction
|
|
1339
|
+
? 'releases.detail.history.versionKind.production'
|
|
1340
|
+
: 'releases.detail.history.versionKind.candidate') }));
|
|
1341
|
+
}
|
|
1342
|
+
function QualityLegend({ series }) {
|
|
1343
|
+
const { t } = useI18n();
|
|
1344
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2 border-t px-4 py-3 text-[11.5px] text-muted-foreground", children: [series.map((item) => (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "h-0.5 w-5 rounded-full", style: { background: item.color }, "aria-hidden": true }), item.label] }, item.id))), _jsxs("span", { className: "inline-flex items-center gap-1.5 border-l pl-4", children: [_jsx("span", { className: "size-2.5 rounded-full bg-[var(--src-prod)]", "aria-hidden": true }), t('releases.detail.quality.legend.production')] }), _jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "size-2.5 rounded-full border-2 border-[var(--src-canary)] bg-card", "aria-hidden": true }), t('releases.detail.quality.legend.canary')] })] }));
|
|
1345
|
+
}
|
|
1346
|
+
function readChartCssColor(name, fallback) {
|
|
1347
|
+
if (typeof window === 'undefined')
|
|
1348
|
+
return fallback;
|
|
1349
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
1350
|
+
return value || fallback;
|
|
1351
|
+
}
|
|
1352
|
+
function resolveChartColor(value, fallback) {
|
|
1353
|
+
const variableName = value.match(/var\((--[^),\s]+)/)?.[1];
|
|
1354
|
+
return variableName ? readChartCssColor(variableName, fallback) : value;
|
|
1355
|
+
}
|
|
1356
|
+
function escapeHtml(value) {
|
|
1357
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
1358
|
+
const escaped = {
|
|
1359
|
+
'&': '&',
|
|
1360
|
+
'<': '<',
|
|
1361
|
+
'>': '>',
|
|
1362
|
+
'"': '"',
|
|
1363
|
+
"'": ''',
|
|
1364
|
+
};
|
|
1365
|
+
return escaped[char] ?? char;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function resolveQualityPercentAxisExtent(extent) {
|
|
1369
|
+
if (!Number.isFinite(extent.min) || !Number.isFinite(extent.max)) {
|
|
1370
|
+
return { min: 0, max: 100 };
|
|
1371
|
+
}
|
|
1372
|
+
const dataMin = Math.max(0, Math.min(100, Math.min(extent.min, extent.max)));
|
|
1373
|
+
const dataMax = Math.max(0, Math.min(100, Math.max(extent.min, extent.max)));
|
|
1374
|
+
const dataSpan = dataMax - dataMin;
|
|
1375
|
+
const padding = Math.max(2, dataSpan * 0.2);
|
|
1376
|
+
let nextMin = Math.max(0, dataMin - padding);
|
|
1377
|
+
let nextMax = Math.min(100, dataMax + padding);
|
|
1378
|
+
if (nextMax - nextMin < 10) {
|
|
1379
|
+
const center = (dataMin + dataMax) / 2;
|
|
1380
|
+
nextMin = Math.max(0, center - 5);
|
|
1381
|
+
nextMax = Math.min(100, center + 5);
|
|
1382
|
+
if (nextMin === 0)
|
|
1383
|
+
nextMax = Math.min(100, Math.max(10, nextMax));
|
|
1384
|
+
if (nextMax === 100)
|
|
1385
|
+
nextMin = Math.max(0, Math.min(90, nextMin));
|
|
1386
|
+
}
|
|
1387
|
+
const roundedMin = Math.max(0, Math.floor(nextMin / 5) * 5);
|
|
1388
|
+
const roundedMax = Math.min(100, Math.ceil(nextMax / 5) * 5);
|
|
1389
|
+
return roundedMax > roundedMin ? { min: roundedMin, max: roundedMax } : { min: 0, max: 100 };
|
|
1390
|
+
}
|
|
1391
|
+
function getQualityPercentAxisMin(extent) {
|
|
1392
|
+
return resolveQualityPercentAxisExtent(extent).min;
|
|
1393
|
+
}
|
|
1394
|
+
function getQualityPercentAxisMax(extent) {
|
|
1395
|
+
return resolveQualityPercentAxisExtent(extent).max;
|
|
1396
|
+
}
|
|
1397
|
+
function QualityMetricsChart({ axisData, series, children, }) {
|
|
896
1398
|
const { t } = useI18n();
|
|
897
1399
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1400
|
+
const chartRef = useRef(null);
|
|
1401
|
+
const chartInstanceRef = useRef(null);
|
|
1402
|
+
const [chartColorVersion, setChartColorVersion] = useState(0);
|
|
1403
|
+
useEffect(() => {
|
|
1404
|
+
if (typeof MutationObserver === 'undefined')
|
|
1405
|
+
return undefined;
|
|
1406
|
+
const observer = new MutationObserver(() => setChartColorVersion((current) => current + 1));
|
|
1407
|
+
observer.observe(document.documentElement, {
|
|
1408
|
+
attributes: true,
|
|
1409
|
+
attributeFilter: ['class', 'style', 'data-theme'],
|
|
1410
|
+
});
|
|
1411
|
+
return () => observer.disconnect();
|
|
1412
|
+
}, []);
|
|
1413
|
+
const chartOption = useMemo(() => {
|
|
1414
|
+
void chartColorVersion;
|
|
1415
|
+
const foregroundColor = readChartCssColor('--foreground', '#e5e7eb');
|
|
1416
|
+
const mutedColor = readChartCssColor('--muted-foreground', '#94a3b8');
|
|
1417
|
+
const borderColor = readChartCssColor('--border', '#1f2937');
|
|
1418
|
+
const cardColor = readChartCssColor('--card', '#020617');
|
|
1419
|
+
const popoverColor = readChartCssColor('--popover', cardColor);
|
|
1420
|
+
const primaryColor = readChartCssColor('--primary', '#60a5fa');
|
|
1421
|
+
const productionColor = readChartCssColor('--src-prod', '#22c55e');
|
|
1422
|
+
const canaryColor = readChartCssColor('--src-canary', '#3b82f6');
|
|
1423
|
+
const categoryLabels = axisData.map((point) => point.releaseVersionLabel);
|
|
1424
|
+
const qualitySeries = series.map((item) => {
|
|
1425
|
+
const pointByEvent = new Map(item.points.map((point) => [point.eventId, point]));
|
|
1426
|
+
const lineColor = resolveChartColor(item.color, primaryColor);
|
|
1427
|
+
return {
|
|
1428
|
+
id: item.id,
|
|
1429
|
+
name: item.label,
|
|
1430
|
+
type: 'line',
|
|
1431
|
+
smooth: true,
|
|
1432
|
+
connectNulls: true,
|
|
1433
|
+
showSymbol: true,
|
|
1434
|
+
symbol: 'circle',
|
|
1435
|
+
symbolSize: 8,
|
|
1436
|
+
lineStyle: {
|
|
1437
|
+
width: 2,
|
|
1438
|
+
color: lineColor,
|
|
1439
|
+
opacity: 1,
|
|
1440
|
+
},
|
|
1441
|
+
itemStyle: {
|
|
1442
|
+
color: lineColor,
|
|
1443
|
+
opacity: 1,
|
|
1444
|
+
},
|
|
1445
|
+
emphasis: {
|
|
1446
|
+
disabled: true,
|
|
1447
|
+
},
|
|
1448
|
+
blur: {
|
|
1449
|
+
lineStyle: {
|
|
1450
|
+
opacity: 1,
|
|
1451
|
+
},
|
|
1452
|
+
itemStyle: {
|
|
1453
|
+
opacity: 1,
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
data: axisData.map((axisPoint) => {
|
|
1457
|
+
const point = pointByEvent.get(axisPoint.eventId);
|
|
1458
|
+
if (!point)
|
|
1459
|
+
return null;
|
|
1460
|
+
const laneColor = point.lane === 'production' ? productionColor : canaryColor;
|
|
1461
|
+
return {
|
|
1462
|
+
value: point.value,
|
|
1463
|
+
qualityPoint: point,
|
|
1464
|
+
symbol: 'circle',
|
|
1465
|
+
symbolSize: 8,
|
|
1466
|
+
itemStyle: {
|
|
1467
|
+
color: point.lane === 'production' ? laneColor : cardColor,
|
|
1468
|
+
borderColor: laneColor,
|
|
1469
|
+
borderWidth: 2,
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
}),
|
|
1473
|
+
};
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
animation: false,
|
|
1477
|
+
backgroundColor: 'transparent',
|
|
1478
|
+
grid: {
|
|
1479
|
+
top: 26,
|
|
1480
|
+
right: 24,
|
|
1481
|
+
bottom: axisData.length > 1 ? 62 : 38,
|
|
1482
|
+
left: 48,
|
|
1483
|
+
containLabel: false,
|
|
1484
|
+
},
|
|
1485
|
+
tooltip: {
|
|
1486
|
+
trigger: 'axis',
|
|
1487
|
+
appendToBody: true,
|
|
1488
|
+
confine: true,
|
|
1489
|
+
borderWidth: 1,
|
|
1490
|
+
borderColor,
|
|
1491
|
+
backgroundColor: popoverColor,
|
|
1492
|
+
textStyle: {
|
|
1493
|
+
color: foregroundColor,
|
|
1494
|
+
fontSize: 12,
|
|
1495
|
+
fontFamily: 'inherit',
|
|
1496
|
+
},
|
|
1497
|
+
extraCssText: 'box-shadow: 0 12px 30px rgba(15, 23, 42, 0.22); border-radius: 6px;',
|
|
1498
|
+
axisPointer: {
|
|
1499
|
+
type: 'line',
|
|
1500
|
+
lineStyle: {
|
|
1501
|
+
color: borderColor,
|
|
1502
|
+
type: 'dashed',
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
formatter: (rawParams) => {
|
|
1506
|
+
const params = (Array.isArray(rawParams) ? rawParams : [rawParams]);
|
|
1507
|
+
const points = params
|
|
1508
|
+
.map((param) => param.data?.qualityPoint)
|
|
1509
|
+
.filter((point) => Boolean(point));
|
|
1510
|
+
const point = points[0];
|
|
1511
|
+
if (!point)
|
|
1512
|
+
return '';
|
|
1513
|
+
const kindText = t(point.releaseVersionKind === 'production'
|
|
1514
|
+
? 'releases.detail.history.versionKind.production'
|
|
1515
|
+
: 'releases.detail.history.versionKind.candidate');
|
|
1516
|
+
const rows = params
|
|
1517
|
+
.map((param) => {
|
|
1518
|
+
const qualityPoint = param.data?.qualityPoint;
|
|
1519
|
+
if (!qualityPoint)
|
|
1520
|
+
return '';
|
|
1521
|
+
const sampleCount = qualityPoint.sampleCount === null
|
|
1522
|
+
? '—'
|
|
1523
|
+
: formatTemplate(t('releases.detail.quality.sampleCountShort'), {
|
|
1524
|
+
count: formatCount(qualityPoint.sampleCount),
|
|
1525
|
+
});
|
|
1526
|
+
return [
|
|
1527
|
+
'<div style="display:flex;align-items:center;gap:8px;min-width:260px;margin-top:4px;">',
|
|
1528
|
+
param.marker ?? '',
|
|
1529
|
+
`<span style="min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${mutedColor};">${escapeHtml(param.seriesName ?? qualityPoint.seriesLabel)}</span>`,
|
|
1530
|
+
`<span style="font-family:JetBrains Mono, ui-monospace, monospace;font-weight:600;">${escapeHtml(formatQualityPercent(qualityPoint.value))}</span>`,
|
|
1531
|
+
`<span style="font-family:JetBrains Mono, ui-monospace, monospace;font-size:11px;color:${mutedColor};">${escapeHtml(sampleCount)}</span>`,
|
|
1532
|
+
'</div>',
|
|
1533
|
+
].join('');
|
|
1534
|
+
})
|
|
1535
|
+
.join('');
|
|
1536
|
+
return [
|
|
1537
|
+
'<div style="min-width:280px;">',
|
|
1538
|
+
`<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;font-family:JetBrains Mono, ui-monospace, monospace;font-size:11px;color:${mutedColor};">`,
|
|
1539
|
+
`<span>${escapeHtml(point.xLabel)}</span><span>·</span><span>${escapeHtml(formatDateTimeOrDash(point.updatedAt ?? point.createdAt))}</span>`,
|
|
1540
|
+
'</div>',
|
|
1541
|
+
'<div style="display:flex;align-items:center;gap:8px;min-width:0;">',
|
|
1542
|
+
`<span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;">${escapeHtml(point.eventLabel)}</span>`,
|
|
1543
|
+
`<span style="display:inline-flex;align-items:center;border-radius:999px;border:1px solid ${borderColor};padding:1px 6px;font-size:11px;color:${mutedColor};">${escapeHtml(kindText)}</span>`,
|
|
1544
|
+
'</div>',
|
|
1545
|
+
`<div style="margin-top:2px;font-size:11.5px;color:${mutedColor};">${escapeHtml(point.releaseVersionLabel)} · ${escapeHtml(point.promptVersionLabel)} · ${escapeHtml(point.modelName)}</div>`,
|
|
1546
|
+
`<div style="margin-top:8px;">${rows}</div>`,
|
|
1547
|
+
'</div>',
|
|
1548
|
+
].join('');
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
xAxis: {
|
|
1552
|
+
type: 'category',
|
|
1553
|
+
boundaryGap: false,
|
|
1554
|
+
data: categoryLabels,
|
|
1555
|
+
axisLine: {
|
|
1556
|
+
show: false,
|
|
1557
|
+
},
|
|
1558
|
+
axisTick: {
|
|
1559
|
+
show: false,
|
|
1560
|
+
},
|
|
1561
|
+
axisLabel: {
|
|
1562
|
+
color: mutedColor,
|
|
1563
|
+
fontSize: 10,
|
|
1564
|
+
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
1565
|
+
hideOverlap: true,
|
|
1566
|
+
margin: 10,
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
yAxis: {
|
|
1570
|
+
type: 'value',
|
|
1571
|
+
min: getQualityPercentAxisMin,
|
|
1572
|
+
max: getQualityPercentAxisMax,
|
|
1573
|
+
scale: true,
|
|
1574
|
+
splitNumber: 4,
|
|
1575
|
+
axisLine: {
|
|
1576
|
+
show: false,
|
|
1577
|
+
},
|
|
1578
|
+
axisTick: {
|
|
1579
|
+
show: false,
|
|
1580
|
+
},
|
|
1581
|
+
axisLabel: {
|
|
1582
|
+
color: mutedColor,
|
|
1583
|
+
fontSize: 10,
|
|
1584
|
+
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
|
1585
|
+
formatter: '{value}%',
|
|
1586
|
+
},
|
|
1587
|
+
splitLine: {
|
|
1588
|
+
lineStyle: {
|
|
1589
|
+
color: borderColor,
|
|
1590
|
+
type: 'dashed',
|
|
1591
|
+
opacity: 0.7,
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
dataZoom: axisData.length > 1
|
|
1596
|
+
? [
|
|
1597
|
+
{
|
|
1598
|
+
type: 'inside',
|
|
1599
|
+
xAxisIndex: 0,
|
|
1600
|
+
filterMode: 'filter',
|
|
1601
|
+
zoomOnMouseWheel: true,
|
|
1602
|
+
moveOnMouseWheel: 'shift',
|
|
1603
|
+
moveOnMouseMove: true,
|
|
1604
|
+
preventDefaultMouseMove: true,
|
|
1605
|
+
minValueSpan: 1,
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
type: 'slider',
|
|
1609
|
+
xAxisIndex: 0,
|
|
1610
|
+
filterMode: 'filter',
|
|
1611
|
+
bottom: 10,
|
|
1612
|
+
height: 24,
|
|
1613
|
+
showDetail: false,
|
|
1614
|
+
brushSelect: true,
|
|
1615
|
+
minValueSpan: 1,
|
|
1616
|
+
borderColor,
|
|
1617
|
+
fillerColor: 'rgba(59, 130, 246, 0.18)',
|
|
1618
|
+
backgroundColor: 'transparent',
|
|
1619
|
+
dataBackground: {
|
|
1620
|
+
lineStyle: {
|
|
1621
|
+
color: borderColor,
|
|
1622
|
+
},
|
|
1623
|
+
areaStyle: {
|
|
1624
|
+
color: 'rgba(148, 163, 184, 0.14)',
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
selectedDataBackground: {
|
|
1628
|
+
lineStyle: {
|
|
1629
|
+
color: primaryColor,
|
|
1630
|
+
},
|
|
1631
|
+
areaStyle: {
|
|
1632
|
+
color: 'rgba(59, 130, 246, 0.22)',
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
moveHandleStyle: {
|
|
1636
|
+
color: primaryColor,
|
|
1637
|
+
},
|
|
1638
|
+
handleStyle: {
|
|
1639
|
+
color: cardColor,
|
|
1640
|
+
borderColor: primaryColor,
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
]
|
|
1644
|
+
: [],
|
|
1645
|
+
toolbox: {
|
|
1646
|
+
show: axisData.length > 1,
|
|
1647
|
+
right: 8,
|
|
1648
|
+
top: 0,
|
|
1649
|
+
itemSize: 14,
|
|
1650
|
+
iconStyle: {
|
|
1651
|
+
borderColor: mutedColor,
|
|
1652
|
+
},
|
|
1653
|
+
emphasis: {
|
|
1654
|
+
iconStyle: {
|
|
1655
|
+
borderColor: foregroundColor,
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
feature: {
|
|
1659
|
+
restore: {
|
|
1660
|
+
title: t('releases.detail.quality.chart.resetView'),
|
|
1661
|
+
},
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
series: qualitySeries,
|
|
1665
|
+
};
|
|
1666
|
+
}, [axisData, chartColorVersion, formatDateTimeOrDash, series, t]);
|
|
1667
|
+
useEffect(() => {
|
|
1668
|
+
const element = chartRef.current;
|
|
1669
|
+
if (!element)
|
|
1670
|
+
return undefined;
|
|
1671
|
+
const instance = echarts.init(element, undefined, { renderer: 'svg' });
|
|
1672
|
+
chartInstanceRef.current = instance;
|
|
1673
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
1674
|
+
? null
|
|
1675
|
+
: new ResizeObserver(() => {
|
|
1676
|
+
instance.resize();
|
|
1677
|
+
});
|
|
1678
|
+
resizeObserver?.observe(element);
|
|
1679
|
+
return () => {
|
|
1680
|
+
resizeObserver?.disconnect();
|
|
1681
|
+
chartInstanceRef.current = null;
|
|
1682
|
+
instance.dispose();
|
|
1683
|
+
};
|
|
1684
|
+
}, []);
|
|
1685
|
+
useEffect(() => {
|
|
1686
|
+
chartInstanceRef.current?.setOption(chartOption, true);
|
|
1687
|
+
}, [chartOption]);
|
|
1688
|
+
return (_jsx("div", { className: "relative min-w-0 w-full", children: _jsxs("div", { className: "relative h-[360px] min-w-0 w-full", children: [_jsx("div", { ref: chartRef, className: "h-full w-full", role: "img", "aria-label": t('releases.detail.quality.chartTitle'), "data-testid": "release-quality-echarts-chart" }), children ? (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center px-4", children: _jsx("div", { className: "pointer-events-auto w-full max-w-[360px] rounded-lg border bg-card/95 px-4 py-4 text-center shadow-sm", children: children }) })) : null] }) }));
|
|
1689
|
+
}
|
|
1690
|
+
function QualityEmptyChartMessage({ annotationHref }) {
|
|
1691
|
+
const { t } = useI18n();
|
|
1692
|
+
return (_jsxs("div", { "data-testid": "release-quality-empty", children: [_jsx("div", { className: "text-[14px] font-semibold", children: t('releases.detail.quality.empty') }), _jsx("p", { className: "mx-auto mt-1 max-w-[300px] text-[12px] leading-5 text-muted-foreground", children: t('releases.detail.quality.emptyDescription') }), _jsx(Button, { asChild: true, size: "sm", className: "mt-3 h-8 gap-1.5", children: _jsxs(Link, { href: annotationHref, children: [_jsx(Plus, { className: "size-3.5", "aria-hidden": true }), t('releases.detail.quality.emptyAction')] }) })] }));
|
|
1693
|
+
}
|
|
1694
|
+
function QualityFilteredEmptyChartMessage() {
|
|
1695
|
+
const { t } = useI18n();
|
|
1696
|
+
return (_jsx("div", { className: "text-[13px] font-medium text-muted-foreground", children: t('releases.detail.quality.filteredEmpty') }));
|
|
904
1697
|
}
|
|
905
|
-
function
|
|
1698
|
+
function formatTemplate(template, values) {
|
|
1699
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ''));
|
|
1700
|
+
}
|
|
1701
|
+
const RELEASE_CONFIG_OPERATIONS = new Set([
|
|
1702
|
+
'traffic_updated',
|
|
1703
|
+
'mode_updated',
|
|
1704
|
+
'config_changed',
|
|
1705
|
+
]);
|
|
1706
|
+
export function buildHistoryGroups(line, releaseEvents, productionHistory, t) {
|
|
1707
|
+
if (releaseEvents.length > 0 || line.versions.length > 0) {
|
|
1708
|
+
return buildCanonicalHistoryGroups(line, releaseEvents, t);
|
|
1709
|
+
}
|
|
1710
|
+
return buildLegacyHistoryGroups(line, productionHistory, t);
|
|
1711
|
+
}
|
|
1712
|
+
function buildCanonicalHistoryGroups(line, releaseEvents, t) {
|
|
1713
|
+
const eventsByVersion = groupReleaseEventsByVersion(releaseEvents);
|
|
1714
|
+
const rowsByVersion = new Map();
|
|
1715
|
+
const looseRows = [];
|
|
1716
|
+
for (const version of line.versions) {
|
|
1717
|
+
const events = eventsByVersion.get(version.id) ?? [];
|
|
1718
|
+
rowsByVersion.set(version.id, buildHistoryRowFromVersion(line, version, events, t));
|
|
1719
|
+
}
|
|
1720
|
+
for (const [key, events] of eventsByVersion.entries()) {
|
|
1721
|
+
if (rowsByVersion.has(key))
|
|
1722
|
+
continue;
|
|
1723
|
+
looseRows.push(buildHistoryRowFromEvents(line, events, t));
|
|
1724
|
+
}
|
|
1725
|
+
const grouped = new Map();
|
|
1726
|
+
for (const row of [...rowsByVersion.values(), ...looseRows]) {
|
|
1727
|
+
const key = getHistoryGroupKey(row);
|
|
1728
|
+
const group = ensureHistoryGroup(grouped, key, row);
|
|
1729
|
+
if (isProductionHistoryRow(row)) {
|
|
1730
|
+
group.production = chooseNewestHistoryRow(group.production, row);
|
|
1731
|
+
group.productionNumber = row.productionNumber ?? row.targetProductionNumber ?? group.productionNumber;
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
group.candidates.push(row);
|
|
1735
|
+
}
|
|
1736
|
+
group.isLive = group.isLive || isHistoryRowLive(row);
|
|
1737
|
+
group.sortAt = chooseNewestDate(group.sortAt, row.updatedAt ?? row.createdAt);
|
|
1738
|
+
}
|
|
1739
|
+
return [...grouped.values()]
|
|
1740
|
+
.map((group) => ({
|
|
1741
|
+
...group,
|
|
1742
|
+
candidates: group.candidates.sort(compareHistoryRows),
|
|
1743
|
+
}))
|
|
1744
|
+
.sort(compareHistoryGroups);
|
|
1745
|
+
}
|
|
1746
|
+
function buildLegacyHistoryGroups(line, productionHistory, t) {
|
|
1747
|
+
const productionRows = productionHistory.map((item) => buildLegacyProductionHistoryRow(item, t));
|
|
1748
|
+
const canaryRows = (line.canaryHistory.length > 0 ? line.canaryHistory : line.canary ? [line.canary] : []).map((item) => buildLegacyCanaryHistoryRow(item, t));
|
|
1749
|
+
if (productionRows.length === 0 && canaryRows.length === 0)
|
|
1750
|
+
return [];
|
|
1751
|
+
const groups = productionRows.map((row) => ({
|
|
1752
|
+
id: row.id,
|
|
1753
|
+
production: row,
|
|
1754
|
+
candidates: [],
|
|
1755
|
+
isLive: isHistoryRowLive(row),
|
|
1756
|
+
sortAt: row.updatedAt ?? row.createdAt,
|
|
1757
|
+
productionNumber: row.productionNumber,
|
|
1758
|
+
}));
|
|
1759
|
+
const fallbackGroup = groups[0] ??
|
|
1760
|
+
{
|
|
1761
|
+
id: canaryRows[0]?.id ?? 'legacy-canary',
|
|
1762
|
+
production: null,
|
|
1763
|
+
candidates: [],
|
|
1764
|
+
isLive: false,
|
|
1765
|
+
sortAt: canaryRows[0]?.updatedAt ?? canaryRows[0]?.createdAt ?? null,
|
|
1766
|
+
productionNumber: null,
|
|
1767
|
+
};
|
|
1768
|
+
if (groups.length === 0)
|
|
1769
|
+
groups.push(fallbackGroup);
|
|
1770
|
+
for (const row of canaryRows) {
|
|
1771
|
+
fallbackGroup.candidates.push(row);
|
|
1772
|
+
fallbackGroup.isLive = fallbackGroup.isLive || isHistoryRowLive(row);
|
|
1773
|
+
fallbackGroup.sortAt = chooseNewestDate(fallbackGroup.sortAt, row.updatedAt ?? row.createdAt);
|
|
1774
|
+
}
|
|
1775
|
+
return groups
|
|
1776
|
+
.map((group) => ({ ...group, candidates: group.candidates.sort(compareHistoryRows) }))
|
|
1777
|
+
.sort(compareHistoryGroups);
|
|
1778
|
+
}
|
|
1779
|
+
function groupReleaseEventsByVersion(events) {
|
|
1780
|
+
const grouped = new Map();
|
|
1781
|
+
for (const event of events) {
|
|
1782
|
+
const key = event.releaseVersionId ?? event.id;
|
|
1783
|
+
const current = grouped.get(key) ?? [];
|
|
1784
|
+
current.push(event);
|
|
1785
|
+
grouped.set(key, current);
|
|
1786
|
+
}
|
|
1787
|
+
return grouped;
|
|
1788
|
+
}
|
|
1789
|
+
function buildHistoryRowFromVersion(line, version, events, t) {
|
|
1790
|
+
const latest = getLatestReleaseEvent(events);
|
|
1791
|
+
const label = latest?.releaseVersionLabel ?? version.label;
|
|
1792
|
+
return {
|
|
1793
|
+
id: latest?.id ?? version.id,
|
|
1794
|
+
sourceEventId: latest?.id ?? null,
|
|
1795
|
+
releaseVersionId: version.id,
|
|
1796
|
+
releaseVersionKind: version.kind,
|
|
1797
|
+
releaseVersionLabel: label,
|
|
1798
|
+
productionNumber: version.productionVersionNumber,
|
|
1799
|
+
targetProductionNumber: version.targetProductionVersionNumber,
|
|
1800
|
+
candidateNumber: version.candidateNumber,
|
|
1801
|
+
event: latest?.operation ?? (version.kind === 'production' ? 'create_production' : 'create_canary'),
|
|
1802
|
+
laneType: latest?.laneType ?? (version.kind === 'production' ? 'production' : 'canary'),
|
|
1803
|
+
promptName: latest?.promptName ?? version.promptName,
|
|
1804
|
+
promptVersionId: latest?.promptVersionId ?? version.promptVersionId,
|
|
1805
|
+
promptVersionLabel: latest?.promptVersionLabel ??
|
|
1806
|
+
version.promptVersionLabel ??
|
|
1807
|
+
(version.promptVersionNumber ? `v${version.promptVersionNumber}` : formatShortId(version.promptVersionId)),
|
|
1808
|
+
modelId: latest?.modelId ?? version.modelId,
|
|
1809
|
+
modelName: formatHistoryModel(latest?.modelName ?? version.modelName, latest?.modelId ?? version.modelId),
|
|
1810
|
+
modelProvider: latest?.modelProvider ?? version.modelProvider,
|
|
1811
|
+
inputConnectorName: latest?.inputConnectorName ?? line.inputConnectorName,
|
|
1812
|
+
inputConnectorType: latest?.inputConnectorType ?? line.inputConnectorType,
|
|
1813
|
+
outputConnectors: latest?.outputConnectors ?? line.outputConnectors,
|
|
1814
|
+
runConfig: normalizeRunConfig(latest?.runConfig),
|
|
1815
|
+
trafficRatio: latest?.trafficRatio ?? null,
|
|
1816
|
+
trafficMode: latest?.trafficMode ?? null,
|
|
1817
|
+
recordMode: latest?.recordMode ?? null,
|
|
1818
|
+
recordCategories: latest?.recordCategories ?? [],
|
|
1819
|
+
status: latest ? formatReleaseEventStatus(latest) : null,
|
|
1820
|
+
isLive: latest?.status === 'running',
|
|
1821
|
+
countSummary: latest ? formatReleaseEventCounts(latest, t) : null,
|
|
1822
|
+
relations: latest ? formatReleaseEventRelations(latest, t) : null,
|
|
1823
|
+
reason: latest?.submitReason.trim() || null,
|
|
1824
|
+
createdAt: latest?.createdAt ?? version.createdAt,
|
|
1825
|
+
updatedAt: latest?.updatedAt ?? version.updatedAt,
|
|
1826
|
+
configChanges: buildReleaseConfigChanges(events, t),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
function buildHistoryRowFromEvents(line, events, t) {
|
|
1830
|
+
const latest = getLatestReleaseEvent(events);
|
|
1831
|
+
if (!latest) {
|
|
1832
|
+
return {
|
|
1833
|
+
id: 'empty',
|
|
1834
|
+
sourceEventId: null,
|
|
1835
|
+
releaseVersionId: null,
|
|
1836
|
+
releaseVersionKind: null,
|
|
1837
|
+
releaseVersionLabel: '—',
|
|
1838
|
+
productionNumber: null,
|
|
1839
|
+
targetProductionNumber: null,
|
|
1840
|
+
candidateNumber: null,
|
|
1841
|
+
event: null,
|
|
1842
|
+
laneType: null,
|
|
1843
|
+
promptName: line.promptName,
|
|
1844
|
+
promptVersionId: null,
|
|
1845
|
+
promptVersionLabel: '—',
|
|
1846
|
+
modelId: null,
|
|
1847
|
+
modelName: '—',
|
|
1848
|
+
modelProvider: null,
|
|
1849
|
+
inputConnectorName: line.inputConnectorName,
|
|
1850
|
+
inputConnectorType: line.inputConnectorType,
|
|
1851
|
+
outputConnectors: line.outputConnectors,
|
|
1852
|
+
runConfig: {},
|
|
1853
|
+
trafficRatio: null,
|
|
1854
|
+
trafficMode: null,
|
|
1855
|
+
recordMode: null,
|
|
1856
|
+
recordCategories: [],
|
|
1857
|
+
status: null,
|
|
1858
|
+
isLive: false,
|
|
1859
|
+
countSummary: null,
|
|
1860
|
+
relations: null,
|
|
1861
|
+
reason: null,
|
|
1862
|
+
createdAt: null,
|
|
1863
|
+
updatedAt: null,
|
|
1864
|
+
configChanges: [],
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
id: latest.id,
|
|
1869
|
+
sourceEventId: latest.id,
|
|
1870
|
+
releaseVersionId: latest.releaseVersionId,
|
|
1871
|
+
releaseVersionKind: latest.releaseVersionKind,
|
|
1872
|
+
releaseVersionLabel: latest.releaseVersionLabel ?? formatShortId(latest.releaseVersionId),
|
|
1873
|
+
productionNumber: latest.releaseVersionProductionNumber,
|
|
1874
|
+
targetProductionNumber: latest.releaseVersionTargetProductionNumber,
|
|
1875
|
+
candidateNumber: latest.releaseVersionCandidateNumber,
|
|
1876
|
+
event: latest.operation,
|
|
1877
|
+
laneType: latest.laneType,
|
|
1878
|
+
promptName: latest.promptName,
|
|
1879
|
+
promptVersionId: latest.promptVersionId,
|
|
1880
|
+
promptVersionLabel: latest.promptVersionLabel ?? formatShortId(latest.promptVersionId),
|
|
1881
|
+
modelId: latest.modelId,
|
|
1882
|
+
modelName: formatHistoryModel(latest.modelName, latest.modelId),
|
|
1883
|
+
modelProvider: latest.modelProvider,
|
|
1884
|
+
inputConnectorName: latest.inputConnectorName ?? line.inputConnectorName,
|
|
1885
|
+
inputConnectorType: latest.inputConnectorType ?? line.inputConnectorType,
|
|
1886
|
+
outputConnectors: latest.outputConnectors.length > 0 ? latest.outputConnectors : line.outputConnectors,
|
|
1887
|
+
runConfig: normalizeRunConfig(latest.runConfig),
|
|
1888
|
+
trafficRatio: latest.trafficRatio,
|
|
1889
|
+
trafficMode: latest.trafficMode,
|
|
1890
|
+
recordMode: latest.recordMode,
|
|
1891
|
+
recordCategories: latest.recordCategories,
|
|
1892
|
+
status: formatReleaseEventStatus(latest),
|
|
1893
|
+
isLive: latest.status === 'running',
|
|
1894
|
+
countSummary: formatReleaseEventCounts(latest, t),
|
|
1895
|
+
relations: formatReleaseEventRelations(latest, t),
|
|
1896
|
+
reason: latest.submitReason.trim() || null,
|
|
1897
|
+
createdAt: latest.createdAt,
|
|
1898
|
+
updatedAt: latest.updatedAt,
|
|
1899
|
+
configChanges: buildReleaseConfigChanges(events, t),
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
function buildLegacyProductionHistoryRow(item, t) {
|
|
1903
|
+
const configChanges = item.eventType === 'config_change'
|
|
1904
|
+
? [
|
|
1905
|
+
{
|
|
1906
|
+
id: item.id,
|
|
1907
|
+
at: item.updatedAt,
|
|
1908
|
+
event: item.eventType,
|
|
1909
|
+
items: buildLegacyProductionConfigItems(item, t),
|
|
1910
|
+
},
|
|
1911
|
+
]
|
|
1912
|
+
: [];
|
|
1913
|
+
const relations = [
|
|
1914
|
+
item.sourceExperimentId
|
|
1915
|
+
? `${t('releases.detail.history.relation.sourceExperiment')} ${formatShortId(item.sourceExperimentId)}`
|
|
1916
|
+
: null,
|
|
1917
|
+
item.sourceCanaryId
|
|
1918
|
+
? `${t('releases.detail.history.relation.sourceEvent')} ${formatShortId(item.sourceCanaryId)}`
|
|
1919
|
+
: null,
|
|
1920
|
+
item.rollbackTargetEventId
|
|
1921
|
+
? `${t('releases.detail.history.relation.rollbackTarget')} ${formatShortId(item.rollbackTargetEventId)}`
|
|
1922
|
+
: null,
|
|
1923
|
+
].filter((value) => Boolean(value));
|
|
1924
|
+
return {
|
|
1925
|
+
id: item.id,
|
|
1926
|
+
sourceEventId: item.id,
|
|
1927
|
+
releaseVersionId: null,
|
|
1928
|
+
releaseVersionKind: 'production',
|
|
1929
|
+
releaseVersionLabel: item.promptVersionLabel ?? formatShortId(item.id),
|
|
1930
|
+
productionNumber: null,
|
|
1931
|
+
targetProductionNumber: null,
|
|
1932
|
+
candidateNumber: null,
|
|
1933
|
+
event: item.eventType,
|
|
1934
|
+
laneType: 'production',
|
|
1935
|
+
promptName: item.promptVersionLabel ?? '—',
|
|
1936
|
+
promptVersionId: item.promptVersionId,
|
|
1937
|
+
promptVersionLabel: item.promptVersionLabel ?? formatShortId(item.promptVersionId),
|
|
1938
|
+
modelId: item.modelId,
|
|
1939
|
+
modelName: item.modelName ?? formatShortId(item.modelId),
|
|
1940
|
+
modelProvider: null,
|
|
1941
|
+
inputConnectorName: item.inputConnectorName,
|
|
1942
|
+
inputConnectorType: null,
|
|
1943
|
+
outputConnectors: [],
|
|
1944
|
+
runConfig: normalizeRunConfig(item.runConfig),
|
|
1945
|
+
trafficRatio: null,
|
|
1946
|
+
trafficMode: null,
|
|
1947
|
+
recordMode: item.recordMode,
|
|
1948
|
+
recordCategories: item.recordCategories ?? [],
|
|
1949
|
+
status: formatLegacyProductionStatus(item),
|
|
1950
|
+
isLive: item.status === 'running',
|
|
1951
|
+
countSummary: null,
|
|
1952
|
+
relations: relations.length > 0 ? relations.join(' · ') : null,
|
|
1953
|
+
reason: item.submitReason.trim() || null,
|
|
1954
|
+
createdAt: item.createdAt,
|
|
1955
|
+
updatedAt: item.updatedAt,
|
|
1956
|
+
configChanges,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function buildLegacyCanaryHistoryRow(canary, t) {
|
|
1960
|
+
return {
|
|
1961
|
+
id: canary.id,
|
|
1962
|
+
sourceEventId: canary.id,
|
|
1963
|
+
releaseVersionId: canary.releaseVersionId,
|
|
1964
|
+
releaseVersionKind: 'candidate',
|
|
1965
|
+
releaseVersionLabel: canary.releaseVersionLabel ?? canary.promptVersionLabel ?? formatShortId(canary.id),
|
|
1966
|
+
productionNumber: null,
|
|
1967
|
+
targetProductionNumber: null,
|
|
1968
|
+
candidateNumber: null,
|
|
1969
|
+
event: canary.status === 'running' ? 'ratio_change' : 'create_canary',
|
|
1970
|
+
laneType: 'canary',
|
|
1971
|
+
promptName: canary.promptName ?? canary.name ?? '—',
|
|
1972
|
+
promptVersionId: canary.promptVersionId,
|
|
1973
|
+
promptVersionLabel: canary.promptVersionLabel ?? formatShortId(canary.promptVersionId),
|
|
1974
|
+
modelId: canary.modelId,
|
|
1975
|
+
modelName: formatHistoryModel(canary.modelName, canary.modelId, canary.modelProvider),
|
|
1976
|
+
modelProvider: canary.modelProvider,
|
|
1977
|
+
inputConnectorName: canary.inputConnectorName,
|
|
1978
|
+
inputConnectorType: canary.inputConnectorType,
|
|
1979
|
+
outputConnectors: canary.outputConnectors,
|
|
1980
|
+
runConfig: normalizeRunConfig(canary.runConfig),
|
|
1981
|
+
trafficRatio: canary.trafficRatio,
|
|
1982
|
+
trafficMode: canary.trafficMode,
|
|
1983
|
+
recordMode: canary.recordMode,
|
|
1984
|
+
recordCategories: canary.recordCategories ?? [],
|
|
1985
|
+
status: canary.status,
|
|
1986
|
+
isLive: canary.status === 'running',
|
|
1987
|
+
countSummary: formatCanaryCounts(canary, t),
|
|
1988
|
+
relations: null,
|
|
1989
|
+
reason: canary.description?.trim() || null,
|
|
1990
|
+
createdAt: canary.createdAt,
|
|
1991
|
+
updatedAt: canary.updatedAt,
|
|
1992
|
+
configChanges: [],
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function formatReleaseEventStatus(event) {
|
|
906
1996
|
const parts = [
|
|
907
1997
|
event.status,
|
|
908
|
-
event.
|
|
909
|
-
event.
|
|
910
|
-
|
|
1998
|
+
event.terminalReason ? `${event.terminalReason}` : null,
|
|
1999
|
+
event.controlState ? `${event.controlState}` : null,
|
|
2000
|
+
].filter((value) => Boolean(value));
|
|
2001
|
+
return parts.join(' · ') || '—';
|
|
2002
|
+
}
|
|
2003
|
+
function formatLegacyProductionStatus(item) {
|
|
2004
|
+
return [item.status, item.stopReason, item.controlState].filter(Boolean).join(' · ') || '—';
|
|
2005
|
+
}
|
|
2006
|
+
function formatHistoryModel(name, id, provider) {
|
|
2007
|
+
const model = name ?? formatShortId(id);
|
|
2008
|
+
return provider ? `${model} · ${provider}` : model;
|
|
2009
|
+
}
|
|
2010
|
+
function formatRecordMode(mode, t, categories = []) {
|
|
2011
|
+
if (mode === 'all')
|
|
2012
|
+
return t('releases.detail.topology.recordMode.all');
|
|
2013
|
+
if (mode === 'selected_categories' || mode === 'correct_only') {
|
|
2014
|
+
const label = t('releases.detail.topology.recordMode.selectedCategories');
|
|
2015
|
+
return categories.length > 0 ? `${label}: ${categories.join('、')}` : label;
|
|
2016
|
+
}
|
|
2017
|
+
return mode || '—';
|
|
2018
|
+
}
|
|
2019
|
+
function formatReleaseEventCounts(event, t) {
|
|
2020
|
+
return [
|
|
2021
|
+
`${t('releases.detail.metric.received')} ${formatCount(event.totalReceived)}`,
|
|
2022
|
+
`${t('releases.detail.metric.processed')} ${formatCount(event.totalProcessed)}`,
|
|
2023
|
+
`${t('releases.detail.metric.errors')} ${formatCount(event.totalErrors)}`,
|
|
2024
|
+
].join(' · ');
|
|
2025
|
+
}
|
|
2026
|
+
function formatCanaryCounts(canary, t) {
|
|
2027
|
+
return [
|
|
2028
|
+
`${t('releases.detail.metric.received')} ${formatCount(canary.totalReceived)}`,
|
|
2029
|
+
`${t('releases.detail.metric.processed')} ${formatCount(canary.totalProcessed)}`,
|
|
2030
|
+
`${t('releases.detail.metric.errors')} ${formatCount(canary.totalErrors)}`,
|
|
2031
|
+
].join(' · ');
|
|
2032
|
+
}
|
|
2033
|
+
function formatReleaseEventRelations(event, t) {
|
|
2034
|
+
const parts = [
|
|
2035
|
+
event.sourceEventId
|
|
2036
|
+
? `${t('releases.detail.history.relation.sourceEvent')} ${formatShortId(event.sourceEventId)}`
|
|
2037
|
+
: null,
|
|
2038
|
+
event.supersedesEventId
|
|
2039
|
+
? `${t('releases.detail.history.relation.supersedes')} ${formatShortId(event.supersedesEventId)}`
|
|
2040
|
+
: null,
|
|
2041
|
+
event.rollbackTargetEventId
|
|
2042
|
+
? `${t('releases.detail.history.relation.rollbackTarget')} ${formatShortId(event.rollbackTargetEventId)}`
|
|
2043
|
+
: null,
|
|
2044
|
+
event.sourceExperimentId
|
|
2045
|
+
? `${t('releases.detail.history.relation.sourceExperiment')} ${formatShortId(event.sourceExperimentId)}`
|
|
2046
|
+
: null,
|
|
911
2047
|
].filter((value) => Boolean(value));
|
|
912
|
-
return parts.join(' · ')
|
|
2048
|
+
return parts.length > 0 ? parts.join(' · ') : null;
|
|
2049
|
+
}
|
|
2050
|
+
function ensureHistoryGroup(map, id, seed) {
|
|
2051
|
+
const current = map.get(id);
|
|
2052
|
+
if (current)
|
|
2053
|
+
return current;
|
|
2054
|
+
const next = {
|
|
2055
|
+
id,
|
|
2056
|
+
production: null,
|
|
2057
|
+
candidates: [],
|
|
2058
|
+
isLive: false,
|
|
2059
|
+
sortAt: seed.updatedAt ?? seed.createdAt,
|
|
2060
|
+
productionNumber: seed.productionNumber ?? seed.targetProductionNumber,
|
|
2061
|
+
};
|
|
2062
|
+
map.set(id, next);
|
|
2063
|
+
return next;
|
|
2064
|
+
}
|
|
2065
|
+
function chooseNewestHistoryRow(current, next) {
|
|
2066
|
+
if (!current)
|
|
2067
|
+
return next;
|
|
2068
|
+
return timeValue(next.updatedAt ?? next.createdAt) >= timeValue(current.updatedAt ?? current.createdAt)
|
|
2069
|
+
? next
|
|
2070
|
+
: current;
|
|
2071
|
+
}
|
|
2072
|
+
function chooseNewestDate(current, next) {
|
|
2073
|
+
if (!next)
|
|
2074
|
+
return current;
|
|
2075
|
+
if (!current)
|
|
2076
|
+
return next;
|
|
2077
|
+
return timeValue(next) >= timeValue(current) ? next : current;
|
|
2078
|
+
}
|
|
2079
|
+
function compareHistoryRows(left, right) {
|
|
2080
|
+
const candidateDelta = (right.candidateNumber ?? 0) - (left.candidateNumber ?? 0);
|
|
2081
|
+
if (candidateDelta !== 0)
|
|
2082
|
+
return candidateDelta;
|
|
2083
|
+
return timeValue(right.updatedAt ?? right.createdAt) - timeValue(left.updatedAt ?? left.createdAt);
|
|
2084
|
+
}
|
|
2085
|
+
export function compareHistoryGroups(left, right) {
|
|
2086
|
+
// Total order, "newest production first":
|
|
2087
|
+
// 1. Groups WITH a productionNumber rank above those without (candidate-only / legacy),
|
|
2088
|
+
// so a numbered production group never sinks below a null group on timestamp alone.
|
|
2089
|
+
// 2. Within numbered groups: productionNumber descending.
|
|
2090
|
+
// 3. Within null groups (single class): sortAt descending.
|
|
2091
|
+
// 4. Ties: sortAt descending.
|
|
2092
|
+
const leftHasNumber = left.productionNumber !== null;
|
|
2093
|
+
const rightHasNumber = right.productionNumber !== null;
|
|
2094
|
+
if (leftHasNumber !== rightHasNumber)
|
|
2095
|
+
return leftHasNumber ? -1 : 1;
|
|
2096
|
+
if (leftHasNumber && rightHasNumber) {
|
|
2097
|
+
const numberDelta = right.productionNumber - left.productionNumber;
|
|
2098
|
+
if (numberDelta !== 0)
|
|
2099
|
+
return numberDelta;
|
|
2100
|
+
}
|
|
2101
|
+
return timeValue(right.sortAt) - timeValue(left.sortAt);
|
|
2102
|
+
}
|
|
2103
|
+
function getLatestReleaseEvent(events) {
|
|
2104
|
+
return [...events].sort((left, right) => timeValue(right.updatedAt) - timeValue(left.updatedAt))[0] ?? null;
|
|
2105
|
+
}
|
|
2106
|
+
function isProductionHistoryRow(row) {
|
|
2107
|
+
return row.releaseVersionKind === 'production' || row.laneType === 'production';
|
|
2108
|
+
}
|
|
2109
|
+
export function isHistoryRowLive(row) {
|
|
2110
|
+
return row.isLive;
|
|
2111
|
+
}
|
|
2112
|
+
function getHistoryGroupKey(row) {
|
|
2113
|
+
const productionNumber = row.releaseVersionKind === 'candidate'
|
|
2114
|
+
? row.targetProductionNumber
|
|
2115
|
+
: (row.productionNumber ?? row.targetProductionNumber);
|
|
2116
|
+
if (productionNumber !== null)
|
|
2117
|
+
return `production-${productionNumber}`;
|
|
2118
|
+
return row.releaseVersionId ?? row.id;
|
|
2119
|
+
}
|
|
2120
|
+
function normalizeRunConfig(value) {
|
|
2121
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2122
|
+
return {};
|
|
2123
|
+
return value;
|
|
2124
|
+
}
|
|
2125
|
+
function getRunConfigNumber(config, key) {
|
|
2126
|
+
const value = config[key];
|
|
2127
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
2128
|
+
}
|
|
2129
|
+
function buildReleaseConfigChanges(events, t) {
|
|
2130
|
+
const sorted = [...events].sort((left, right) => timeValue(left.updatedAt) - timeValue(right.updatedAt));
|
|
2131
|
+
const changes = [];
|
|
2132
|
+
sorted.forEach((event, index) => {
|
|
2133
|
+
if (!RELEASE_CONFIG_OPERATIONS.has(event.operation))
|
|
2134
|
+
return;
|
|
2135
|
+
const previous = [...sorted.slice(0, index)].reverse().find((item) => item.releaseVersionId === event.releaseVersionId) ?? null;
|
|
2136
|
+
changes.push({
|
|
2137
|
+
id: event.id,
|
|
2138
|
+
at: event.updatedAt ?? event.createdAt,
|
|
2139
|
+
event: event.operation,
|
|
2140
|
+
items: buildConfigChangeItems(previous, event, t),
|
|
2141
|
+
});
|
|
2142
|
+
});
|
|
2143
|
+
return changes;
|
|
2144
|
+
}
|
|
2145
|
+
function buildLegacyProductionConfigItems(item, t) {
|
|
2146
|
+
const config = normalizeRunConfig(item.runConfig);
|
|
2147
|
+
const changes = [];
|
|
2148
|
+
addConfigChange(changes, t('releases.detail.topology.field.rpmLimit'), '—', formatHistoryNumber(getRunConfigNumber(config, 'rpmLimit')), true);
|
|
2149
|
+
addConfigChange(changes, t('releases.detail.topology.field.tpmLimit'), '—', formatHistoryNumber(getRunConfigNumber(config, 'tpmLimit')), true);
|
|
2150
|
+
addConfigChange(changes, t('releases.detail.topology.field.concurrency'), '—', formatHistoryNumber(getRunConfigNumber(config, 'concurrency')), true);
|
|
2151
|
+
addConfigChange(changes, t('releases.detail.topology.field.temperature'), '—', formatHistoryTemperature(getRunConfigNumber(config, 'temperature')), true);
|
|
2152
|
+
if (changes.length > 0)
|
|
2153
|
+
return changes;
|
|
2154
|
+
return [
|
|
2155
|
+
{
|
|
2156
|
+
field: t('releases.detail.history.field.snapshot'),
|
|
2157
|
+
previous: '—',
|
|
2158
|
+
next: t('releases.detail.history.change.updated'),
|
|
2159
|
+
},
|
|
2160
|
+
];
|
|
2161
|
+
}
|
|
2162
|
+
function buildConfigChangeItems(previous, current, t) {
|
|
2163
|
+
const previousConfig = normalizeRunConfig(previous?.runConfig);
|
|
2164
|
+
const currentConfig = normalizeRunConfig(current.runConfig);
|
|
2165
|
+
const changes = [];
|
|
2166
|
+
const includeInitial = previous === null;
|
|
2167
|
+
addConfigChange(changes, t('releases.detail.history.traffic'), formatRatioValue(previous?.trafficRatio ?? null), formatRatioValue(current.trafficRatio), includeInitial);
|
|
2168
|
+
addConfigChange(changes, t('releases.detail.history.field.trafficMode'), previous?.trafficMode ?? '—', current.trafficMode ?? '—', includeInitial);
|
|
2169
|
+
addConfigChange(changes, t('releases.detail.topology.field.rpmLimit'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'rpmLimit')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'rpmLimit')), includeInitial);
|
|
2170
|
+
addConfigChange(changes, t('releases.detail.topology.field.tpmLimit'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'tpmLimit')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'tpmLimit')), includeInitial);
|
|
2171
|
+
addConfigChange(changes, t('releases.detail.topology.field.concurrency'), formatHistoryNumber(getRunConfigNumber(previousConfig, 'concurrency')), formatHistoryNumber(getRunConfigNumber(currentConfig, 'concurrency')), includeInitial);
|
|
2172
|
+
addConfigChange(changes, t('releases.detail.topology.field.temperature'), formatHistoryTemperature(getRunConfigNumber(previousConfig, 'temperature')), formatHistoryTemperature(getRunConfigNumber(currentConfig, 'temperature')), includeInitial);
|
|
2173
|
+
addConfigChange(changes, t('releases.detail.history.model'), formatHistoryModel(previous?.modelName, previous?.modelId, previous?.modelProvider), formatHistoryModel(current.modelName, current.modelId, current.modelProvider), includeInitial);
|
|
2174
|
+
addConfigChange(changes, t('releases.detail.field.upstream'), formatConnectorLabel(previous?.inputConnectorName ?? null, previous?.inputConnectorType ?? null), formatConnectorLabel(current.inputConnectorName, current.inputConnectorType), includeInitial);
|
|
2175
|
+
addConfigChange(changes, t('releases.detail.history.field.outputConnectors'), formatOutputConnectors(previous?.outputConnectors ?? []), formatOutputConnectors(current.outputConnectors), includeInitial);
|
|
2176
|
+
addConfigChange(changes, t('releases.detail.history.recordMode'), formatRecordMode(previous?.recordMode, t, previous?.recordCategories ?? []), formatRecordMode(current.recordMode, t, current.recordCategories), includeInitial);
|
|
2177
|
+
if (changes.length > 0)
|
|
2178
|
+
return changes;
|
|
2179
|
+
return [
|
|
2180
|
+
{
|
|
2181
|
+
field: t('releases.detail.history.field.snapshot'),
|
|
2182
|
+
previous: '—',
|
|
2183
|
+
next: t('releases.detail.history.change.updated'),
|
|
2184
|
+
},
|
|
2185
|
+
];
|
|
2186
|
+
}
|
|
2187
|
+
function addConfigChange(changes, field, previous, next, includeInitial = false) {
|
|
2188
|
+
if (next === '—')
|
|
2189
|
+
return;
|
|
2190
|
+
if (!includeInitial && previous === next)
|
|
2191
|
+
return;
|
|
2192
|
+
changes.push({ field, previous, next });
|
|
2193
|
+
}
|
|
2194
|
+
function formatRatioValue(value) {
|
|
2195
|
+
return typeof value === 'number' && Number.isFinite(value) ? `${Math.round(value * 100)}%` : '—';
|
|
2196
|
+
}
|
|
2197
|
+
function formatHistoryNumber(value) {
|
|
2198
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
2199
|
+
return '—';
|
|
2200
|
+
return Number.isInteger(value) ? formatCount(value) : formatHistoryTemperature(value);
|
|
2201
|
+
}
|
|
2202
|
+
function formatHistoryTemperature(value) {
|
|
2203
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
2204
|
+
return '—';
|
|
2205
|
+
return value.toFixed(2).replace(/\.?0+$/, '');
|
|
2206
|
+
}
|
|
2207
|
+
function formatConnectorLabel(name, type) {
|
|
2208
|
+
if (!name && !type)
|
|
2209
|
+
return '—';
|
|
2210
|
+
return [type, name].filter((value) => Boolean(value)).join(' · ');
|
|
2211
|
+
}
|
|
2212
|
+
function formatOutputConnectors(connectors) {
|
|
2213
|
+
if (connectors.length === 0)
|
|
2214
|
+
return '—';
|
|
2215
|
+
return connectors.map((connector) => formatConnectorLabel(connector.name, connector.type)).join(' / ');
|
|
2216
|
+
}
|
|
2217
|
+
function formatHistoryVersionLabel(label) {
|
|
2218
|
+
return label.replace(/^v(?=\d)/, 'V');
|
|
2219
|
+
}
|
|
2220
|
+
function buildHistoryRuntimeItems(row, t) {
|
|
2221
|
+
const items = [];
|
|
2222
|
+
const rpm = getRunConfigNumber(row.runConfig, 'rpmLimit');
|
|
2223
|
+
const tpm = getRunConfigNumber(row.runConfig, 'tpmLimit');
|
|
2224
|
+
const concurrency = getRunConfigNumber(row.runConfig, 'concurrency');
|
|
2225
|
+
const temperature = getRunConfigNumber(row.runConfig, 'temperature');
|
|
2226
|
+
if (row.trafficRatio !== null || row.trafficMode) {
|
|
2227
|
+
items.push({ label: t('releases.detail.history.traffic'), value: formatReleaseRowTraffic(row), mono: true });
|
|
2228
|
+
}
|
|
2229
|
+
if (rpm !== null)
|
|
2230
|
+
items.push({ label: t('releases.detail.topology.field.rpmLimit'), value: formatHistoryNumber(rpm), mono: true });
|
|
2231
|
+
if (tpm !== null)
|
|
2232
|
+
items.push({ label: t('releases.detail.topology.field.tpmLimit'), value: formatHistoryNumber(tpm), mono: true });
|
|
2233
|
+
if (concurrency !== null) {
|
|
2234
|
+
items.push({
|
|
2235
|
+
label: t('releases.detail.topology.field.concurrency'),
|
|
2236
|
+
value: formatHistoryNumber(concurrency),
|
|
2237
|
+
mono: true,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
if (temperature !== null) {
|
|
2241
|
+
items.push({
|
|
2242
|
+
label: t('releases.detail.topology.field.temperature'),
|
|
2243
|
+
value: formatHistoryTemperature(temperature),
|
|
2244
|
+
mono: true,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
if (row.recordMode) {
|
|
2248
|
+
items.push({
|
|
2249
|
+
label: t('releases.detail.history.recordMode'),
|
|
2250
|
+
value: formatRecordMode(row.recordMode, t, row.recordCategories),
|
|
2251
|
+
mono: true,
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
return items;
|
|
913
2255
|
}
|
|
914
|
-
function
|
|
915
|
-
|
|
2256
|
+
function buildHistoryConnectorItems(row, t) {
|
|
2257
|
+
return [
|
|
2258
|
+
{
|
|
2259
|
+
label: t('releases.detail.field.upstream'),
|
|
2260
|
+
value: formatConnectorLabel(row.inputConnectorName, row.inputConnectorType),
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
label: t('releases.detail.field.downstream'),
|
|
2264
|
+
value: row.outputConnectors.length > 0
|
|
2265
|
+
? formatOutputConnectors(row.outputConnectors)
|
|
2266
|
+
: t('releases.detail.history.field.noDownstream'),
|
|
2267
|
+
},
|
|
2268
|
+
];
|
|
2269
|
+
}
|
|
2270
|
+
function buildHistoryReasonItems(row, t) {
|
|
2271
|
+
if (!row.reason)
|
|
2272
|
+
return [];
|
|
2273
|
+
return [{ label: t('releases.detail.history.reason'), value: row.reason }];
|
|
2274
|
+
}
|
|
2275
|
+
function formatReleaseRowTraffic(row) {
|
|
2276
|
+
return ([formatRatioValue(row.trafficRatio), row.trafficMode].filter((value) => value && value !== '—').join(' · ') || '—');
|
|
2277
|
+
}
|
|
2278
|
+
function buildReleaseResultsHref(line, row) {
|
|
2279
|
+
if (!row.releaseVersionId)
|
|
2280
|
+
return null;
|
|
2281
|
+
return `/releases/${encodeURIComponent(line.id)}?tab=results&version=${encodeURIComponent(row.releaseVersionId)}`;
|
|
2282
|
+
}
|
|
2283
|
+
function buildAnnotationHref(line, row) {
|
|
2284
|
+
if (!row.releaseVersionId)
|
|
916
2285
|
return null;
|
|
917
|
-
|
|
918
|
-
const promptVersion = event.promptVersionLabel ?? formatShortId(event.promptVersionId);
|
|
919
|
-
const model = event.modelName ?? formatShortId(event.modelId);
|
|
920
|
-
return `${label} · ${promptVersion} · ${model}`;
|
|
2286
|
+
return `/annotations/new?line=${encodeURIComponent(line.id)}&version=${encodeURIComponent(row.releaseVersionId)}`;
|
|
921
2287
|
}
|
|
922
|
-
function
|
|
2288
|
+
function buildQualityAnnotationHref(line, releaseEvents) {
|
|
2289
|
+
const latestEvent = [...releaseEvents]
|
|
2290
|
+
.filter((event) => Boolean(event.releaseVersionId))
|
|
2291
|
+
.sort((left, right) => timeValue(right.updatedAt ?? right.createdAt) - timeValue(left.updatedAt ?? left.createdAt))[0];
|
|
2292
|
+
const params = new URLSearchParams({ line: line.id });
|
|
2293
|
+
if (latestEvent?.releaseVersionId)
|
|
2294
|
+
params.set('version', latestEvent.releaseVersionId);
|
|
2295
|
+
return `/annotations/new?${params.toString()}`;
|
|
2296
|
+
}
|
|
2297
|
+
function HistoryPane({ projectId, line, productionHistory, releaseEvents, loading, }) {
|
|
923
2298
|
const { t } = useI18n();
|
|
924
2299
|
const formatDateTimeOrDash = useDateTimeOrDash();
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
meta: item.submitReason || item.status,
|
|
942
|
-
variant: null,
|
|
943
|
-
}));
|
|
944
|
-
const canary = line.canary
|
|
945
|
-
? [
|
|
946
|
-
{
|
|
947
|
-
id: line.canary.id,
|
|
948
|
-
event: line.canary.status === 'running' ? 'ratio_change' : 'create_canary',
|
|
949
|
-
title: `${line.canary.promptVersionLabel ?? line.canary.id.slice(0, 8)} · ${Math.round(line.canary.trafficRatio * 100)}%`,
|
|
950
|
-
createdAt: line.canary.updatedAt,
|
|
951
|
-
meta: line.canary.description ?? line.canary.status,
|
|
952
|
-
variant: line.canary.releaseVariantLabel
|
|
953
|
-
? `${line.canary.releaseVariantLabel} · ${line.canary.promptVersionLabel ?? '-'} · ${line.canary.modelName ?? '-'}`
|
|
954
|
-
: null,
|
|
955
|
-
},
|
|
956
|
-
]
|
|
957
|
-
: [];
|
|
958
|
-
return [...canary, ...prod].sort((left, right) => (right.createdAt ? Date.parse(right.createdAt) : 0) - (left.createdAt ? Date.parse(left.createdAt) : 0));
|
|
959
|
-
}, [line.canary, productionHistory, releaseEvents, t]);
|
|
2300
|
+
const restoreToProductionMutation = useRestoreReleaseLineHistoryToProduction(projectId);
|
|
2301
|
+
const restoreToCanaryMutation = useRestoreReleaseLineHistoryToCanary(projectId);
|
|
2302
|
+
const restorePending = restoreToProductionMutation.isPending || restoreToCanaryMutation.isPending;
|
|
2303
|
+
const [restoreFeedback, setRestoreFeedback] = useState(null);
|
|
2304
|
+
const groups = useMemo(() => buildHistoryGroups(line, releaseEvents, productionHistory, t), [line, productionHistory, releaseEvents, t]);
|
|
2305
|
+
const [groupOpen, setGroupOpen] = useState({});
|
|
2306
|
+
const [moreOpen, setMoreOpen] = useState({});
|
|
2307
|
+
const [configOpen, setConfigOpen] = useState({});
|
|
2308
|
+
const historyGroupResetKey = `${line.id}:${groups.length}`;
|
|
2309
|
+
const [visibleGroupState, setVisibleGroupState] = useState(() => ({
|
|
2310
|
+
key: historyGroupResetKey,
|
|
2311
|
+
count: HISTORY_INITIAL_GROUP_LIMIT,
|
|
2312
|
+
}));
|
|
2313
|
+
const [loadingMoreKey, setLoadingMoreKey] = useState(null);
|
|
2314
|
+
const loadingMoreKeyRef = useRef(null);
|
|
2315
|
+
const loadMoreTimerRef = useRef(null);
|
|
960
2316
|
const showLoader = useDelayedLoading(loading);
|
|
2317
|
+
const visibleGroupCount = visibleGroupState.key === historyGroupResetKey ? visibleGroupState.count : HISTORY_INITIAL_GROUP_LIMIT;
|
|
2318
|
+
const visibleGroups = useMemo(() => groups.slice(0, visibleGroupCount), [groups, visibleGroupCount]);
|
|
2319
|
+
const hasMoreGroups = visibleGroupCount < groups.length;
|
|
2320
|
+
const isLoadingMoreGroups = loadingMoreKey === historyGroupResetKey && hasMoreGroups;
|
|
2321
|
+
const restoreHistoryToProduction = useCallback((row, versionLabel) => {
|
|
2322
|
+
if (!row.sourceEventId || line.status === 'archived')
|
|
2323
|
+
return;
|
|
2324
|
+
setRestoreFeedback(null);
|
|
2325
|
+
restoreToProductionMutation.mutate({
|
|
2326
|
+
releaseLineId: line.id,
|
|
2327
|
+
body: {
|
|
2328
|
+
sourceEventId: row.sourceEventId,
|
|
2329
|
+
reason: formatTemplate(t('releases.detail.history.action.restoreToProductionReason'), {
|
|
2330
|
+
version: versionLabel,
|
|
2331
|
+
}),
|
|
2332
|
+
},
|
|
2333
|
+
}, {
|
|
2334
|
+
onSuccess: () => setRestoreFeedback({
|
|
2335
|
+
tone: 'success',
|
|
2336
|
+
message: t('releases.detail.history.action.restoreSuccess'),
|
|
2337
|
+
}),
|
|
2338
|
+
onError: (error) => setRestoreFeedback({
|
|
2339
|
+
tone: 'error',
|
|
2340
|
+
message: getApiErrorMessage(error) ?? t('releases.detail.history.action.restoreFailed'),
|
|
2341
|
+
}),
|
|
2342
|
+
});
|
|
2343
|
+
}, [line.id, line.status, restoreToProductionMutation, t]);
|
|
2344
|
+
const restoreHistoryToCanary = useCallback((row, versionLabel) => {
|
|
2345
|
+
if (!row.sourceEventId || line.status === 'archived')
|
|
2346
|
+
return;
|
|
2347
|
+
setRestoreFeedback(null);
|
|
2348
|
+
restoreToCanaryMutation.mutate({
|
|
2349
|
+
releaseLineId: line.id,
|
|
2350
|
+
body: {
|
|
2351
|
+
sourceEventId: row.sourceEventId,
|
|
2352
|
+
reason: formatTemplate(t('releases.detail.history.action.restoreToCanaryReason'), {
|
|
2353
|
+
version: versionLabel,
|
|
2354
|
+
}),
|
|
2355
|
+
},
|
|
2356
|
+
}, {
|
|
2357
|
+
onSuccess: () => setRestoreFeedback({
|
|
2358
|
+
tone: 'success',
|
|
2359
|
+
message: t('releases.detail.history.action.restoreSuccess'),
|
|
2360
|
+
}),
|
|
2361
|
+
onError: (error) => setRestoreFeedback({
|
|
2362
|
+
tone: 'error',
|
|
2363
|
+
message: getApiErrorMessage(error) ?? t('releases.detail.history.action.restoreFailed'),
|
|
2364
|
+
}),
|
|
2365
|
+
});
|
|
2366
|
+
}, [line.id, line.status, restoreToCanaryMutation, t]);
|
|
2367
|
+
const loadMoreHistoryGroups = useCallback(() => {
|
|
2368
|
+
if (!hasMoreGroups)
|
|
2369
|
+
return;
|
|
2370
|
+
if (loadingMoreKeyRef.current === historyGroupResetKey)
|
|
2371
|
+
return;
|
|
2372
|
+
loadingMoreKeyRef.current = historyGroupResetKey;
|
|
2373
|
+
setLoadingMoreKey(historyGroupResetKey);
|
|
2374
|
+
setVisibleGroupState((current) => {
|
|
2375
|
+
const currentCount = current.key === historyGroupResetKey ? current.count : HISTORY_INITIAL_GROUP_LIMIT;
|
|
2376
|
+
return {
|
|
2377
|
+
key: historyGroupResetKey,
|
|
2378
|
+
count: Math.min(groups.length, currentCount + HISTORY_GROUP_PAGE_SIZE),
|
|
2379
|
+
};
|
|
2380
|
+
});
|
|
2381
|
+
if (loadMoreTimerRef.current)
|
|
2382
|
+
window.clearTimeout(loadMoreTimerRef.current);
|
|
2383
|
+
loadMoreTimerRef.current = window.setTimeout(() => {
|
|
2384
|
+
loadingMoreKeyRef.current = null;
|
|
2385
|
+
setLoadingMoreKey((current) => (current === historyGroupResetKey ? null : current));
|
|
2386
|
+
loadMoreTimerRef.current = null;
|
|
2387
|
+
}, 360);
|
|
2388
|
+
}, [groups.length, hasMoreGroups, historyGroupResetKey]);
|
|
2389
|
+
useEffect(() => {
|
|
2390
|
+
return () => {
|
|
2391
|
+
if (loadMoreTimerRef.current) {
|
|
2392
|
+
window.clearTimeout(loadMoreTimerRef.current);
|
|
2393
|
+
loadMoreTimerRef.current = null;
|
|
2394
|
+
}
|
|
2395
|
+
loadingMoreKeyRef.current = null;
|
|
2396
|
+
};
|
|
2397
|
+
}, []);
|
|
961
2398
|
if (loading) {
|
|
962
2399
|
return showLoader ? _jsx(PlatformLoader, { className: "py-8", size: "sm" }) : null;
|
|
963
2400
|
}
|
|
964
|
-
if (
|
|
2401
|
+
if (groups.length === 0) {
|
|
965
2402
|
return (_jsx("div", { className: "rounded-lg border bg-card p-10 text-center text-sm text-muted-foreground", children: t('releases.detail.history.empty') }));
|
|
966
2403
|
}
|
|
967
|
-
return (_jsxs("
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2404
|
+
return (_jsxs("section", { className: "w-full", "aria-label": t('releases.detail.history.title'), children: [restoreFeedback ? (_jsxs("div", { role: restoreFeedback.tone === 'error' ? 'alert' : 'status', className: cn('mb-3 flex items-center gap-2 rounded-md border px-3 py-2 text-[12.5px]', restoreFeedback.tone === 'error'
|
|
2405
|
+
? 'border-destructive/35 bg-destructive/5 text-destructive'
|
|
2406
|
+
: 'border-border bg-muted/55 text-muted-foreground'), children: [restoreFeedback.tone === 'error' ? _jsx(AlertTriangle, { className: "size-3.5" }) : _jsx(Check, { className: "size-3.5" }), _jsx("span", { children: restoreFeedback.message })] })) : null, _jsx("div", { className: "relative pl-[30px]", children: visibleGroups.map((group, index) => {
|
|
2407
|
+
const headline = group.production ?? group.candidates[0] ?? null;
|
|
2408
|
+
if (!headline)
|
|
2409
|
+
return null;
|
|
2410
|
+
const children = group.production ? group.candidates : group.candidates.slice(1);
|
|
2411
|
+
const defaultOpen = group.isLive || index === 0;
|
|
2412
|
+
const isOpen = groupOpen[group.id] ?? defaultOpen;
|
|
2413
|
+
return (_jsxs("div", { className: "relative mb-4", children: [_jsx("div", { className: "absolute bottom-3.5 left-[-22px] top-[18px] w-[1.5px] bg-border", "aria-hidden": true }), _jsx(HistoryVersionCard, { line: line, row: headline, variant: isProductionHistoryRow(headline) ? 'production' : 'canary', live: group.isLive && isProductionHistoryRow(headline), hasChildren: children.length > 0, childrenOpen: isOpen, moreOpen: Boolean(moreOpen[headline.id]), configOpen: Boolean(configOpen[headline.id]), onToggleChildren: () => setGroupOpen((current) => ({
|
|
2414
|
+
...current,
|
|
2415
|
+
[group.id]: !(current[group.id] ?? defaultOpen),
|
|
2416
|
+
})), onToggleMore: () => setMoreOpen((current) => ({
|
|
2417
|
+
...current,
|
|
2418
|
+
[headline.id]: !current[headline.id],
|
|
2419
|
+
})), onToggleConfig: () => setConfigOpen((current) => ({
|
|
2420
|
+
...current,
|
|
2421
|
+
[headline.id]: !current[headline.id],
|
|
2422
|
+
})), onRestoreToProduction: restoreHistoryToProduction, onRestoreToCanary: restoreHistoryToCanary, restorePending: restorePending, formatDateTimeOrDash: formatDateTimeOrDash }), isOpen && children.length > 0 ? (_jsx("div", { className: "mb-0.5 mt-2.5 flex flex-col gap-2.5", children: children.map((row) => (_jsx(HistoryVersionCard, { line: line, row: row, variant: "canary", compact: true, live: isHistoryRowLive(row), hasChildren: false, childrenOpen: false, moreOpen: Boolean(moreOpen[row.id]), configOpen: Boolean(configOpen[row.id]), onToggleChildren: () => undefined, onToggleMore: () => setMoreOpen((current) => ({
|
|
2423
|
+
...current,
|
|
2424
|
+
[row.id]: !current[row.id],
|
|
2425
|
+
})), onToggleConfig: () => setConfigOpen((current) => ({
|
|
2426
|
+
...current,
|
|
2427
|
+
[row.id]: !current[row.id],
|
|
2428
|
+
})), onRestoreToProduction: restoreHistoryToProduction, onRestoreToCanary: restoreHistoryToCanary, restorePending: restorePending, formatDateTimeOrDash: formatDateTimeOrDash }, row.id))) })) : null] }, group.id));
|
|
2429
|
+
}) }), hasMoreGroups ? (_jsx(HistoryLoadMoreIndicator, { loading: isLoadingMoreGroups, label: isLoadingMoreGroups ? t('releases.detail.history.loadingMore') : t('releases.detail.history.moreAvailable'), onClick: loadMoreHistoryGroups })) : null] }));
|
|
2430
|
+
}
|
|
2431
|
+
function HistoryLoadMoreIndicator({ loading, label, onClick, }) {
|
|
2432
|
+
if (!loading) {
|
|
2433
|
+
return (_jsx("div", { className: "flex justify-center py-3", children: _jsxs("button", { type: "button", onClick: onClick, className: "inline-flex h-8 items-center gap-1.5 rounded-full border bg-card px-3.5 text-[12px] font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", children: [_jsx(ChevronDown, { className: "size-3.5", "aria-hidden": true }), _jsx("span", { children: label })] }) }));
|
|
2434
|
+
}
|
|
2435
|
+
return (_jsxs("div", { className: "flex justify-center py-4", role: "status", "aria-live": "polite", "aria-label": label, children: [_jsx("span", { className: "sr-only", children: label }), _jsxs("span", { className: "relative flex h-8 w-28 items-center justify-center", "aria-hidden": true, children: [_jsx("span", { className: "absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-gradient-to-r from-transparent via-border to-transparent" }), _jsx("span", { className: "relative inline-flex items-center gap-1.5 rounded-full border bg-card px-3 py-2 shadow-sm", children: [0, 1, 2].map((index) => (_jsx("span", { className: "size-1.5 rounded-full bg-muted-foreground/80 motion-safe:animate-bounce", style: { animationDelay: `${index * 120}ms` } }, index))) })] })] }));
|
|
2436
|
+
}
|
|
2437
|
+
function HistoryVersionCard({ line, row, variant, live, compact = false, hasChildren, childrenOpen, moreOpen, configOpen, onToggleChildren, onToggleMore, onToggleConfig, onRestoreToProduction, onRestoreToCanary, restorePending, formatDateTimeOrDash, }) {
|
|
2438
|
+
const { t } = useI18n();
|
|
2439
|
+
const isProduction = variant === 'production';
|
|
2440
|
+
const runtimeItems = buildHistoryRuntimeItems(row, t);
|
|
2441
|
+
const connectorItems = buildHistoryConnectorItems(row, t);
|
|
2442
|
+
const reasonItems = buildHistoryReasonItems(row, t);
|
|
2443
|
+
const hasConfig = row.configChanges.length > 0;
|
|
2444
|
+
const dateLabel = formatDateTimeOrDash(row.updatedAt ?? row.createdAt);
|
|
2445
|
+
const resultsHref = buildReleaseResultsHref(line, row);
|
|
2446
|
+
const annotationHref = buildAnnotationHref(line, row);
|
|
2447
|
+
const versionLabel = formatHistoryVersionLabel(row.releaseVersionLabel || '—');
|
|
2448
|
+
const expandLabel = formatTemplate(t(childrenOpen ? 'releases.detail.history.action.collapseVersion' : 'releases.detail.history.action.expandVersion'), { version: versionLabel });
|
|
2449
|
+
const moreLabel = formatTemplate(t('releases.detail.history.action.moreInfo'), { version: versionLabel });
|
|
2450
|
+
const configLabel = formatTemplate(t('releases.detail.history.action.configChanges'), { version: versionLabel });
|
|
2451
|
+
const resultLabel = formatTemplate(t('releases.detail.history.action.viewResults'), { version: versionLabel });
|
|
2452
|
+
const annotationLabel = formatTemplate(t('releases.detail.history.action.createAnnotation'), {
|
|
2453
|
+
version: versionLabel,
|
|
2454
|
+
});
|
|
2455
|
+
const actionMenuLabel = formatTemplate(t('releases.detail.history.action.moreActions'), { version: versionLabel });
|
|
2456
|
+
const restoreDisabled = !row.sourceEventId || line.status === 'archived' || restorePending;
|
|
2457
|
+
const restoreHistoryRow = () => {
|
|
2458
|
+
if (isProduction)
|
|
2459
|
+
onRestoreToProduction(row, versionLabel);
|
|
2460
|
+
else
|
|
2461
|
+
onRestoreToCanary(row, versionLabel);
|
|
2462
|
+
};
|
|
2463
|
+
const rowClickProps = hasChildren
|
|
2464
|
+
? {
|
|
2465
|
+
role: 'button',
|
|
2466
|
+
tabIndex: 0,
|
|
2467
|
+
'aria-expanded': childrenOpen,
|
|
2468
|
+
'aria-label': expandLabel,
|
|
2469
|
+
onClick: onToggleChildren,
|
|
2470
|
+
onKeyDown: (event) => {
|
|
2471
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
2472
|
+
return;
|
|
2473
|
+
event.preventDefault();
|
|
2474
|
+
onToggleChildren();
|
|
2475
|
+
},
|
|
2476
|
+
}
|
|
2477
|
+
: {};
|
|
2478
|
+
return (_jsxs("div", { ...rowClickProps, className: cn('relative flex flex-wrap items-center gap-x-3 gap-y-2 rounded-[10px] border bg-card px-[15px] shadow-sm transition-shadow hover:shadow-md', hasChildren && 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', compact ? 'py-[11px]' : 'py-[13px]'), style: live && isProduction
|
|
2479
|
+
? {
|
|
2480
|
+
borderColor: 'color-mix(in srgb, var(--src-prod) 35%, var(--border))',
|
|
2481
|
+
boxShadow: '0 1px 3px color-mix(in srgb, var(--src-prod) 12%, transparent)',
|
|
2482
|
+
}
|
|
2483
|
+
: undefined, children: [_jsx("span", { className: cn('absolute z-10 rounded-full bg-card', isProduction
|
|
2484
|
+
? 'left-[-29px] top-[14px] size-4 border-[2.5px]'
|
|
2485
|
+
: 'left-[-26px] top-[15px] size-2.5 border-[2.5px]'), style: {
|
|
2486
|
+
background: live ? (isProduction ? 'var(--src-prod)' : 'var(--src-canary)') : 'var(--card)',
|
|
2487
|
+
borderColor: isProduction ? (live ? 'var(--src-prod)' : 'var(--muted-foreground)') : 'var(--src-canary)',
|
|
2488
|
+
boxShadow: live
|
|
2489
|
+
? `0 0 0 4px color-mix(in srgb, ${isProduction ? 'var(--src-prod)' : 'var(--src-canary)'} 16%, transparent)`
|
|
2490
|
+
: undefined,
|
|
2491
|
+
}, "aria-hidden": true }), _jsx("span", { className: "absolute left-[-14px] top-[19px] h-[1.5px] w-[11px] bg-border", style: !isProduction ? { background: 'color-mix(in srgb, var(--src-canary) 35%, var(--border))' } : undefined, "aria-hidden": true }), _jsx(HistoryVersionBadge, { label: versionLabel, variant: variant, compact: compact }), _jsxs("div", { className: "flex min-w-0 flex-1 items-center", children: [_jsxs("div", { className: "flex w-[200px] min-w-0 shrink-0 items-baseline gap-[7px] whitespace-nowrap", children: [_jsx("span", { className: "shrink-0 text-[11.5px] text-muted-foreground", children: t('releases.detail.history.model') }), _jsx("span", { className: "min-w-0 truncate text-[13.5px] font-medium text-foreground", children: row.modelName })] }), _jsxs("div", { className: "flex w-[212px] min-w-0 shrink-0 items-baseline gap-[7px] whitespace-nowrap", children: [_jsx("span", { className: "shrink-0 text-[11.5px] text-muted-foreground", children: t('releases.detail.history.field.prompt') }), _jsx("span", { className: "min-w-0 truncate text-[13.5px] font-medium text-foreground", children: row.promptName }), _jsx("span", { className: "shrink-0 text-muted-foreground", children: "\u00B7" }), _jsx("span", { className: "shrink-0 font-mono text-[12.5px] font-semibold text-muted-foreground", children: row.promptVersionLabel })] }), _jsx("div", { className: "shrink-0 whitespace-nowrap font-mono text-[11.5px] text-muted-foreground", children: dateLabel })] }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: onToggleMore, "aria-pressed": moreOpen, "aria-label": moreLabel, title: moreLabel, className: cn('inline-flex size-[30px] items-center justify-center rounded-lg border bg-card p-0 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', moreOpen && 'border-ring bg-muted text-foreground', compact && 'size-7 rounded-md'), children: _jsx(ChevronDown, { className: cn(compact ? 'size-3.5' : 'size-4', 'transition-transform', moreOpen && 'rotate-180') }) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": actionMenuLabel, title: actionMenuLabel, className: cn('inline-flex size-[30px] items-center justify-center rounded-lg border bg-card p-0 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', compact && 'size-7 rounded-md'), children: _jsx(MoreHorizontal, { className: compact ? 'size-3.5' : 'size-4' }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", onClick: (event) => event.stopPropagation(), children: [_jsxs(DropdownMenuItem, { disabled: !hasConfig, onSelect: onToggleConfig, className: "gap-2", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), configLabel] }), _jsx(HistoryDropdownLink, { href: resultsHref, label: resultLabel, children: _jsx(ScrollText, { className: "size-3.5" }) }), _jsx(HistoryDropdownLink, { href: annotationHref, label: annotationLabel, children: _jsx(Tag, { className: "size-3.5" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { disabled: restoreDisabled, onSelect: (event) => {
|
|
2492
|
+
event.preventDefault();
|
|
2493
|
+
event.stopPropagation();
|
|
2494
|
+
restoreHistoryRow();
|
|
2495
|
+
}, className: "gap-2", children: [_jsx(RotateCcw, { className: "size-3.5" }), restorePending
|
|
2496
|
+
? t('releases.detail.history.action.restoring')
|
|
2497
|
+
: t('releases.detail.history.action.restore')] })] })] })] }), moreOpen ? (_jsx(HistoryMorePanel, { runtimeItems: runtimeItems, connectorItems: connectorItems, reasonItems: reasonItems })) : null, configOpen && hasConfig ? (_jsx(HistoryConfigPanel, { changes: row.configChanges, formatDateTimeOrDash: formatDateTimeOrDash })) : null] }));
|
|
2498
|
+
}
|
|
2499
|
+
function HistoryDropdownLink({ href, label, children }) {
|
|
2500
|
+
if (!href) {
|
|
2501
|
+
return (_jsxs(DropdownMenuItem, { disabled: true, className: "gap-2", children: [children, label] }));
|
|
2502
|
+
}
|
|
2503
|
+
return (_jsx(DropdownMenuItem, { asChild: true, className: "gap-2", children: _jsxs(Link, { href: href, children: [children, label] }) }));
|
|
2504
|
+
}
|
|
2505
|
+
function HistoryMorePanel({ runtimeItems, connectorItems, reasonItems, }) {
|
|
2506
|
+
const { t } = useI18n();
|
|
2507
|
+
return (_jsxs("div", { className: "mt-3 basis-full rounded-lg border bg-muted/55 px-[13px] py-[11px]", children: [_jsx(HistoryPanelRow, { title: t('releases.detail.history.runtimeSection'), items: runtimeItems }), _jsx(HistoryPanelRow, { title: t('releases.detail.history.connectorSection'), items: connectorItems }), reasonItems.length > 0 ? (_jsx(HistoryPanelRow, { title: t('releases.detail.history.reasonSection'), items: reasonItems })) : null] }));
|
|
2508
|
+
}
|
|
2509
|
+
function HistoryPanelRow({ title, items }) {
|
|
2510
|
+
if (items.length === 0)
|
|
2511
|
+
return null;
|
|
2512
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 py-[7px] first:pt-0 last:pb-0 sm:flex-row sm:items-baseline [&+&]:border-t", children: [_jsx("div", { className: "w-[58px] shrink-0 text-[11.5px] font-semibold text-muted-foreground", children: title }), _jsx("dl", { className: "flex min-w-0 flex-1 flex-wrap gap-x-[30px] gap-y-[9px]", children: items.map((item) => (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-[11px] text-muted-foreground", children: item.label }), _jsx("dd", { className: cn('mt-0.5 max-w-[360px] break-words text-[13px] font-semibold text-foreground', item.mono && 'font-mono'), children: item.value })] }, item.label))) })] }));
|
|
2513
|
+
}
|
|
2514
|
+
function HistoryConfigPanel({ changes, formatDateTimeOrDash, }) {
|
|
2515
|
+
const { t } = useI18n();
|
|
2516
|
+
return (_jsxs("div", { className: "mt-3 basis-full rounded-lg border bg-muted/55 px-[13px] py-[11px]", children: [_jsx("div", { className: "mb-2 text-[11.5px] font-semibold text-muted-foreground", children: t('releases.detail.history.configChanges') }), _jsx("div", { className: "ml-[3px] flex flex-col gap-2.5 border-l-[1.5px] border-dotted border-muted-foreground/60 pl-[13px]", children: changes.map((change) => (_jsxs("div", { className: "relative flex flex-col gap-1.5 sm:flex-row sm:items-baseline sm:gap-4", children: [_jsx("span", { className: "absolute left-[-16.5px] top-1.5 size-1.5 rounded-full bg-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "min-w-[118px] shrink-0 font-mono text-[11.5px] text-muted-foreground", children: formatDateTimeOrDash(change.at) }), _jsx("div", { className: "flex min-w-0 flex-wrap gap-x-0 gap-y-1", children: change.items.map((item) => (_jsxs("span", { className: "inline-flex items-baseline border-l px-3 text-[12.5px] first:border-l-0 first:pl-0", children: [_jsx("span", { className: "mr-2 font-semibold text-foreground", children: item.field }), _jsx("span", { className: "font-mono text-muted-foreground", children: item.previous }), _jsx("span", { className: "mx-1.5 text-muted-foreground", children: "\u2192" }), _jsx("span", { className: "font-mono font-semibold text-foreground", children: item.next })] }, `${change.id}-${item.field}-${item.next}`))) })] }, change.id))) })] }));
|
|
2517
|
+
}
|
|
2518
|
+
function HistoryVersionBadge({ label, variant, compact, }) {
|
|
2519
|
+
const isProduction = variant === 'production';
|
|
2520
|
+
return (_jsx("span", { className: cn('inline-flex h-[26px] w-[54px] shrink-0 items-center justify-center truncate rounded-md border px-2 font-mono text-[13px] font-semibold leading-none', compact && 'h-[25px] text-[12px]'), style: {
|
|
2521
|
+
background: isProduction ? 'var(--src-prod-soft)' : 'var(--src-canary-soft)',
|
|
2522
|
+
color: isProduction ? 'var(--src-prod-fg)' : 'var(--src-canary-fg)',
|
|
2523
|
+
borderColor: isProduction
|
|
2524
|
+
? 'color-mix(in srgb, var(--src-prod) 35%, var(--border))'
|
|
2525
|
+
: 'color-mix(in srgb, var(--src-canary) 35%, var(--border))',
|
|
2526
|
+
}, children: label }));
|
|
972
2527
|
}
|
|
973
2528
|
//# sourceMappingURL=release-line-detail-page.js.map
|