windmill-components 1.501.24 → 1.502.2
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/package/components/Dev.svelte +4 -5
- package/package/components/apps/components/display/AppCarouselList.svelte +39 -4
- package/package/components/apps/editor/appUtils.js +4 -0
- package/package/components/apps/editor/componentsPanel/componentControlUtils.js +3 -1
- package/package/components/apps/editor/settingsPanel/ComponentPanelDataSource.svelte +0 -1
- package/package/components/auditLogs/AuditLogsFilters.svelte +12 -2
- package/package/components/auditLogs/AuditLogsTable.svelte +209 -122
- package/package/components/auditLogs/AuditLogsTable.svelte.d.ts +5 -20
- package/package/components/auditLogs/AuditLogsTimeline.svelte +449 -0
- package/package/components/auditLogs/AuditLogsTimeline.svelte.d.ts +16 -0
- package/package/components/copilot/chat/AIChatDisplay.svelte +23 -165
- package/package/components/copilot/chat/AIChatInput.svelte +128 -0
- package/package/components/copilot/chat/AIChatInput.svelte.d.ts +16 -0
- package/package/components/copilot/chat/AIChatManager.svelte.d.ts +4 -1
- package/package/components/copilot/chat/AIChatManager.svelte.js +33 -13
- package/package/components/copilot/chat/AIChatMessage.svelte +93 -0
- package/package/components/copilot/chat/AIChatMessage.svelte.d.ts +12 -0
- package/package/components/copilot/chat/ContextTextarea.svelte +13 -15
- package/package/components/copilot/chat/ContextTextarea.svelte.d.ts +4 -2
- package/package/components/copilot/chat/flow/FlowAIChat.svelte +11 -0
- package/package/components/copilot/chat/shared.d.ts +13 -3
- package/package/components/flow_builder.d.ts +10 -1
- package/package/components/graph/FlowGraphV2.svelte +1 -0
- package/package/components/search/GlobalSearchModal.svelte +28 -18
- package/package/gen/core/OpenAPI.js +1 -1
- package/package/gen/schemas.gen.d.ts +11 -2
- package/package/gen/schemas.gen.js +11 -2
- package/package/gen/types.gen.d.ts +5 -2
- package/package.json +2 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<script lang="ts">import 'chartjs-adapter-date-fns';
|
|
2
|
+
import zoomPlugin from 'chartjs-plugin-zoom';
|
|
3
|
+
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement, TimeScale } from 'chart.js';
|
|
4
|
+
import { Scatter } from '../chartjs-wrappers/chartJs';
|
|
5
|
+
import { Loader2 } from 'lucide-svelte';
|
|
6
|
+
import { untrack } from 'svelte';
|
|
7
|
+
import { sleep } from '../../utils';
|
|
8
|
+
import { usePromise } from '../../svelte5Utils.svelte';
|
|
9
|
+
let { logs = [], minTimeSet, maxTimeSet, onMissingJobSpan, onZoom, onLogSelected } = $props();
|
|
10
|
+
// Register ChartJS components
|
|
11
|
+
ChartJS.register(Title, Tooltip, Legend, zoomPlugin, LineElement, CategoryScale, LinearScale, PointElement, TimeScale);
|
|
12
|
+
function addSeconds(date, seconds) {
|
|
13
|
+
date.setTime(date.getTime() + seconds * 1000);
|
|
14
|
+
return date;
|
|
15
|
+
}
|
|
16
|
+
const zoomOptions = {
|
|
17
|
+
pan: {
|
|
18
|
+
mode: 'x',
|
|
19
|
+
enabled: true,
|
|
20
|
+
modifierKey: 'ctrl',
|
|
21
|
+
onPanComplete: ({ chart }) => {
|
|
22
|
+
chartInstance = chart;
|
|
23
|
+
onZoom?.({
|
|
24
|
+
min: addSeconds(new Date(chart.scales.x.min), -1),
|
|
25
|
+
max: addSeconds(new Date(chart.scales.x.max), 1)
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
zoom: {
|
|
30
|
+
drag: {
|
|
31
|
+
enabled: true
|
|
32
|
+
},
|
|
33
|
+
mode: 'x',
|
|
34
|
+
scaleMode: 'y',
|
|
35
|
+
onZoom: ({ chart }) => {
|
|
36
|
+
chartInstance = chart;
|
|
37
|
+
onZoom?.({
|
|
38
|
+
min: addSeconds(new Date(chart.scales.x.min), -1),
|
|
39
|
+
max: addSeconds(new Date(chart.scales.x.max), 1)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// Color mapping for different action kinds
|
|
45
|
+
const actionColors = {
|
|
46
|
+
Execute: '#3b82f6', // blue
|
|
47
|
+
Delete: '#ef4444', // red
|
|
48
|
+
Update: '#eab308', // yellow
|
|
49
|
+
Create: '#22c55e', // green
|
|
50
|
+
default: '#6b7280' // gray
|
|
51
|
+
};
|
|
52
|
+
function getActionColor(actionKind) {
|
|
53
|
+
return actionColors[actionKind] || actionColors.default;
|
|
54
|
+
}
|
|
55
|
+
async function groupLogsBySpan(logs, onMissingJobSpan) {
|
|
56
|
+
const grouped = {};
|
|
57
|
+
const jobGrouped = new Map();
|
|
58
|
+
for (const log of logs) {
|
|
59
|
+
const spanId = log.span || 'untraced';
|
|
60
|
+
if (spanId.startsWith('job-span-')) {
|
|
61
|
+
const jobid = spanId.slice('job-span-'.length);
|
|
62
|
+
if (!jobGrouped.has(jobid)) {
|
|
63
|
+
jobGrouped.set(jobid, []);
|
|
64
|
+
}
|
|
65
|
+
jobGrouped.get(jobid)?.push(log);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!grouped[spanId]) {
|
|
69
|
+
grouped[spanId] = [];
|
|
70
|
+
}
|
|
71
|
+
grouped[spanId].push(log);
|
|
72
|
+
}
|
|
73
|
+
for (const jobid of jobGrouped.keys()) {
|
|
74
|
+
const j = Object.values(grouped)
|
|
75
|
+
.flat()
|
|
76
|
+
.find((log) => log.parameters?.uuid === jobid);
|
|
77
|
+
if (j?.span != undefined) {
|
|
78
|
+
grouped[j.span].push(...jobGrouped.get(jobid));
|
|
79
|
+
jobGrouped.get(jobid)?.push(j);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Try to fetch missing job execution audit log
|
|
83
|
+
if (onMissingJobSpan) {
|
|
84
|
+
try {
|
|
85
|
+
const jobLogs = jobGrouped.get(jobid);
|
|
86
|
+
const additionalLogs = await onMissingJobSpan(jobid, jobLogs);
|
|
87
|
+
// Look for the job execution audit log in the new results
|
|
88
|
+
const jobExecutionLog = additionalLogs.find((log) => log.parameters?.uuid === jobid);
|
|
89
|
+
if (jobExecutionLog?.span) {
|
|
90
|
+
if (!grouped[jobExecutionLog.span]) {
|
|
91
|
+
grouped[jobExecutionLog.span] = [];
|
|
92
|
+
}
|
|
93
|
+
grouped[jobExecutionLog.span].push(jobExecutionLog, ...jobLogs);
|
|
94
|
+
jobGrouped.get(jobid)?.push(jobExecutionLog);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.warn(`Failed to fetch missing job audit span for job ${jobid}:`, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!grouped[jobid]) {
|
|
103
|
+
grouped[jobid] = [];
|
|
104
|
+
}
|
|
105
|
+
grouped[jobid].push(...jobGrouped.get(jobid));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Sort logs within each span by timestamp
|
|
109
|
+
Object.values(grouped).forEach((spanLogs) => {
|
|
110
|
+
spanLogs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
111
|
+
});
|
|
112
|
+
return { grouped, jobGrouped };
|
|
113
|
+
}
|
|
114
|
+
function isDark() {
|
|
115
|
+
return document.documentElement.classList.contains('dark');
|
|
116
|
+
}
|
|
117
|
+
// Function to apply zoom-aware jittering to overlapping points
|
|
118
|
+
function applyJittering(dataPoints, baseY, chartInstance) {
|
|
119
|
+
if (dataPoints.length <= 1)
|
|
120
|
+
return dataPoints;
|
|
121
|
+
// Sort by timestamp
|
|
122
|
+
const sorted = [...dataPoints].sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
|
|
123
|
+
// Calculate visual overlap based on chart scale
|
|
124
|
+
const pointRadius = 0.8; // Current point radius
|
|
125
|
+
const overlapThreshold = pointRadius * 2; // Points overlap if closer than this in pixels
|
|
126
|
+
// Group points that visually overlap
|
|
127
|
+
const groups = [];
|
|
128
|
+
let currentGroup = [sorted[0]];
|
|
129
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
130
|
+
const prevTime = new Date(sorted[i - 1].x).getTime();
|
|
131
|
+
const currTime = new Date(sorted[i].x).getTime();
|
|
132
|
+
// Calculate pixel distance between points
|
|
133
|
+
let pixelDistance = overlapThreshold + 1; // Default to no overlap
|
|
134
|
+
if (chartInstance && chartInstance.scales && chartInstance.scales.x) {
|
|
135
|
+
const prevPixel = chartInstance.scales.x.getPixelForValue(prevTime);
|
|
136
|
+
const currPixel = chartInstance.scales.x.getPixelForValue(currTime);
|
|
137
|
+
pixelDistance = Math.abs(currPixel - prevPixel);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
pixelDistance = 20000;
|
|
141
|
+
}
|
|
142
|
+
if (pixelDistance < overlapThreshold) {
|
|
143
|
+
currentGroup.push(sorted[i]);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
groups.push(currentGroup);
|
|
147
|
+
currentGroup = [sorted[i]];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
groups.push(currentGroup);
|
|
151
|
+
const jitteredPoints = [];
|
|
152
|
+
groups.forEach((group) => {
|
|
153
|
+
if (group.length === 1) {
|
|
154
|
+
jitteredPoints.push({
|
|
155
|
+
...group[0],
|
|
156
|
+
y: baseY,
|
|
157
|
+
isCluster: false,
|
|
158
|
+
clusterSize: 1
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const jitterRange = 0.4;
|
|
163
|
+
group.forEach((point, index) => {
|
|
164
|
+
let jitterOffset = (1 - Math.exp(-group.length / 50)) * jitterRange * (Math.random() - 0.5);
|
|
165
|
+
jitteredPoints.push({
|
|
166
|
+
...point,
|
|
167
|
+
y: baseY + jitterOffset,
|
|
168
|
+
originalY: baseY,
|
|
169
|
+
isCluster: false,
|
|
170
|
+
clusterSize: group.length,
|
|
171
|
+
clusterIndex: index
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return jitteredPoints;
|
|
177
|
+
}
|
|
178
|
+
// Set chart defaults based on theme
|
|
179
|
+
ChartJS.defaults.color = isDark() ? '#ccc' : '#666';
|
|
180
|
+
ChartJS.defaults.borderColor = isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
181
|
+
async function getGroupedData() {
|
|
182
|
+
if (logs.length === 0) {
|
|
183
|
+
await sleep(1);
|
|
184
|
+
return { grouped: {}, jobGrouped: new Map() };
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return await groupLogsBySpan(logs, onMissingJobSpan);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error('Error grouping logs:', error);
|
|
191
|
+
return await groupLogsBySpan(logs);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let groupedData = usePromise(getGroupedData);
|
|
195
|
+
// let isGrouping = $state(false)
|
|
196
|
+
let chartInstance = $state(null);
|
|
197
|
+
let { minTime, maxTime } = $derived(computeMinMaxTime(logs, minTimeSet, maxTimeSet));
|
|
198
|
+
function computeMinMaxTime(logs, minTimeSet, maxTimeSet) {
|
|
199
|
+
let minTime = addSeconds(new Date(), -300);
|
|
200
|
+
let maxTime = new Date();
|
|
201
|
+
let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined;
|
|
202
|
+
let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined;
|
|
203
|
+
if (minTimeSetDate && maxTimeSetDate) {
|
|
204
|
+
minTime = minTimeSetDate;
|
|
205
|
+
maxTime = maxTimeSetDate;
|
|
206
|
+
return { minTime, maxTime };
|
|
207
|
+
}
|
|
208
|
+
if (logs == undefined || logs?.length == 0) {
|
|
209
|
+
minTime = minTimeSetDate ?? addSeconds(new Date(), -300);
|
|
210
|
+
maxTime = maxTimeSetDate ?? new Date();
|
|
211
|
+
return { minTime, maxTime };
|
|
212
|
+
}
|
|
213
|
+
const maxLogsTime = new Date(logs.reduce((max, current) => new Date(current.timestamp) > new Date(max.timestamp) ? current : max).timestamp);
|
|
214
|
+
const maxJob = maxTimeSetDate === undefined ? new Date() : maxLogsTime;
|
|
215
|
+
const minJob = new Date(logs.reduce((max, current) => new Date(current.timestamp) < new Date(max.timestamp) ? current : max).timestamp);
|
|
216
|
+
const diff = (maxJob.getTime() - minJob.getTime()) / 20000;
|
|
217
|
+
minTime = minTimeSetDate ?? addSeconds(minJob, -diff);
|
|
218
|
+
if (maxTimeSetDate) {
|
|
219
|
+
maxTime = maxTimeSetDate ?? maxJob;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff);
|
|
223
|
+
}
|
|
224
|
+
return { minTime, maxTime };
|
|
225
|
+
}
|
|
226
|
+
$effect(() => {
|
|
227
|
+
logs && untrack(() => groupedData.refresh());
|
|
228
|
+
});
|
|
229
|
+
const groupedLogs = $derived(groupedData.value?.grouped ?? {});
|
|
230
|
+
const jobGrouped = $derived(groupedData.value?.jobGrouped ?? new Map());
|
|
231
|
+
const spanIds = $derived(Object.keys(groupedLogs).sort());
|
|
232
|
+
const spanAuthors = $derived(spanIds.map((span) => {
|
|
233
|
+
if (span == 'untraced') {
|
|
234
|
+
return 'untraced';
|
|
235
|
+
}
|
|
236
|
+
const endUser = groupedLogs[span][0]?.parameters?.end_user;
|
|
237
|
+
const endUserText = endUser ? ` (${endUser})` : '';
|
|
238
|
+
return groupedLogs[span]?.length > 0 ? `${groupedLogs[span][0].username}${endUserText}` : '';
|
|
239
|
+
}));
|
|
240
|
+
// Transform data for ChartJS scatter plot
|
|
241
|
+
const chartData = $derived(() => {
|
|
242
|
+
if (untrack(() => logs.length) === 0) {
|
|
243
|
+
return { datasets: [] };
|
|
244
|
+
}
|
|
245
|
+
const datasets = [];
|
|
246
|
+
// Create datasets for regular span groups (points only)
|
|
247
|
+
spanIds.forEach((spanId, index) => {
|
|
248
|
+
const spanLogs = groupedLogs[spanId];
|
|
249
|
+
// Create initial data points
|
|
250
|
+
const dataPoints = spanLogs.map((log) => ({
|
|
251
|
+
x: log.timestamp,
|
|
252
|
+
y: index, // Each span gets its own y-axis position
|
|
253
|
+
log: log // Store full log data for tooltips
|
|
254
|
+
}));
|
|
255
|
+
// Apply zoom-aware jittering to spread out overlapping points
|
|
256
|
+
const jitteredPoints = applyJittering(dataPoints, index, chartInstance);
|
|
257
|
+
// const jitteredPoints = dataPoints
|
|
258
|
+
datasets.push({
|
|
259
|
+
label: spanId === 'untraced' ? 'Untraced' : spanId,
|
|
260
|
+
data: jitteredPoints,
|
|
261
|
+
backgroundColor: jitteredPoints.map((point) => {
|
|
262
|
+
const baseColor = getActionColor(point.log.action_kind);
|
|
263
|
+
// Make clustered points slightly more opaque
|
|
264
|
+
return point.isCluster ? baseColor + 'E0' : baseColor;
|
|
265
|
+
}),
|
|
266
|
+
borderColor: jitteredPoints.map((point) => {
|
|
267
|
+
const baseColor = getActionColor(point.log.action_kind);
|
|
268
|
+
// Add white border to clustered points for better visibility
|
|
269
|
+
return point.isCluster ? '#ffffff' : baseColor;
|
|
270
|
+
}),
|
|
271
|
+
borderWidth: jitteredPoints.map((point) => (point.isCluster ? 1 : 1)),
|
|
272
|
+
pointRadius: jitteredPoints.map((point) => (point.isCluster ? 3 : 3)),
|
|
273
|
+
pointHoverRadius: jitteredPoints.map((point) => (point.isCluster ? 5 : 5)),
|
|
274
|
+
showLine: false
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
// Create datasets for job-connected lines
|
|
278
|
+
jobGrouped.forEach((jobLogs, jobId) => {
|
|
279
|
+
if (jobLogs.length > 1) {
|
|
280
|
+
// Only create lines if there are multiple points
|
|
281
|
+
// Sort job logs by timestamp to ensure proper line connection
|
|
282
|
+
const sortedJobLogs = [...jobLogs].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
283
|
+
// Find the y-position for each log based on its span and jittered position
|
|
284
|
+
const lineData = sortedJobLogs.map((log) => {
|
|
285
|
+
const spanId = log.span || 'untraced';
|
|
286
|
+
let baseYPosition = spanIds.indexOf(spanId);
|
|
287
|
+
if (baseYPosition === -1) {
|
|
288
|
+
// Handle job-span logs that might not be in regular spans
|
|
289
|
+
const jobSpanId = spanId.startsWith('job-span-')
|
|
290
|
+
? spanId.slice('job-span-'.length)
|
|
291
|
+
: spanId;
|
|
292
|
+
baseYPosition = spanIds.findIndex((id) => id === jobSpanId);
|
|
293
|
+
if (baseYPosition === -1) {
|
|
294
|
+
// If still not found, assign to the span where this job's audit logs are grouped
|
|
295
|
+
const auditSpan = Object.entries(groupedLogs).find(([, spanLogs]) => spanLogs.some((l) => l.parameters?.uuid === jobId))?.[0];
|
|
296
|
+
baseYPosition = auditSpan ? spanIds.indexOf(auditSpan) : 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Find the jittered position for this specific log
|
|
300
|
+
let jitteredY = baseYPosition;
|
|
301
|
+
if (baseYPosition >= 0 && baseYPosition < datasets.length) {
|
|
302
|
+
const spanDataset = datasets[baseYPosition];
|
|
303
|
+
if (spanDataset && spanDataset.data) {
|
|
304
|
+
const matchingPoint = spanDataset.data.find((point) => point.log && point.log.id === log.id);
|
|
305
|
+
if (matchingPoint) {
|
|
306
|
+
jitteredY = matchingPoint.y;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
x: log.timestamp,
|
|
312
|
+
y: jitteredY,
|
|
313
|
+
log: log
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
datasets.push({
|
|
317
|
+
label: `Job ${jobId} Connection`,
|
|
318
|
+
data: lineData,
|
|
319
|
+
backgroundColor: 'transparent',
|
|
320
|
+
borderColor: '#8b5cf6', // Purple color for job connections
|
|
321
|
+
borderWidth: 2,
|
|
322
|
+
pointRadius: 0, // Hide points for connection lines
|
|
323
|
+
pointHoverRadius: 0,
|
|
324
|
+
showLine: true,
|
|
325
|
+
tension: 0, // Straight lines
|
|
326
|
+
fill: false
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return { datasets };
|
|
331
|
+
});
|
|
332
|
+
const chartOptions = $derived(() => ({
|
|
333
|
+
responsive: true,
|
|
334
|
+
maintainAspectRatio: false,
|
|
335
|
+
plugins: {
|
|
336
|
+
zoom: zoomOptions,
|
|
337
|
+
legend: {
|
|
338
|
+
display: false // We'll create our own legend
|
|
339
|
+
},
|
|
340
|
+
tooltip: {
|
|
341
|
+
callbacks: {
|
|
342
|
+
title: function (context) {
|
|
343
|
+
const log = context[0].raw.log;
|
|
344
|
+
let title = `${log.operation} - ${log.action_kind}`;
|
|
345
|
+
return title;
|
|
346
|
+
},
|
|
347
|
+
label: function (context) {
|
|
348
|
+
const log = context.raw.log;
|
|
349
|
+
const labels = [
|
|
350
|
+
`User: ${log.username}`,
|
|
351
|
+
`Resource: ${log.resource}`,
|
|
352
|
+
`Time: ${new Date(log.timestamp).toLocaleString()}`
|
|
353
|
+
];
|
|
354
|
+
return labels;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
scales: {
|
|
360
|
+
x: {
|
|
361
|
+
type: 'time',
|
|
362
|
+
time: {
|
|
363
|
+
displayFormats: {
|
|
364
|
+
millisecond: 'HH:mm:ss.SSS',
|
|
365
|
+
second: 'HH:mm:ss',
|
|
366
|
+
minute: 'HH:mm',
|
|
367
|
+
hour: 'MMM dd HH:mm',
|
|
368
|
+
day: 'MMM dd',
|
|
369
|
+
week: 'MMM dd',
|
|
370
|
+
month: 'MMM yyyy',
|
|
371
|
+
quarter: 'MMM yyyy',
|
|
372
|
+
year: 'yyyy'
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
title: {
|
|
376
|
+
display: true,
|
|
377
|
+
text: 'Time'
|
|
378
|
+
},
|
|
379
|
+
grid: {
|
|
380
|
+
display: false
|
|
381
|
+
},
|
|
382
|
+
ticks: {
|
|
383
|
+
maxTicksLimit: 15
|
|
384
|
+
},
|
|
385
|
+
min: minTime.getTime(),
|
|
386
|
+
max: maxTime.getTime()
|
|
387
|
+
},
|
|
388
|
+
y: {
|
|
389
|
+
type: 'linear',
|
|
390
|
+
min: -1,
|
|
391
|
+
max: spanIds.length,
|
|
392
|
+
grid: {
|
|
393
|
+
display: true,
|
|
394
|
+
color: isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
|
395
|
+
},
|
|
396
|
+
ticks: {
|
|
397
|
+
autoSkip: false,
|
|
398
|
+
maxTicksLimit: 22,
|
|
399
|
+
stepSize: 1,
|
|
400
|
+
callback: function (value) {
|
|
401
|
+
if (spanIds.length > 20) {
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
const index = Math.round(value);
|
|
405
|
+
if (index >= 0 && index < spanIds.length) {
|
|
406
|
+
// const spanId = `${spanAuthors[index]} - ${index}`
|
|
407
|
+
const spanId = spanAuthors[index];
|
|
408
|
+
return spanId === 'untraced' ? 'Untraced' : spanId.slice(0, 30);
|
|
409
|
+
}
|
|
410
|
+
return '';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
onClick: (event, elements, chart) => {
|
|
416
|
+
// Capture chart instance for jittering calculations
|
|
417
|
+
if (!chartInstance) {
|
|
418
|
+
chartInstance = chart;
|
|
419
|
+
}
|
|
420
|
+
if (elements.length > 0) {
|
|
421
|
+
const element = elements[0];
|
|
422
|
+
const log = chartData().datasets[element.datasetIndex].data[element.index].log;
|
|
423
|
+
onLogSelected?.(log);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
onHover: (event, elements, chart) => {
|
|
427
|
+
// Capture chart instance for jittering calculations
|
|
428
|
+
if (!chartInstance) {
|
|
429
|
+
chartInstance = chart;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
animation: {
|
|
433
|
+
duration: 300
|
|
434
|
+
}
|
|
435
|
+
}));
|
|
436
|
+
</script>
|
|
437
|
+
|
|
438
|
+
<div class="p-4 bg-surface mb-4 h-full">
|
|
439
|
+
{#if logs.length === 0}
|
|
440
|
+
<div class="text-center py-8 text-secondary"> No audit logs to display </div>
|
|
441
|
+
{:else if !groupedData || groupedData.status === 'loading'}
|
|
442
|
+
<div class="text-center py-8 text-secondary">
|
|
443
|
+
<Loader2 size={24} class="animate-spin mx-auto mb-2" />
|
|
444
|
+
Processing audit logs...
|
|
445
|
+
</div>
|
|
446
|
+
{:else}
|
|
447
|
+
<Scatter data={chartData()} options={chartOptions()} />
|
|
448
|
+
{/if}
|
|
449
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'chartjs-adapter-date-fns';
|
|
2
|
+
import type { AuditLog } from '../../gen';
|
|
3
|
+
interface Props {
|
|
4
|
+
logs: AuditLog[];
|
|
5
|
+
minTimeSet: string | undefined;
|
|
6
|
+
maxTimeSet: string | undefined;
|
|
7
|
+
onMissingJobSpan?: (jobId: string, jobLogs: AuditLog[]) => Promise<AuditLog[]>;
|
|
8
|
+
onZoom?: (range: {
|
|
9
|
+
min: Date;
|
|
10
|
+
max: Date;
|
|
11
|
+
}) => void;
|
|
12
|
+
onLogSelected?: (log: any) => void;
|
|
13
|
+
}
|
|
14
|
+
declare const AuditLogsTimeline: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type AuditLogsTimeline = ReturnType<typeof AuditLogsTimeline>;
|
|
16
|
+
export default AuditLogsTimeline;
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
<script lang="ts">import
|
|
2
|
-
import AssistantMessage from './AssistantMessage.svelte';
|
|
1
|
+
<script lang="ts">import AIChatMessage from './AIChatMessage.svelte';
|
|
3
2
|
import {} from 'svelte';
|
|
4
|
-
import { CheckIcon, HistoryIcon, Loader2, Plus,
|
|
5
|
-
import autosize from '../../../autosize';
|
|
3
|
+
import { CheckIcon, HistoryIcon, Loader2, Plus, StopCircleIcon, X, XIcon } from 'lucide-svelte';
|
|
6
4
|
import Button from '../../common/button/Button.svelte';
|
|
7
5
|
import Popover from '../../meltComponents/Popover.svelte';
|
|
8
6
|
import {} from './shared';
|
|
9
|
-
import ContextElementBadge from './ContextElementBadge.svelte';
|
|
10
|
-
import ContextTextarea from './ContextTextarea.svelte';
|
|
11
|
-
import AvailableContextList from './AvailableContextList.svelte';
|
|
12
7
|
import ChatQuickActions from './ChatQuickActions.svelte';
|
|
13
8
|
import ProviderModelSelector from './ProviderModelSelector.svelte';
|
|
14
9
|
import ChatMode from './ChatMode.svelte';
|
|
15
10
|
import Markdown from 'svelte-exmarkdown';
|
|
16
11
|
import { aiChatManager, AIMode } from './AIChatManager.svelte';
|
|
12
|
+
import AIChatInput from './AIChatInput.svelte';
|
|
17
13
|
let { messages, pastChats, hasDiff, diffMode = false, // todo: remove default
|
|
18
14
|
selectedContext = $bindable([]), // todo: remove default
|
|
19
15
|
availableContext = [], // todo: remove default
|
|
20
16
|
loadPastChat, deletePastChat, saveAndClear, cancel, askAi = () => { }, // todo: remove default,
|
|
21
17
|
headerLeft, headerRight, disabled = false, disabledMessage = '', suggestions = [] } = $props();
|
|
22
|
-
let
|
|
23
|
-
let
|
|
24
|
-
export function focusInput() {
|
|
25
|
-
if (aiChatManager.mode === 'script') {
|
|
26
|
-
contextTextareaComponent?.focus();
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
instructionsTextarea?.focus();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
18
|
+
let aiChatInput = $state();
|
|
19
|
+
let editingMessageIndex = $state(null);
|
|
32
20
|
let scrollEl = $state();
|
|
33
21
|
async function scrollDown() {
|
|
34
22
|
scrollEl?.scrollTo({
|
|
@@ -40,29 +28,11 @@ let height = $state(0);
|
|
|
40
28
|
$effect(() => {
|
|
41
29
|
aiChatManager.automaticScroll && height && scrollDown();
|
|
42
30
|
});
|
|
43
|
-
function addContextToSelection(contextElement) {
|
|
44
|
-
if (selectedContext &&
|
|
45
|
-
availableContext &&
|
|
46
|
-
!selectedContext.find((c) => c.type === contextElement.type && c.title === contextElement.title) &&
|
|
47
|
-
availableContext.find((c) => c.type === contextElement.type && c.title === contextElement.title)) {
|
|
48
|
-
selectedContext = [...selectedContext, contextElement];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
31
|
function submitSuggestion(suggestion) {
|
|
52
|
-
aiChatManager.instructions
|
|
53
|
-
aiChatManager.sendRequest();
|
|
32
|
+
aiChatManager.sendRequest({ instructions: suggestion });
|
|
54
33
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
58
|
-
if (messages[i].role === 'user') {
|
|
59
|
-
return i === messageIndex;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
function restartGeneration(messageIndex) {
|
|
65
|
-
aiChatManager.restartLastGeneration(messageIndex);
|
|
34
|
+
export function focusInput() {
|
|
35
|
+
aiChatInput?.focusInput();
|
|
66
36
|
}
|
|
67
37
|
</script>
|
|
68
38
|
|
|
@@ -152,68 +122,13 @@ function restartGeneration(messageIndex) {
|
|
|
152
122
|
>
|
|
153
123
|
<div class="flex flex-col" bind:clientHeight={height}>
|
|
154
124
|
{#each messages as message, messageIndex}
|
|
155
|
-
<
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{/if}
|
|
163
|
-
<div
|
|
164
|
-
class={twMerge(
|
|
165
|
-
'text-sm py-1 mx-2',
|
|
166
|
-
message.role === 'user' &&
|
|
167
|
-
'px-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 rounded-lg relative group',
|
|
168
|
-
(message.role === 'assistant' || message.role === 'tool') && 'px-[1px]',
|
|
169
|
-
message.role === 'tool' && 'text-tertiary'
|
|
170
|
-
)}
|
|
171
|
-
>
|
|
172
|
-
{#if message.role === 'assistant'}
|
|
173
|
-
<AssistantMessage {message} />
|
|
174
|
-
{:else}
|
|
175
|
-
{message.content}
|
|
176
|
-
{/if}
|
|
177
|
-
|
|
178
|
-
{#if message.role === 'user' && isLastUserMessage(messageIndex) && !aiChatManager.loading}
|
|
179
|
-
<div
|
|
180
|
-
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
181
|
-
>
|
|
182
|
-
<Button
|
|
183
|
-
size="xs2"
|
|
184
|
-
variant="border"
|
|
185
|
-
color="light"
|
|
186
|
-
iconOnly
|
|
187
|
-
title="Restart generation"
|
|
188
|
-
startIcon={{ icon: RefreshCwIcon }}
|
|
189
|
-
btnClasses="!p-1 !h-6 !w-6"
|
|
190
|
-
on:click={() => restartGeneration(messageIndex)}
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
193
|
-
{/if}
|
|
194
|
-
</div>
|
|
195
|
-
{#if message.role === 'user' && message.snapshot}
|
|
196
|
-
<div
|
|
197
|
-
class="mx-2 text-sm text-tertiary flex flex-row items-center justify-between gap-2 mt-2"
|
|
198
|
-
>
|
|
199
|
-
Saved a flow snapshot
|
|
200
|
-
<Button
|
|
201
|
-
size="xs2"
|
|
202
|
-
variant="border"
|
|
203
|
-
color="light"
|
|
204
|
-
on:click={() => {
|
|
205
|
-
if (message.snapshot) {
|
|
206
|
-
aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot)
|
|
207
|
-
}
|
|
208
|
-
}}
|
|
209
|
-
title="Revert to snapshot"
|
|
210
|
-
startIcon={{ icon: Undo2Icon }}
|
|
211
|
-
>
|
|
212
|
-
Revert
|
|
213
|
-
</Button>
|
|
214
|
-
</div>
|
|
215
|
-
{/if}
|
|
216
|
-
</div>
|
|
125
|
+
<AIChatMessage
|
|
126
|
+
{message}
|
|
127
|
+
{messageIndex}
|
|
128
|
+
{availableContext}
|
|
129
|
+
bind:selectedContext
|
|
130
|
+
bind:editingMessageIndex
|
|
131
|
+
/>
|
|
217
132
|
{/each}
|
|
218
133
|
{#if aiChatManager.loading && !aiChatManager.currentReply}
|
|
219
134
|
<div class="mb-6 py-1 px-2">
|
|
@@ -267,71 +182,14 @@ function restartGeneration(messageIndex) {
|
|
|
267
182
|
</Button>
|
|
268
183
|
</div>
|
|
269
184
|
{/if}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
</svelte:fragment>
|
|
279
|
-
<svelte:fragment slot="content" let:close>
|
|
280
|
-
<AvailableContextList
|
|
281
|
-
{availableContext}
|
|
282
|
-
{selectedContext}
|
|
283
|
-
onSelect={(element) => {
|
|
284
|
-
addContextToSelection(element)
|
|
285
|
-
close()
|
|
286
|
-
}}
|
|
287
|
-
/>
|
|
288
|
-
</svelte:fragment>
|
|
289
|
-
</Popover>
|
|
290
|
-
{#each selectedContext as element}
|
|
291
|
-
<ContextElementBadge
|
|
292
|
-
contextElement={element}
|
|
293
|
-
deletable
|
|
294
|
-
on:delete={() => {
|
|
295
|
-
selectedContext = selectedContext?.filter(
|
|
296
|
-
(c) => c.type !== element.type || c.title !== element.title
|
|
297
|
-
)
|
|
298
|
-
}}
|
|
299
|
-
/>
|
|
300
|
-
{/each}
|
|
301
|
-
</div>
|
|
302
|
-
<ContextTextarea
|
|
303
|
-
bind:this={contextTextareaComponent}
|
|
304
|
-
{availableContext}
|
|
305
|
-
{selectedContext}
|
|
306
|
-
isFirstMessage={messages.length === 0}
|
|
307
|
-
onAddContext={(contextElement) => addContextToSelection(contextElement)}
|
|
308
|
-
onSendRequest={() => {
|
|
309
|
-
if (!aiChatManager.loading) {
|
|
310
|
-
aiChatManager.sendRequest()
|
|
311
|
-
}
|
|
312
|
-
}}
|
|
313
|
-
onUpdateInstructions={(value) => (aiChatManager.instructions = value)}
|
|
314
|
-
{disabled}
|
|
315
|
-
/>
|
|
316
|
-
{:else}
|
|
317
|
-
<div class="relative w-full px-2 scroll-pb-2 pt-2">
|
|
318
|
-
<textarea
|
|
319
|
-
bind:this={instructionsTextarea}
|
|
320
|
-
bind:value={aiChatManager.instructions}
|
|
321
|
-
use:autosize
|
|
322
|
-
onkeydown={(e) => {
|
|
323
|
-
if (e.key === 'Enter' && !e.shiftKey && !aiChatManager.loading) {
|
|
324
|
-
e.preventDefault()
|
|
325
|
-
aiChatManager.sendRequest()
|
|
326
|
-
}
|
|
327
|
-
}}
|
|
328
|
-
rows={3}
|
|
329
|
-
placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
|
|
330
|
-
class="resize-none"
|
|
331
|
-
{disabled}
|
|
332
|
-
></textarea>
|
|
333
|
-
</div>
|
|
334
|
-
{/if}
|
|
185
|
+
<AIChatInput
|
|
186
|
+
bind:this={aiChatInput}
|
|
187
|
+
bind:selectedContext
|
|
188
|
+
{availableContext}
|
|
189
|
+
{disabled}
|
|
190
|
+
isFirstMessage={messages.length === 0}
|
|
191
|
+
placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
|
|
192
|
+
/>
|
|
335
193
|
<div
|
|
336
194
|
class={`flex flex-row ${
|
|
337
195
|
aiChatManager.mode === 'script' && hasDiff ? 'justify-between' : 'justify-end'
|