@burdenoff/fluidgrids-fe-sdk 2026.1031.6 → 2026.1101.1
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/{ExecutionDetailPage-GKX4OUUR.js → ExecutionDetailPage-4LPCNWM3.js} +11 -11
- package/dist/{ExecutionsListPage-ZZOUZQBS.js → ExecutionsListPage-HNPUKYD7.js} +8 -8
- package/dist/{WorkflowBuilder-4QEJNKVE.js → WorkflowBuilder-QFSCYCUC.js} +12 -11
- package/dist/{WorkflowsList-4CTHUYM7.js → WorkflowsList-RV4BRJO7.js} +8 -8
- package/dist/{chunk-JUHSLK7C.js → chunk-26NENGXA.js} +5 -5
- package/dist/{chunk-RYUSWVIR.js → chunk-3M36XWAH.js} +1014 -5
- package/dist/{chunk-EPYTVCYH.js → chunk-6ILIBJFH.js} +16 -4
- package/dist/{chunk-I5BPMBIV.js → chunk-6JX5UPDW.js} +1 -1
- package/dist/{chunk-HCKYGGUU.js → chunk-76UUEMTN.js} +4 -4
- package/dist/{chunk-P4R4ACCC.js → chunk-77HIU3CX.js} +1 -1
- package/dist/{chunk-HQX7JZLY.js → chunk-7KKYIRLH.js} +26 -1
- package/dist/{chunk-GREPXIXS.js → chunk-A47PSD7A.js} +2 -2
- package/dist/{chunk-34HHBNZE.js → chunk-AD7BICOT.js} +1 -1
- package/dist/{chunk-NLRQSHRS.js → chunk-ALFMJSD6.js} +2 -2
- package/dist/{chunk-ZEHMJURX.js → chunk-BXR6BWXM.js} +5 -5
- package/dist/{chunk-AHDYETEE.js → chunk-G634XDO3.js} +1 -1
- package/dist/chunk-GLYINMOR.js +27 -0
- package/dist/{chunk-KEYKB4E7.js → chunk-IHEV5HTD.js} +10 -1
- package/dist/{chunk-5XQMYHJ7.js → chunk-JWROEOG5.js} +2 -2
- package/dist/{chunk-GMVXPC5N.js → chunk-NMEI7TQW.js} +4 -4
- package/dist/{chunk-TVOXRODW.js → chunk-PZ45H72P.js} +1 -1
- package/dist/{chunk-IJBT25CS.js → chunk-QPZCJKII.js} +3 -3
- package/dist/{chunk-PP62ABIG.js → chunk-RIHYPPMQ.js} +1 -1
- package/dist/{chunk-FUYHCONW.js → chunk-S7XV65DB.js} +19 -7
- package/dist/{chunk-VWFA3RQR.js → chunk-SEFOT2LD.js} +2 -2
- package/dist/{chunk-3KKGE7CE.js → chunk-UO5DUKYN.js} +9 -9
- package/dist/{chunk-34XK5BC7.js → chunk-VJL3DFJT.js} +4 -4
- package/dist/{chunk-CAZHE2MQ.js → chunk-WNZV4SLU.js} +5 -5
- package/dist/{chunk-E7AQAPZN.js → chunk-XKQMCPSB.js} +4 -4
- package/dist/{chunk-35N3PEO6.js → chunk-YOLPHVOE.js} +1 -1
- package/dist/executions.js +12 -12
- package/dist/history.js +4 -4
- package/dist/index.d.ts +1735 -1244
- package/dist/index.js +39 -38
- package/dist/nodes.js +2 -2
- package/dist/observability.js +7 -6
- package/dist/settings.js +6 -6
- package/dist/templates.js +11 -10
- package/dist/workflow-builder.js +13 -12
- package/dist/workflows-list.js +8 -8
- package/package.json +1 -1
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { FailureTriagePanel } from './chunk-
|
|
2
|
-
import { useDataSinks, useDataSinkData, useInsightsData, useRunReplayActions } from './chunk-
|
|
3
|
-
import {
|
|
1
|
+
import { FailureTriagePanel } from './chunk-ALFMJSD6.js';
|
|
2
|
+
import { useDataSinks, useDataSinkData, useInsightsData, useRunReplayActions } from './chunk-RIHYPPMQ.js';
|
|
3
|
+
import { proposalCanvasPath } from './chunk-GLYINMOR.js';
|
|
4
|
+
import { useContinuousOptimisationEnabled, throwIfResultError } from './chunk-6ILIBJFH.js';
|
|
5
|
+
import { useFluidGridsPermission, FLUIDGRIDS_PERMISSIONS, SdkPermissionButton } from './chunk-ZUVFD5KV.js';
|
|
6
|
+
import { SetContinuousOptimisationEnabledDocument, FgRunOptimisationScanDocument, ProposeOptimisationDocument, WorkflowProposalDocument, DismissOptimisationSuggestionDocument, SnoozeOptimisationSuggestionDocument, OptimisationSuggestionsDocument, ContinuousOptimisationSettingsDocument } from './chunk-IHEV5HTD.js';
|
|
4
7
|
import { useTr } from './chunk-UULVIT25.js';
|
|
5
|
-
import {
|
|
8
|
+
import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
|
|
9
|
+
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
6
10
|
import { Icon } from '@iconify/react';
|
|
7
11
|
import { Button } from '@burdenoff/fe-libs/button';
|
|
8
|
-
import { Input } from '@burdenoff/fe-libs/ui';
|
|
12
|
+
import { Input, Button as Button$1, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, Textarea, DialogFooter } from '@burdenoff/fe-libs/ui';
|
|
9
13
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
10
14
|
import { useNavigate } from 'react-router';
|
|
11
15
|
import { toast } from 'sonner';
|
|
16
|
+
import { Lightbulb, Scissors, Timer, ShieldCheck, DatabaseZap, BadgeDollarSign, RotateCcw } from 'lucide-react';
|
|
12
17
|
|
|
13
18
|
// src/modules/observability/utils.ts
|
|
14
19
|
function percentage(value, total) {
|
|
@@ -339,6 +344,999 @@ function DataSinkDashboardPage() {
|
|
|
339
344
|
] })
|
|
340
345
|
] });
|
|
341
346
|
}
|
|
347
|
+
var SUGGESTIONS_PAGE_SIZE = 20;
|
|
348
|
+
function messageOf(error) {
|
|
349
|
+
return error instanceof Error ? error.message : String(error);
|
|
350
|
+
}
|
|
351
|
+
function suggestionsReadable(settings) {
|
|
352
|
+
return Boolean(settings?.killSwitchOn && settings.enabled);
|
|
353
|
+
}
|
|
354
|
+
async function fetchSettings() {
|
|
355
|
+
const result = throwIfResultError(
|
|
356
|
+
await getFluidGridsClient().query({
|
|
357
|
+
query: ContinuousOptimisationSettingsDocument,
|
|
358
|
+
fetchPolicy: "network-only"
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
return result.data?.continuousOptimisationSettings ?? null;
|
|
362
|
+
}
|
|
363
|
+
async function fetchOpenSuggestions() {
|
|
364
|
+
const result = throwIfResultError(
|
|
365
|
+
await getFluidGridsClient().query({
|
|
366
|
+
query: OptimisationSuggestionsDocument,
|
|
367
|
+
variables: {
|
|
368
|
+
filter: { status: ["OPEN" /* Open */] },
|
|
369
|
+
limit: SUGGESTIONS_PAGE_SIZE,
|
|
370
|
+
offset: 0
|
|
371
|
+
},
|
|
372
|
+
fetchPolicy: "network-only"
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
const list = result.data?.optimisationSuggestions;
|
|
376
|
+
return { items: list?.items ?? [], total: list?.total ?? 0 };
|
|
377
|
+
}
|
|
378
|
+
function useContinuousOptimisation(active) {
|
|
379
|
+
const [settings, setSettings] = useState(null);
|
|
380
|
+
const [suggestions, setSuggestions] = useState(
|
|
381
|
+
[]
|
|
382
|
+
);
|
|
383
|
+
const [total, setTotal] = useState(0);
|
|
384
|
+
const [loading, setLoading] = useState(active);
|
|
385
|
+
const [busy, setBusy] = useState(false);
|
|
386
|
+
const [pendingId, setPendingId] = useState(null);
|
|
387
|
+
const [error, setError] = useState(null);
|
|
388
|
+
const [lastScan, setLastScan] = useState(null);
|
|
389
|
+
const loadRef = useRef(0);
|
|
390
|
+
const refetchRef = useRef(0);
|
|
391
|
+
const loadSuggestions = useCallback(
|
|
392
|
+
async (next, load) => {
|
|
393
|
+
if (!suggestionsReadable(next)) {
|
|
394
|
+
if (load === loadRef.current) {
|
|
395
|
+
setSuggestions([]);
|
|
396
|
+
setTotal(0);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const list = await fetchOpenSuggestions();
|
|
401
|
+
if (load !== loadRef.current) return;
|
|
402
|
+
setSuggestions(list.items);
|
|
403
|
+
setTotal(list.total);
|
|
404
|
+
},
|
|
405
|
+
[]
|
|
406
|
+
);
|
|
407
|
+
const refetch = useCallback(async () => {
|
|
408
|
+
const load = ++loadRef.current;
|
|
409
|
+
const refetchToken = ++refetchRef.current;
|
|
410
|
+
setLoading(true);
|
|
411
|
+
setError(null);
|
|
412
|
+
try {
|
|
413
|
+
const next = await fetchSettings();
|
|
414
|
+
if (load !== loadRef.current) return;
|
|
415
|
+
setSettings(next);
|
|
416
|
+
await loadSuggestions(next, load);
|
|
417
|
+
} catch (loadError) {
|
|
418
|
+
if (load === loadRef.current) setError(messageOf(loadError));
|
|
419
|
+
} finally {
|
|
420
|
+
if (refetchToken === refetchRef.current) setLoading(false);
|
|
421
|
+
}
|
|
422
|
+
}, [loadSuggestions]);
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
void refetch();
|
|
425
|
+
return () => {
|
|
426
|
+
loadRef.current++;
|
|
427
|
+
};
|
|
428
|
+
}, [active, refetch]);
|
|
429
|
+
const setEnabled = useCallback(
|
|
430
|
+
async (enabled) => {
|
|
431
|
+
const load = ++loadRef.current;
|
|
432
|
+
setBusy(true);
|
|
433
|
+
setError(null);
|
|
434
|
+
try {
|
|
435
|
+
const result = throwIfResultError(
|
|
436
|
+
await getFluidGridsClient().mutate({
|
|
437
|
+
mutation: SetContinuousOptimisationEnabledDocument,
|
|
438
|
+
variables: { enabled }
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
const next = result.data?.setContinuousOptimisationEnabled ?? null;
|
|
442
|
+
if (load !== loadRef.current) return;
|
|
443
|
+
setSettings(next);
|
|
444
|
+
await loadSuggestions(next, load);
|
|
445
|
+
} catch (toggleError) {
|
|
446
|
+
if (load === loadRef.current) setError(messageOf(toggleError));
|
|
447
|
+
} finally {
|
|
448
|
+
setBusy(false);
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
[loadSuggestions]
|
|
452
|
+
);
|
|
453
|
+
const runNow = useCallback(async () => {
|
|
454
|
+
const load = ++loadRef.current;
|
|
455
|
+
setBusy(true);
|
|
456
|
+
setError(null);
|
|
457
|
+
try {
|
|
458
|
+
const result = throwIfResultError(
|
|
459
|
+
await getFluidGridsClient().mutate({ mutation: FgRunOptimisationScanDocument })
|
|
460
|
+
);
|
|
461
|
+
if (load !== loadRef.current) return;
|
|
462
|
+
setLastScan(result.data?.fgRunOptimisationScan ?? null);
|
|
463
|
+
const next = await fetchSettings();
|
|
464
|
+
if (load !== loadRef.current) return;
|
|
465
|
+
setSettings(next);
|
|
466
|
+
await loadSuggestions(next, load);
|
|
467
|
+
} catch (scanError) {
|
|
468
|
+
if (load === loadRef.current) setError(messageOf(scanError));
|
|
469
|
+
} finally {
|
|
470
|
+
setBusy(false);
|
|
471
|
+
}
|
|
472
|
+
}, [loadSuggestions]);
|
|
473
|
+
const replace = useCallback((updated) => {
|
|
474
|
+
setSuggestions(
|
|
475
|
+
(current) => current.map((item) => item.id === updated.id ? updated : item)
|
|
476
|
+
);
|
|
477
|
+
}, []);
|
|
478
|
+
const remove = useCallback((id) => {
|
|
479
|
+
setSuggestions((current) => current.filter((item) => item.id !== id));
|
|
480
|
+
setTotal((count) => Math.max(0, count - 1));
|
|
481
|
+
}, []);
|
|
482
|
+
const propose = useCallback(
|
|
483
|
+
async (id) => {
|
|
484
|
+
setPendingId(id);
|
|
485
|
+
setError(null);
|
|
486
|
+
try {
|
|
487
|
+
const client = getFluidGridsClient();
|
|
488
|
+
const result = throwIfResultError(
|
|
489
|
+
await client.mutate({
|
|
490
|
+
mutation: ProposeOptimisationDocument,
|
|
491
|
+
variables: { suggestionId: id }
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
const updated = result.data?.proposeOptimisation ?? null;
|
|
495
|
+
if (!updated?.proposalId) return null;
|
|
496
|
+
replace(updated);
|
|
497
|
+
const proposal = throwIfResultError(
|
|
498
|
+
await client.query({
|
|
499
|
+
query: WorkflowProposalDocument,
|
|
500
|
+
variables: { id: updated.proposalId },
|
|
501
|
+
fetchPolicy: "network-only"
|
|
502
|
+
})
|
|
503
|
+
).data?.workflowProposal;
|
|
504
|
+
return {
|
|
505
|
+
workflowKey: proposal?.workflowKey || updated.workflowKey,
|
|
506
|
+
version: proposal?.baseVersion || updated.workflowVersion,
|
|
507
|
+
proposalId: updated.proposalId
|
|
508
|
+
};
|
|
509
|
+
} catch (proposeError) {
|
|
510
|
+
setError(messageOf(proposeError));
|
|
511
|
+
return null;
|
|
512
|
+
} finally {
|
|
513
|
+
setPendingId(null);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
[replace]
|
|
517
|
+
);
|
|
518
|
+
const dismiss = useCallback(
|
|
519
|
+
async (id, reason, note) => {
|
|
520
|
+
setPendingId(id);
|
|
521
|
+
setError(null);
|
|
522
|
+
const trimmed = note?.trim();
|
|
523
|
+
try {
|
|
524
|
+
throwIfResultError(
|
|
525
|
+
await getFluidGridsClient().mutate({
|
|
526
|
+
mutation: DismissOptimisationSuggestionDocument,
|
|
527
|
+
variables: { id, reason, ...trimmed ? { note: trimmed } : {} }
|
|
528
|
+
})
|
|
529
|
+
);
|
|
530
|
+
remove(id);
|
|
531
|
+
return true;
|
|
532
|
+
} catch (dismissError) {
|
|
533
|
+
setError(messageOf(dismissError));
|
|
534
|
+
return false;
|
|
535
|
+
} finally {
|
|
536
|
+
setPendingId(null);
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
[remove]
|
|
540
|
+
);
|
|
541
|
+
const snooze = useCallback(
|
|
542
|
+
async (id) => {
|
|
543
|
+
setPendingId(id);
|
|
544
|
+
setError(null);
|
|
545
|
+
try {
|
|
546
|
+
throwIfResultError(
|
|
547
|
+
await getFluidGridsClient().mutate({
|
|
548
|
+
mutation: SnoozeOptimisationSuggestionDocument,
|
|
549
|
+
variables: { id }
|
|
550
|
+
})
|
|
551
|
+
);
|
|
552
|
+
remove(id);
|
|
553
|
+
return true;
|
|
554
|
+
} catch (snoozeError) {
|
|
555
|
+
setError(messageOf(snoozeError));
|
|
556
|
+
return false;
|
|
557
|
+
} finally {
|
|
558
|
+
setPendingId(null);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
[remove]
|
|
562
|
+
);
|
|
563
|
+
return {
|
|
564
|
+
settings,
|
|
565
|
+
suggestions,
|
|
566
|
+
total,
|
|
567
|
+
loading,
|
|
568
|
+
busy,
|
|
569
|
+
pendingId,
|
|
570
|
+
error,
|
|
571
|
+
lastScan,
|
|
572
|
+
refetch,
|
|
573
|
+
setEnabled,
|
|
574
|
+
runNow,
|
|
575
|
+
propose,
|
|
576
|
+
dismiss,
|
|
577
|
+
snooze
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/modules/observability/optimisation.ts
|
|
582
|
+
var DEFAULT_WINDOW_DAYS = 30;
|
|
583
|
+
var DISMISS_REASONS = [
|
|
584
|
+
"NOT_APPLICABLE" /* NotApplicable */,
|
|
585
|
+
"ACCEPTED_COST" /* AcceptedCost */,
|
|
586
|
+
"WILL_FIX_LATER" /* WillFixLater */,
|
|
587
|
+
"OTHER" /* Other */
|
|
588
|
+
];
|
|
589
|
+
var MAX_DISMISS_NOTE_LENGTH = 500;
|
|
590
|
+
function isRecord(value) {
|
|
591
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
592
|
+
}
|
|
593
|
+
function evidenceRecord(evidence) {
|
|
594
|
+
if (typeof evidence === "string") {
|
|
595
|
+
try {
|
|
596
|
+
const parsed = JSON.parse(evidence);
|
|
597
|
+
return isRecord(parsed) ? parsed : {};
|
|
598
|
+
} catch {
|
|
599
|
+
return {};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return isRecord(evidence) ? evidence : {};
|
|
603
|
+
}
|
|
604
|
+
function numberAt(evidence, key) {
|
|
605
|
+
const value = evidence[key];
|
|
606
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
607
|
+
}
|
|
608
|
+
function formatShare(value) {
|
|
609
|
+
return `${Math.round(value * 100)}%`;
|
|
610
|
+
}
|
|
611
|
+
function formatSeconds(ms) {
|
|
612
|
+
const seconds = ms / 1e3;
|
|
613
|
+
return `${Number.isInteger(seconds) ? seconds : seconds.toFixed(1)} s`;
|
|
614
|
+
}
|
|
615
|
+
function formatCount(value) {
|
|
616
|
+
return Math.round(value).toLocaleString("en-US");
|
|
617
|
+
}
|
|
618
|
+
function formatMicroUsd(value) {
|
|
619
|
+
const dollars = value / 1e6;
|
|
620
|
+
if (dollars > 0 && dollars < 0.01) return "< $0.01";
|
|
621
|
+
return `$${dollars.toLocaleString("en-US", {
|
|
622
|
+
minimumFractionDigits: 2,
|
|
623
|
+
maximumFractionDigits: 2
|
|
624
|
+
})}`;
|
|
625
|
+
}
|
|
626
|
+
function monthlySavingLabel(microUsd) {
|
|
627
|
+
if (microUsd === null || microUsd === void 0) return null;
|
|
628
|
+
if (!Number.isFinite(microUsd) || microUsd <= 0) return null;
|
|
629
|
+
return formatMicroUsd(microUsd);
|
|
630
|
+
}
|
|
631
|
+
function evidenceWindowDays(evidence) {
|
|
632
|
+
const days = numberAt(evidenceRecord(evidence), "windowDays");
|
|
633
|
+
return days !== null && days > 0 ? days : DEFAULT_WINDOW_DAYS;
|
|
634
|
+
}
|
|
635
|
+
var FACTS = {
|
|
636
|
+
["RAISE_TIMEOUT" /* RaiseTimeout */]: [
|
|
637
|
+
{ id: "executions", format: formatCount },
|
|
638
|
+
{ id: "timeoutRate", format: formatShare },
|
|
639
|
+
{ id: "currentTimeoutMs", format: formatSeconds },
|
|
640
|
+
{
|
|
641
|
+
id: "p95CompletedMs",
|
|
642
|
+
format: formatSeconds
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "suggestedTimeoutMs",
|
|
646
|
+
format: formatSeconds
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
["ADD_RETRY" /* AddRetry */]: [
|
|
650
|
+
{ id: "executions", format: formatCount },
|
|
651
|
+
{
|
|
652
|
+
id: "transientFailures",
|
|
653
|
+
format: formatCount
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "transientFailureRate",
|
|
657
|
+
format: formatShare
|
|
658
|
+
}
|
|
659
|
+
],
|
|
660
|
+
["UNUSED_NODE" /* UnusedNode */]: [{ id: "completedRuns", format: formatCount }],
|
|
661
|
+
["ENABLE_CACHE" /* EnableCache */]: [
|
|
662
|
+
{ id: "calls", format: formatCount },
|
|
663
|
+
{ id: "repeatedCalls", format: formatCount },
|
|
664
|
+
{ id: "repeatShare", format: formatShare },
|
|
665
|
+
{ id: "costMicroUsd", format: formatMicroUsd }
|
|
666
|
+
],
|
|
667
|
+
["CHEAPER_MODEL" /* CheaperModel */]: [],
|
|
668
|
+
["MISSING_GUARDRAIL" /* MissingGuardrail */]: []
|
|
669
|
+
};
|
|
670
|
+
function evidenceFacts(kind, evidence) {
|
|
671
|
+
const record = evidenceRecord(evidence);
|
|
672
|
+
const facts = [];
|
|
673
|
+
for (const spec of FACTS[kind] ?? []) {
|
|
674
|
+
const value = numberAt(record, spec.id);
|
|
675
|
+
if (value === null) continue;
|
|
676
|
+
facts.push({ id: spec.id, value: spec.format(value) });
|
|
677
|
+
}
|
|
678
|
+
if (kind === "UNUSED_NODE" /* UnusedNode */) {
|
|
679
|
+
const removed = record.removedNodeIds;
|
|
680
|
+
if (Array.isArray(removed) && removed.length > 0) {
|
|
681
|
+
facts.push({
|
|
682
|
+
id: "removedNodeIds",
|
|
683
|
+
value: removed.map(String).join(", ")
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (kind === "ADD_RETRY" /* AddRetry */) {
|
|
688
|
+
const byType = record.failuresByType;
|
|
689
|
+
if (isRecord(byType)) {
|
|
690
|
+
const parts = Object.entries(byType).filter(
|
|
691
|
+
(entry) => typeof entry[1] === "number" && entry[1] > 0
|
|
692
|
+
).map(([type, count]) => `${type} ${formatCount(count)}`);
|
|
693
|
+
if (parts.length > 0) {
|
|
694
|
+
facts.push({
|
|
695
|
+
id: "failuresByType",
|
|
696
|
+
value: parts.join(", ")
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return facts;
|
|
702
|
+
}
|
|
703
|
+
function shortRunId(runId) {
|
|
704
|
+
return runId.length > 8 ? runId.slice(0, 8) : runId;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/modules/observability/optimisation-labels.ts
|
|
708
|
+
function kindLabel(tr, kind) {
|
|
709
|
+
switch (kind) {
|
|
710
|
+
case "ADD_RETRY" /* AddRetry */:
|
|
711
|
+
return tr(
|
|
712
|
+
"fluidgrids.optimisation.kind.ADD_RETRY",
|
|
713
|
+
"Retry transient failures"
|
|
714
|
+
);
|
|
715
|
+
case "CHEAPER_MODEL" /* CheaperModel */:
|
|
716
|
+
return tr(
|
|
717
|
+
"fluidgrids.optimisation.kind.CHEAPER_MODEL",
|
|
718
|
+
"Use a cheaper model"
|
|
719
|
+
);
|
|
720
|
+
case "ENABLE_CACHE" /* EnableCache */:
|
|
721
|
+
return tr(
|
|
722
|
+
"fluidgrids.optimisation.kind.ENABLE_CACHE",
|
|
723
|
+
"Cache repeated model calls"
|
|
724
|
+
);
|
|
725
|
+
case "MISSING_GUARDRAIL" /* MissingGuardrail */:
|
|
726
|
+
return tr(
|
|
727
|
+
"fluidgrids.optimisation.kind.MISSING_GUARDRAIL",
|
|
728
|
+
"Add a guardrail policy"
|
|
729
|
+
);
|
|
730
|
+
case "RAISE_TIMEOUT" /* RaiseTimeout */:
|
|
731
|
+
return tr(
|
|
732
|
+
"fluidgrids.optimisation.kind.RAISE_TIMEOUT",
|
|
733
|
+
"Raise the node timeout"
|
|
734
|
+
);
|
|
735
|
+
case "UNUSED_NODE" /* UnusedNode */:
|
|
736
|
+
return tr(
|
|
737
|
+
"fluidgrids.optimisation.kind.UNUSED_NODE",
|
|
738
|
+
"Remove nodes that never run"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function evidenceLabel(tr, id) {
|
|
743
|
+
switch (id) {
|
|
744
|
+
case "executions":
|
|
745
|
+
return tr("fluidgrids.optimisation.evidence.executions", "Executions");
|
|
746
|
+
case "timeoutRate":
|
|
747
|
+
return tr("fluidgrids.optimisation.evidence.timeoutRate", "Timed out");
|
|
748
|
+
case "currentTimeoutMs":
|
|
749
|
+
return tr(
|
|
750
|
+
"fluidgrids.optimisation.evidence.currentTimeoutMs",
|
|
751
|
+
"Current timeout"
|
|
752
|
+
);
|
|
753
|
+
case "p95CompletedMs":
|
|
754
|
+
return tr(
|
|
755
|
+
"fluidgrids.optimisation.evidence.p95CompletedMs",
|
|
756
|
+
"p95 when it finishes"
|
|
757
|
+
);
|
|
758
|
+
case "suggestedTimeoutMs":
|
|
759
|
+
return tr(
|
|
760
|
+
"fluidgrids.optimisation.evidence.suggestedTimeoutMs",
|
|
761
|
+
"Suggested timeout"
|
|
762
|
+
);
|
|
763
|
+
case "transientFailures":
|
|
764
|
+
return tr(
|
|
765
|
+
"fluidgrids.optimisation.evidence.transientFailures",
|
|
766
|
+
"Temporary failures"
|
|
767
|
+
);
|
|
768
|
+
case "transientFailureRate":
|
|
769
|
+
return tr(
|
|
770
|
+
"fluidgrids.optimisation.evidence.transientFailureRate",
|
|
771
|
+
"Failure rate"
|
|
772
|
+
);
|
|
773
|
+
case "failuresByType":
|
|
774
|
+
return tr(
|
|
775
|
+
"fluidgrids.optimisation.evidence.failuresByType",
|
|
776
|
+
"By error type"
|
|
777
|
+
);
|
|
778
|
+
case "completedRuns":
|
|
779
|
+
return tr(
|
|
780
|
+
"fluidgrids.optimisation.evidence.completedRuns",
|
|
781
|
+
"Completed runs"
|
|
782
|
+
);
|
|
783
|
+
case "removedNodeIds":
|
|
784
|
+
return tr(
|
|
785
|
+
"fluidgrids.optimisation.evidence.removedNodeIds",
|
|
786
|
+
"Nodes removed"
|
|
787
|
+
);
|
|
788
|
+
case "calls":
|
|
789
|
+
return tr("fluidgrids.optimisation.evidence.calls", "Model calls");
|
|
790
|
+
case "repeatedCalls":
|
|
791
|
+
return tr(
|
|
792
|
+
"fluidgrids.optimisation.evidence.repeatedCalls",
|
|
793
|
+
"Repeated calls"
|
|
794
|
+
);
|
|
795
|
+
case "repeatShare":
|
|
796
|
+
return tr("fluidgrids.optimisation.evidence.repeatShare", "Repeated");
|
|
797
|
+
case "costMicroUsd":
|
|
798
|
+
return tr("fluidgrids.optimisation.evidence.costMicroUsd", "Model spend");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function dismissReasonLabel(tr, reason) {
|
|
802
|
+
switch (reason) {
|
|
803
|
+
case "NOT_APPLICABLE" /* NotApplicable */:
|
|
804
|
+
return tr(
|
|
805
|
+
"fluidgrids.optimisation.dismissReasons.NOT_APPLICABLE",
|
|
806
|
+
"Not applicable to this workflow"
|
|
807
|
+
);
|
|
808
|
+
case "ACCEPTED_COST" /* AcceptedCost */:
|
|
809
|
+
return tr(
|
|
810
|
+
"fluidgrids.optimisation.dismissReasons.ACCEPTED_COST",
|
|
811
|
+
"The cost is accepted"
|
|
812
|
+
);
|
|
813
|
+
case "WILL_FIX_LATER" /* WillFixLater */:
|
|
814
|
+
return tr(
|
|
815
|
+
"fluidgrids.optimisation.dismissReasons.WILL_FIX_LATER",
|
|
816
|
+
"Will fix it later"
|
|
817
|
+
);
|
|
818
|
+
case "OTHER" /* Other */:
|
|
819
|
+
return tr("fluidgrids.optimisation.dismissReasons.OTHER", "Other reason");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
var KIND_ICONS = {
|
|
823
|
+
["ADD_RETRY" /* AddRetry */]: RotateCcw,
|
|
824
|
+
["CHEAPER_MODEL" /* CheaperModel */]: BadgeDollarSign,
|
|
825
|
+
["ENABLE_CACHE" /* EnableCache */]: DatabaseZap,
|
|
826
|
+
["MISSING_GUARDRAIL" /* MissingGuardrail */]: ShieldCheck,
|
|
827
|
+
["RAISE_TIMEOUT" /* RaiseTimeout */]: Timer,
|
|
828
|
+
["UNUSED_NODE" /* UnusedNode */]: Scissors
|
|
829
|
+
};
|
|
830
|
+
var MAX_RUN_LINKS = 3;
|
|
831
|
+
function OptimisationSuggestionsPanel(props) {
|
|
832
|
+
const enabled = useContinuousOptimisationEnabled();
|
|
833
|
+
if (!enabled) return null;
|
|
834
|
+
return /* @__PURE__ */ jsx(OptimisationSuggestionsSection, { ...props });
|
|
835
|
+
}
|
|
836
|
+
function OptimisationSuggestionsSection({
|
|
837
|
+
onOpenRun,
|
|
838
|
+
onOpenProposal
|
|
839
|
+
}) {
|
|
840
|
+
const tr = useTr();
|
|
841
|
+
const optimisation = useContinuousOptimisation(true);
|
|
842
|
+
const { settings } = optimisation;
|
|
843
|
+
const enabled = settings?.enabled ?? false;
|
|
844
|
+
const paused = settings !== null && !settings.killSwitchOn;
|
|
845
|
+
const [dismissing, setDismissing] = useState(null);
|
|
846
|
+
const apply = async (suggestion) => {
|
|
847
|
+
const target = await optimisation.propose(suggestion.id);
|
|
848
|
+
if (target) onOpenProposal(target);
|
|
849
|
+
};
|
|
850
|
+
return /* @__PURE__ */ jsxs(
|
|
851
|
+
"section",
|
|
852
|
+
{
|
|
853
|
+
className: "mb-6 rounded-xl border border-border-secondary bg-background-secondary p-4",
|
|
854
|
+
"data-testid": "optimisation-panel",
|
|
855
|
+
children: [
|
|
856
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [
|
|
857
|
+
/* @__PURE__ */ jsx(
|
|
858
|
+
PanelHeading,
|
|
859
|
+
{
|
|
860
|
+
enabled,
|
|
861
|
+
paused,
|
|
862
|
+
lastScanAt: settings?.lastScanAt ?? null,
|
|
863
|
+
tr
|
|
864
|
+
}
|
|
865
|
+
),
|
|
866
|
+
/* @__PURE__ */ jsx(
|
|
867
|
+
ManageActions,
|
|
868
|
+
{
|
|
869
|
+
optimisation,
|
|
870
|
+
enabled,
|
|
871
|
+
paused,
|
|
872
|
+
tr
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
] }),
|
|
876
|
+
/* @__PURE__ */ jsx(ReadOnlyNote, { tr }),
|
|
877
|
+
optimisation.error && /* @__PURE__ */ jsx(
|
|
878
|
+
"p",
|
|
879
|
+
{
|
|
880
|
+
className: "mt-3 text-sm text-status-error-text",
|
|
881
|
+
role: "alert",
|
|
882
|
+
"data-testid": "optimisation-error",
|
|
883
|
+
children: optimisation.error
|
|
884
|
+
}
|
|
885
|
+
),
|
|
886
|
+
optimisation.lastScan && /* @__PURE__ */ jsx(ScanSummary, { scan: optimisation.lastScan, tr }),
|
|
887
|
+
optimisation.suggestions.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
888
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-4 space-y-2", "data-testid": "optimisation-list", children: optimisation.suggestions.map((suggestion) => /* @__PURE__ */ jsx(
|
|
889
|
+
OptimisationSuggestionRow,
|
|
890
|
+
{
|
|
891
|
+
suggestion,
|
|
892
|
+
pending: optimisation.pendingId === suggestion.id,
|
|
893
|
+
onOpenRun,
|
|
894
|
+
onApply: () => void apply(suggestion),
|
|
895
|
+
onSnooze: () => void optimisation.snooze(suggestion.id),
|
|
896
|
+
onDismiss: () => setDismissing(suggestion),
|
|
897
|
+
tr
|
|
898
|
+
},
|
|
899
|
+
suggestion.id
|
|
900
|
+
)) }),
|
|
901
|
+
optimisation.total > optimisation.suggestions.length && /* @__PURE__ */ jsx(
|
|
902
|
+
"p",
|
|
903
|
+
{
|
|
904
|
+
className: "mt-2 text-xs text-text-secondary",
|
|
905
|
+
"data-testid": "optimisation-more",
|
|
906
|
+
children: tr(
|
|
907
|
+
"fluidgrids.optimisation.showingTop",
|
|
908
|
+
"Showing the best {{shown}} of {{total}} open suggestions.",
|
|
909
|
+
{
|
|
910
|
+
shown: optimisation.suggestions.length,
|
|
911
|
+
total: optimisation.total
|
|
912
|
+
}
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
] }) : !optimisation.loading && /* @__PURE__ */ jsx(EmptyState, { enabled, paused, tr }),
|
|
917
|
+
/* @__PURE__ */ jsx(
|
|
918
|
+
DismissDialog,
|
|
919
|
+
{
|
|
920
|
+
suggestion: dismissing,
|
|
921
|
+
pending: dismissing !== null && optimisation.pendingId === dismissing.id,
|
|
922
|
+
onClose: () => setDismissing(null),
|
|
923
|
+
onConfirm: async (reason, note) => {
|
|
924
|
+
if (!dismissing) return;
|
|
925
|
+
const done = await optimisation.dismiss(dismissing.id, reason, note);
|
|
926
|
+
if (done) setDismissing(null);
|
|
927
|
+
},
|
|
928
|
+
tr
|
|
929
|
+
},
|
|
930
|
+
dismissing?.id ?? "none"
|
|
931
|
+
)
|
|
932
|
+
]
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
function PanelHeading({
|
|
937
|
+
enabled,
|
|
938
|
+
paused,
|
|
939
|
+
lastScanAt,
|
|
940
|
+
tr
|
|
941
|
+
}) {
|
|
942
|
+
const parts = [
|
|
943
|
+
enabled ? tr("fluidgrids.optimisation.on", "On") : tr("fluidgrids.optimisation.off", "Off")
|
|
944
|
+
];
|
|
945
|
+
if (lastScanAt) {
|
|
946
|
+
parts.push(
|
|
947
|
+
tr("fluidgrids.optimisation.lastScan", "last checked {{when}}", {
|
|
948
|
+
when: new Date(lastScanAt).toLocaleString()
|
|
949
|
+
})
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (paused) {
|
|
953
|
+
parts.push(
|
|
954
|
+
tr("fluidgrids.optimisation.paused", "paused for this workspace")
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
958
|
+
/* @__PURE__ */ jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold text-text-primary", children: [
|
|
959
|
+
/* @__PURE__ */ jsx(Lightbulb, { className: "size-[18px]" }),
|
|
960
|
+
tr("fluidgrids.optimisation.title", "Suggestions")
|
|
961
|
+
] }),
|
|
962
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 max-w-2xl text-sm text-text-secondary", children: tr(
|
|
963
|
+
"fluidgrids.optimisation.description",
|
|
964
|
+
"Every night, reads the last 30 days of production runs and model calls and suggests changes that save money or avoid failures. Applying one opens a proposal on the canvas for you to review; nothing changes on its own."
|
|
965
|
+
) }),
|
|
966
|
+
/* @__PURE__ */ jsx(
|
|
967
|
+
"p",
|
|
968
|
+
{
|
|
969
|
+
className: "mt-1 text-xs text-text-secondary",
|
|
970
|
+
"data-testid": "optimisation-status",
|
|
971
|
+
children: parts.join(" \xB7 ")
|
|
972
|
+
}
|
|
973
|
+
)
|
|
974
|
+
] });
|
|
975
|
+
}
|
|
976
|
+
function ReadOnlyNote({ tr }) {
|
|
977
|
+
const { allowed: canManage } = useFluidGridsPermission(
|
|
978
|
+
FLUIDGRIDS_PERMISSIONS.WORKFLOW_PUBLISH
|
|
979
|
+
);
|
|
980
|
+
const { allowed: canApply } = useFluidGridsPermission(
|
|
981
|
+
FLUIDGRIDS_PERMISSIONS.AI_WORKFLOW_CREATE
|
|
982
|
+
);
|
|
983
|
+
const { allowed: canUpdate, isLoading } = useFluidGridsPermission(
|
|
984
|
+
FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE
|
|
985
|
+
);
|
|
986
|
+
if (isLoading || canManage || canApply || canUpdate) return null;
|
|
987
|
+
return /* @__PURE__ */ jsx(
|
|
988
|
+
"p",
|
|
989
|
+
{
|
|
990
|
+
className: "mt-3 text-xs text-text-secondary",
|
|
991
|
+
"data-testid": "optimisation-read-only",
|
|
992
|
+
children: tr(
|
|
993
|
+
"fluidgrids.optimisation.readOnly",
|
|
994
|
+
"You can view suggestions. Applying, dismissing or changing them needs edit access to workflows."
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
function ScanSummary({
|
|
1000
|
+
scan,
|
|
1001
|
+
tr
|
|
1002
|
+
}) {
|
|
1003
|
+
return /* @__PURE__ */ jsx(
|
|
1004
|
+
"p",
|
|
1005
|
+
{
|
|
1006
|
+
className: "mt-3 text-xs text-text-secondary",
|
|
1007
|
+
"data-testid": "optimisation-last-scan",
|
|
1008
|
+
children: scan.skipped ? tr(
|
|
1009
|
+
"fluidgrids.optimisation.scanSkipped",
|
|
1010
|
+
"The check was skipped: continuous optimisation is off for this workspace."
|
|
1011
|
+
) : tr(
|
|
1012
|
+
"fluidgrids.optimisation.scanSummary",
|
|
1013
|
+
"Checked {{workflows}} workflow(s): {{created}} new, {{resolved}} no longer needed, {{open}} open.",
|
|
1014
|
+
{
|
|
1015
|
+
workflows: scan.workflowsScanned,
|
|
1016
|
+
created: scan.created,
|
|
1017
|
+
resolved: scan.resolved,
|
|
1018
|
+
open: scan.openSuggestions
|
|
1019
|
+
}
|
|
1020
|
+
)
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
function EmptyState({
|
|
1025
|
+
enabled,
|
|
1026
|
+
paused,
|
|
1027
|
+
tr
|
|
1028
|
+
}) {
|
|
1029
|
+
let message = tr(
|
|
1030
|
+
"fluidgrids.optimisation.emptyOff",
|
|
1031
|
+
"Turn continuous optimisation on to get nightly suggestions for your workflows here."
|
|
1032
|
+
);
|
|
1033
|
+
if (paused) {
|
|
1034
|
+
message = tr(
|
|
1035
|
+
"fluidgrids.optimisation.emptyPaused",
|
|
1036
|
+
"Continuous optimisation is paused for this workspace."
|
|
1037
|
+
);
|
|
1038
|
+
} else if (enabled) {
|
|
1039
|
+
message = tr(
|
|
1040
|
+
"fluidgrids.optimisation.empty",
|
|
1041
|
+
"No suggestions right now. New ones appear here the morning after a check finds something."
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
return /* @__PURE__ */ jsx(
|
|
1045
|
+
"p",
|
|
1046
|
+
{
|
|
1047
|
+
className: "mt-4 text-sm text-text-secondary",
|
|
1048
|
+
"data-testid": "optimisation-empty",
|
|
1049
|
+
children: message
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
function ManageActions({
|
|
1054
|
+
optimisation,
|
|
1055
|
+
enabled,
|
|
1056
|
+
paused,
|
|
1057
|
+
tr
|
|
1058
|
+
}) {
|
|
1059
|
+
const denied = tr(
|
|
1060
|
+
"fluidgrids.optimisation.manageDenied",
|
|
1061
|
+
"You need permission to publish workflows to change this"
|
|
1062
|
+
);
|
|
1063
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap gap-2", children: [
|
|
1064
|
+
enabled && !paused && /* @__PURE__ */ jsx(
|
|
1065
|
+
SdkPermissionButton,
|
|
1066
|
+
{
|
|
1067
|
+
permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_PUBLISH,
|
|
1068
|
+
deniedTooltip: denied,
|
|
1069
|
+
children: /* @__PURE__ */ jsx(
|
|
1070
|
+
Button$1,
|
|
1071
|
+
{
|
|
1072
|
+
size: "sm",
|
|
1073
|
+
variant: "outline",
|
|
1074
|
+
onClick: () => void optimisation.runNow(),
|
|
1075
|
+
disabled: optimisation.busy,
|
|
1076
|
+
"data-testid": "optimisation-run-now",
|
|
1077
|
+
children: tr("fluidgrids.optimisation.runNow", "Check now")
|
|
1078
|
+
}
|
|
1079
|
+
)
|
|
1080
|
+
}
|
|
1081
|
+
),
|
|
1082
|
+
/* @__PURE__ */ jsx(
|
|
1083
|
+
SdkPermissionButton,
|
|
1084
|
+
{
|
|
1085
|
+
permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_PUBLISH,
|
|
1086
|
+
deniedTooltip: denied,
|
|
1087
|
+
children: /* @__PURE__ */ jsx(
|
|
1088
|
+
Button$1,
|
|
1089
|
+
{
|
|
1090
|
+
size: "sm",
|
|
1091
|
+
variant: enabled ? "outline" : "default",
|
|
1092
|
+
onClick: () => void optimisation.setEnabled(!enabled),
|
|
1093
|
+
disabled: optimisation.busy || optimisation.loading || optimisation.settings === null || paused && !enabled,
|
|
1094
|
+
"data-testid": "optimisation-toggle",
|
|
1095
|
+
children: enabled ? tr("fluidgrids.optimisation.turnOff", "Turn off") : tr("fluidgrids.optimisation.turnOn", "Turn on")
|
|
1096
|
+
}
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
] });
|
|
1101
|
+
}
|
|
1102
|
+
function OptimisationSuggestionRow({
|
|
1103
|
+
suggestion,
|
|
1104
|
+
pending,
|
|
1105
|
+
onOpenRun,
|
|
1106
|
+
onApply,
|
|
1107
|
+
onSnooze,
|
|
1108
|
+
onDismiss,
|
|
1109
|
+
tr
|
|
1110
|
+
}) {
|
|
1111
|
+
const kind = suggestion.kind;
|
|
1112
|
+
const KindIcon = KIND_ICONS[kind] ?? Lightbulb;
|
|
1113
|
+
const facts = evidenceFacts(kind, suggestion.evidence);
|
|
1114
|
+
const saving = monthlySavingLabel(suggestion.estimatedMonthlySavingMicroUsd);
|
|
1115
|
+
const runs = suggestion.sampleRunIds.slice(0, MAX_RUN_LINKS);
|
|
1116
|
+
const updateDenied = tr(
|
|
1117
|
+
"fluidgrids.optimisation.updateDenied",
|
|
1118
|
+
"You need edit access to workflows to change suggestions"
|
|
1119
|
+
);
|
|
1120
|
+
return /* @__PURE__ */ jsx(
|
|
1121
|
+
"li",
|
|
1122
|
+
{
|
|
1123
|
+
className: "rounded-lg border border-border-secondary bg-background-primary p-3",
|
|
1124
|
+
"data-testid": "optimisation-suggestion",
|
|
1125
|
+
"data-kind": String(kind),
|
|
1126
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between", children: [
|
|
1127
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
1128
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
|
|
1129
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-background-secondary px-2 py-0.5 text-xs text-text-secondary", children: [
|
|
1130
|
+
/* @__PURE__ */ jsx(KindIcon, { className: "size-3.5" }),
|
|
1131
|
+
kindLabel(tr, kind)
|
|
1132
|
+
] }),
|
|
1133
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-text-primary", children: suggestion.workflowName || suggestion.workflowKey }),
|
|
1134
|
+
/* @__PURE__ */ jsx("span", { className: "text-text-secondary", children: tr("fluidgrids.optimisation.atNode", "at {{node}}", {
|
|
1135
|
+
node: suggestion.nodeId
|
|
1136
|
+
}) }),
|
|
1137
|
+
saving && /* @__PURE__ */ jsx(
|
|
1138
|
+
"span",
|
|
1139
|
+
{
|
|
1140
|
+
className: "inline-flex items-center rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 text-xs text-status-success-text",
|
|
1141
|
+
"data-testid": "optimisation-saving",
|
|
1142
|
+
children: tr(
|
|
1143
|
+
"fluidgrids.optimisation.saving",
|
|
1144
|
+
"Saves about {{amount}} a month",
|
|
1145
|
+
{ amount: saving }
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
)
|
|
1149
|
+
] }),
|
|
1150
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 break-words text-sm text-text-primary", children: suggestion.advice }),
|
|
1151
|
+
facts.length > 0 && /* @__PURE__ */ jsx(
|
|
1152
|
+
"dl",
|
|
1153
|
+
{
|
|
1154
|
+
className: "mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs",
|
|
1155
|
+
"data-testid": "optimisation-evidence",
|
|
1156
|
+
children: facts.map((fact) => /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
1157
|
+
/* @__PURE__ */ jsx("dt", { className: "text-text-secondary", children: evidenceLabel(tr, fact.id) }),
|
|
1158
|
+
/* @__PURE__ */ jsx("dd", { className: "font-medium text-text-primary", children: fact.value })
|
|
1159
|
+
] }, fact.id))
|
|
1160
|
+
}
|
|
1161
|
+
),
|
|
1162
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-text-secondary", children: [
|
|
1163
|
+
tr(
|
|
1164
|
+
"fluidgrids.optimisation.window",
|
|
1165
|
+
"Last {{days}} days \xB7 first seen {{when}}",
|
|
1166
|
+
{
|
|
1167
|
+
days: evidenceWindowDays(suggestion.evidence),
|
|
1168
|
+
when: new Date(
|
|
1169
|
+
String(suggestion.firstSeenAt)
|
|
1170
|
+
).toLocaleDateString()
|
|
1171
|
+
}
|
|
1172
|
+
),
|
|
1173
|
+
runs.length > 0 && ` \xB7 ${tr("fluidgrids.optimisation.runs", "runs")} `,
|
|
1174
|
+
runs.map((runId, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1175
|
+
index > 0 && ", ",
|
|
1176
|
+
/* @__PURE__ */ jsx(
|
|
1177
|
+
"button",
|
|
1178
|
+
{
|
|
1179
|
+
type: "button",
|
|
1180
|
+
className: "text-text-link underline-offset-2 hover:underline",
|
|
1181
|
+
onClick: () => onOpenRun(runId),
|
|
1182
|
+
"data-testid": "optimisation-run-link",
|
|
1183
|
+
children: shortRunId(runId)
|
|
1184
|
+
}
|
|
1185
|
+
)
|
|
1186
|
+
] }, runId))
|
|
1187
|
+
] }),
|
|
1188
|
+
!suggestion.applicable && suggestion.applyBlockedReason && /* @__PURE__ */ jsx(
|
|
1189
|
+
"p",
|
|
1190
|
+
{
|
|
1191
|
+
className: "mt-2 text-xs text-status-warning-text",
|
|
1192
|
+
"data-testid": "optimisation-apply-blocked",
|
|
1193
|
+
children: suggestion.applyBlockedReason
|
|
1194
|
+
}
|
|
1195
|
+
)
|
|
1196
|
+
] }),
|
|
1197
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap gap-2", children: [
|
|
1198
|
+
/* @__PURE__ */ jsx(
|
|
1199
|
+
SdkPermissionButton,
|
|
1200
|
+
{
|
|
1201
|
+
permission: FLUIDGRIDS_PERMISSIONS.AI_WORKFLOW_CREATE,
|
|
1202
|
+
deniedTooltip: tr(
|
|
1203
|
+
"fluidgrids.optimisation.applyDenied",
|
|
1204
|
+
"You need permission to create AI workflow proposals to apply suggestions"
|
|
1205
|
+
),
|
|
1206
|
+
children: /* @__PURE__ */ jsx(
|
|
1207
|
+
Button$1,
|
|
1208
|
+
{
|
|
1209
|
+
size: "sm",
|
|
1210
|
+
onClick: onApply,
|
|
1211
|
+
disabled: pending || !suggestion.applicable,
|
|
1212
|
+
title: suggestion.applicable ? void 0 : suggestion.applyBlockedReason ?? void 0,
|
|
1213
|
+
"data-testid": "optimisation-apply",
|
|
1214
|
+
children: tr("fluidgrids.optimisation.apply", "Apply")
|
|
1215
|
+
}
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
),
|
|
1219
|
+
/* @__PURE__ */ jsx(
|
|
1220
|
+
SdkPermissionButton,
|
|
1221
|
+
{
|
|
1222
|
+
permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE,
|
|
1223
|
+
deniedTooltip: updateDenied,
|
|
1224
|
+
children: /* @__PURE__ */ jsx(
|
|
1225
|
+
Button$1,
|
|
1226
|
+
{
|
|
1227
|
+
size: "sm",
|
|
1228
|
+
variant: "outline",
|
|
1229
|
+
onClick: onSnooze,
|
|
1230
|
+
disabled: pending,
|
|
1231
|
+
"data-testid": "optimisation-snooze",
|
|
1232
|
+
children: tr("fluidgrids.optimisation.snooze", "Snooze 30 days")
|
|
1233
|
+
}
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
),
|
|
1237
|
+
/* @__PURE__ */ jsx(
|
|
1238
|
+
SdkPermissionButton,
|
|
1239
|
+
{
|
|
1240
|
+
permission: FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE,
|
|
1241
|
+
deniedTooltip: updateDenied,
|
|
1242
|
+
children: /* @__PURE__ */ jsx(
|
|
1243
|
+
Button$1,
|
|
1244
|
+
{
|
|
1245
|
+
size: "sm",
|
|
1246
|
+
variant: "ghost",
|
|
1247
|
+
onClick: onDismiss,
|
|
1248
|
+
disabled: pending,
|
|
1249
|
+
"data-testid": "optimisation-dismiss",
|
|
1250
|
+
children: tr("fluidgrids.optimisation.dismiss", "Dismiss")
|
|
1251
|
+
}
|
|
1252
|
+
)
|
|
1253
|
+
}
|
|
1254
|
+
)
|
|
1255
|
+
] })
|
|
1256
|
+
] })
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
function DismissDialog({
|
|
1261
|
+
suggestion,
|
|
1262
|
+
pending,
|
|
1263
|
+
onClose,
|
|
1264
|
+
onConfirm,
|
|
1265
|
+
tr
|
|
1266
|
+
}) {
|
|
1267
|
+
const [reason, setReason] = useState(
|
|
1268
|
+
"NOT_APPLICABLE" /* NotApplicable */
|
|
1269
|
+
);
|
|
1270
|
+
const [note, setNote] = useState("");
|
|
1271
|
+
return /* @__PURE__ */ jsx(
|
|
1272
|
+
Dialog,
|
|
1273
|
+
{
|
|
1274
|
+
open: suggestion !== null,
|
|
1275
|
+
onOpenChange: (open) => {
|
|
1276
|
+
if (!open) onClose();
|
|
1277
|
+
},
|
|
1278
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { "data-testid": "optimisation-dismiss-dialog", children: [
|
|
1279
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
1280
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: tr("fluidgrids.optimisation.dismissTitle", "Dismiss suggestion") }),
|
|
1281
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: tr(
|
|
1282
|
+
"fluidgrids.optimisation.dismissDescription",
|
|
1283
|
+
"Tell us why. Suggestions marked not applicable rank lower for this workspace from now on. A dismissed suggestion comes back only if its evidence at least doubles."
|
|
1284
|
+
) })
|
|
1285
|
+
] }),
|
|
1286
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "flex flex-col gap-2", children: [
|
|
1287
|
+
/* @__PURE__ */ jsx("legend", { className: "sr-only", children: tr("fluidgrids.optimisation.dismissReason", "Reason") }),
|
|
1288
|
+
DISMISS_REASONS.map((option) => /* @__PURE__ */ jsxs(
|
|
1289
|
+
"label",
|
|
1290
|
+
{
|
|
1291
|
+
className: "flex cursor-pointer items-center gap-2 text-sm text-text-primary",
|
|
1292
|
+
children: [
|
|
1293
|
+
/* @__PURE__ */ jsx(
|
|
1294
|
+
"input",
|
|
1295
|
+
{
|
|
1296
|
+
type: "radio",
|
|
1297
|
+
name: "optimisation-dismiss-reason",
|
|
1298
|
+
value: option,
|
|
1299
|
+
checked: reason === option,
|
|
1300
|
+
onChange: () => setReason(option),
|
|
1301
|
+
"data-testid": `optimisation-dismiss-reason-${option}`
|
|
1302
|
+
}
|
|
1303
|
+
),
|
|
1304
|
+
dismissReasonLabel(tr, option)
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
option
|
|
1308
|
+
))
|
|
1309
|
+
] }),
|
|
1310
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
1311
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "optimisation-dismiss-note", children: tr("fluidgrids.optimisation.dismissNote", "Note (optional)") }),
|
|
1312
|
+
/* @__PURE__ */ jsx(
|
|
1313
|
+
Textarea,
|
|
1314
|
+
{
|
|
1315
|
+
id: "optimisation-dismiss-note",
|
|
1316
|
+
"data-testid": "optimisation-dismiss-note",
|
|
1317
|
+
rows: 3,
|
|
1318
|
+
maxLength: MAX_DISMISS_NOTE_LENGTH,
|
|
1319
|
+
value: note,
|
|
1320
|
+
onChange: (event) => setNote(event.target.value)
|
|
1321
|
+
}
|
|
1322
|
+
)
|
|
1323
|
+
] }),
|
|
1324
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
1325
|
+
/* @__PURE__ */ jsx(Button$1, { variant: "outline", onClick: onClose, disabled: pending, children: tr("fluidgrids.optimisation.cancel", "Cancel") }),
|
|
1326
|
+
/* @__PURE__ */ jsx(
|
|
1327
|
+
Button$1,
|
|
1328
|
+
{
|
|
1329
|
+
onClick: () => void onConfirm(reason, note),
|
|
1330
|
+
disabled: pending,
|
|
1331
|
+
"data-testid": "optimisation-dismiss-confirm",
|
|
1332
|
+
children: tr("fluidgrids.optimisation.dismissConfirm", "Dismiss")
|
|
1333
|
+
}
|
|
1334
|
+
)
|
|
1335
|
+
] })
|
|
1336
|
+
] })
|
|
1337
|
+
}
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
342
1340
|
function normalizedRate(summary) {
|
|
343
1341
|
const supplied = summary.workflowSuccessRate;
|
|
344
1342
|
if (supplied !== null && supplied !== void 0) {
|
|
@@ -516,6 +1514,17 @@ function InsightsPage({ showErrors = false }) {
|
|
|
516
1514
|
}
|
|
517
1515
|
}
|
|
518
1516
|
),
|
|
1517
|
+
activeTab === "overview" && // Continuous optimisation (BOFF-9969): renders only with the
|
|
1518
|
+
// fluidgrids.continuous-optimisation flag on.
|
|
1519
|
+
/* @__PURE__ */ jsx(
|
|
1520
|
+
OptimisationSuggestionsPanel,
|
|
1521
|
+
{
|
|
1522
|
+
onOpenRun: (runId) => navigate(`../runs/${runId}`),
|
|
1523
|
+
onOpenProposal: (target) => navigate(
|
|
1524
|
+
`../${proposalCanvasPath(target.workflowKey, target.version, target.proposalId)}`
|
|
1525
|
+
)
|
|
1526
|
+
}
|
|
1527
|
+
),
|
|
519
1528
|
loading && !summary ? /* @__PURE__ */ jsx(LoadingState, {}) : activeTab === "overview" && summary ? /* @__PURE__ */ jsx(
|
|
520
1529
|
OverviewContent,
|
|
521
1530
|
{
|