@skyhook-io/radar-app 1.16.1 → 1.16.3
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.json +5 -5
- package/src/App.tsx +51 -9
- package/src/RadarApp.tsx +10 -2
- package/src/api/client.ts +61 -49
- package/src/api/client.updateResource.test.tsx +58 -0
- package/src/api/cnpg.ts +95 -0
- package/src/api/diagnose.ts +26 -0
- package/src/api/httpErrors.ts +12 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/components/ConnectionErrorView.test.tsx +2 -0
- package/src/components/ConnectionErrorView.tsx +2 -1
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +49 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +62 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +311 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +228 -0
- package/src/components/cnpg/CNPGDrawerTrail.tsx +37 -0
- package/src/components/cnpg/CNPGOperator.tsx +234 -0
- package/src/components/cnpg/CNPGOverview.tsx +307 -0
- package/src/components/cnpg/CNPGPooling.tsx +116 -0
- package/src/components/cnpg/CNPGProtection.tsx +320 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +143 -0
- package/src/components/cnpg/CNPGView.tsx +156 -0
- package/src/components/cnpg/paths.ts +13 -0
- package/src/components/cnpg/routes.test.ts +60 -0
- package/src/components/cnpg/routes.ts +112 -0
- package/src/components/cnpg/shared.tsx +290 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +88 -0
- package/src/components/diagnose/AgentControls.tsx +1 -1
- package/src/components/diagnose/AssessmentCard.tsx +11 -1
- package/src/components/diagnose/DiagnoseContext.tsx +35 -22
- package/src/components/diagnose/DiagnoseSurface.tsx +12 -8
- package/src/components/diagnose/Home.tsx +1 -1
- package/src/components/diagnose/InvestigationEvidencePane.tsx +1 -1
- package/src/components/diagnose/InvestigationRefusalAction.test.tsx +156 -0
- package/src/components/diagnose/InvestigationRefusalAction.tsx +14 -0
- package/src/components/diagnose/InvestigationView.tsx +199 -65
- package/src/components/diagnose/investigationSourceFocus.ts +6 -6
- package/src/components/diagnose/investigationState.test.ts +83 -1
- package/src/components/diagnose/investigationState.ts +43 -0
- package/src/components/diagnose/launch.ts +2 -2
- package/src/components/diagnose/useDisclosureReveal.ts +1 -1
- package/src/components/dock/BottomDock.tsx +1 -1
- package/src/components/dock/DockContext.tsx +27 -2
- package/src/components/dock/LocalTerminalContext.test.tsx +210 -0
- package/src/components/dock/LocalTerminalTab.test.tsx +122 -0
- package/src/components/dock/LocalTerminalTab.tsx +81 -6
- package/src/components/helm/ChartBrowser.tsx +6 -7
- package/src/components/helm/HelmCompareRoute.tsx +108 -115
- package/src/components/helm/HelmReleaseDrawer.tsx +26 -44
- package/src/components/helm/InstallWizard.tsx +5 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/logs/LogsViewer.tsx +3 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +3 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/settings/LocalConfigurationDetails.tsx +21 -15
- package/src/components/settings/LocalConnectionSettings.tsx +41 -10
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +57 -11
- package/src/components/timeline/TimelineView.tsx +9 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -0
- package/src/components/whats-new/WhatsNew.test.ts +107 -28
- package/src/components/whats-new/WhatsNew.tsx +146 -86
- package/src/components/whats-new/WhatsNewDialog.test.tsx +132 -10
- package/src/components/whats-new/releaseNotes.ts +156 -25
- package/src/components/workload/WorkloadView.tsx +30 -4
- package/src/context/DiagnoseCustomization.tsx +13 -3
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/index.ts +4 -2
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
|
@@ -28,9 +28,11 @@ import {
|
|
|
28
28
|
createRun,
|
|
29
29
|
recordConsent,
|
|
30
30
|
DiagnoseError,
|
|
31
|
+
investigationRefusal,
|
|
31
32
|
} from "../../api/diagnose";
|
|
32
33
|
import { getApiBase } from "../../api/config";
|
|
33
34
|
import {
|
|
35
|
+
type InvestigationRefusal,
|
|
34
36
|
type RunSummary,
|
|
35
37
|
type AgentInfo,
|
|
36
38
|
type ExecutionProfile,
|
|
@@ -86,6 +88,9 @@ interface DiagnoseCtx {
|
|
|
86
88
|
historyDegraded: boolean; // persistence broke — history won't survive a restart
|
|
87
89
|
needsConsent: boolean; // a start is pending the one-time consent
|
|
88
90
|
startError: string | null;
|
|
91
|
+
// The host's refusal behind startError, when it sent one, so the surface can
|
|
92
|
+
// offer the host's action beside the message.
|
|
93
|
+
startRefusal: InvestigationRefusal | null;
|
|
89
94
|
// Kept apart from startError: the consent card renders this as "why your
|
|
90
95
|
// approval was refused", and a run-start failure landing in the same slot
|
|
91
96
|
// would be read as exactly that — the two paths don't share a lifecycle.
|
|
@@ -426,7 +431,10 @@ function RoutedDiagnoseProvider({
|
|
|
426
431
|
const [runsLoadFailed, setRunsLoadFailed] = useState(false);
|
|
427
432
|
const [historyDegraded, setHistoryDegraded] = useState(false);
|
|
428
433
|
const [pendingTarget, setPendingTarget] = useState<Target | null>(null);
|
|
429
|
-
const [
|
|
434
|
+
const [startFailure, setStartFailure] = useState<{
|
|
435
|
+
message: string;
|
|
436
|
+
refusal: InvestigationRefusal | null;
|
|
437
|
+
} | null>(null);
|
|
430
438
|
const [consentError, setConsentError] = useState<string | null>(null);
|
|
431
439
|
const [width, setWidth] = useState<number>(() => {
|
|
432
440
|
try {
|
|
@@ -694,24 +702,28 @@ function RoutedDiagnoseProvider({
|
|
|
694
702
|
})
|
|
695
703
|
.catch((e) => {
|
|
696
704
|
if (seq !== startSeqRef.current) return;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
705
|
+
setStartFailure({
|
|
706
|
+
message:
|
|
707
|
+
e instanceof DiagnoseError
|
|
708
|
+
? e.message
|
|
709
|
+
: "Couldn't start the investigation.",
|
|
710
|
+
refusal: investigationRefusal(e),
|
|
711
|
+
});
|
|
702
712
|
});
|
|
703
713
|
};
|
|
704
714
|
|
|
705
715
|
const openInvestigation = useCallback(
|
|
706
716
|
(t: Target) => {
|
|
707
|
-
|
|
717
|
+
setStartFailure(null);
|
|
708
718
|
setConsentError(null);
|
|
709
719
|
setOpen(true);
|
|
710
720
|
if (!hosted && !consentSurface) {
|
|
711
721
|
setPendingTarget(null);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
722
|
+
setStartFailure({
|
|
723
|
+
message:
|
|
724
|
+
"Radar can’t run this agent with a verified execution profile.",
|
|
725
|
+
refusal: null,
|
|
726
|
+
});
|
|
715
727
|
setView("investigation");
|
|
716
728
|
return;
|
|
717
729
|
}
|
|
@@ -728,7 +740,7 @@ function RoutedDiagnoseProvider({
|
|
|
728
740
|
const openRun = useCallback(
|
|
729
741
|
(id: string) => {
|
|
730
742
|
unavailableRunIDsRef.current.delete(id);
|
|
731
|
-
|
|
743
|
+
setStartFailure(null);
|
|
732
744
|
setActiveRunId(id);
|
|
733
745
|
setView("investigation");
|
|
734
746
|
setOpen(true);
|
|
@@ -748,7 +760,7 @@ function RoutedDiagnoseProvider({
|
|
|
748
760
|
);
|
|
749
761
|
|
|
750
762
|
// Browser navigation is the source of truth for presentation: a dedicated
|
|
751
|
-
// /investigations route is the full
|
|
763
|
+
// /investigations route is the full investigations page, while ?ai-run keeps the
|
|
752
764
|
// contextual drawer on its underlying page. Fetch exact ids rather than
|
|
753
765
|
// assuming the bounded recent list contains them.
|
|
754
766
|
useEffect(() => {
|
|
@@ -756,7 +768,7 @@ function RoutedDiagnoseProvider({
|
|
|
756
768
|
return;
|
|
757
769
|
const workspace = isInvestigationWorkspacePath(location.pathname);
|
|
758
770
|
// Eligibility is unresolved until the agent probe returns. Once it has
|
|
759
|
-
// definitively resolved to off,
|
|
771
|
+
// definitively resolved to off, investigations-page routes cannot render anything
|
|
760
772
|
// useful; return to the app instead of leaving an eternal loading panel.
|
|
761
773
|
if (agentEligibilityResolved && !eligible) {
|
|
762
774
|
setActiveRunId(null);
|
|
@@ -823,7 +835,7 @@ function RoutedDiagnoseProvider({
|
|
|
823
835
|
|
|
824
836
|
urlRunIdRef.current = id;
|
|
825
837
|
unavailableRunIDsRef.current.delete(id);
|
|
826
|
-
|
|
838
|
+
setStartFailure(null);
|
|
827
839
|
setActiveRunId(id);
|
|
828
840
|
setView("investigation");
|
|
829
841
|
setOpen(true);
|
|
@@ -856,14 +868,14 @@ function RoutedDiagnoseProvider({
|
|
|
856
868
|
unavailableRunIDsRef.current.clear();
|
|
857
869
|
setActiveRunId(null);
|
|
858
870
|
setView("home");
|
|
859
|
-
|
|
871
|
+
setStartFailure(null);
|
|
860
872
|
setOpen(true);
|
|
861
873
|
writeFocusedRunID(null, false);
|
|
862
874
|
}, [writeFocusedRunID]);
|
|
863
875
|
const openWorkspace = useCallback(
|
|
864
876
|
(preferredRunID?: string | null) => {
|
|
865
877
|
setOpen(true);
|
|
866
|
-
|
|
878
|
+
setStartFailure(null);
|
|
867
879
|
if (!forceRouterURLState && !diagnoseURLStateEnabled(browserURLState)) {
|
|
868
880
|
if (preferredRunID) {
|
|
869
881
|
setActiveRunId(preferredRunID);
|
|
@@ -876,8 +888,8 @@ function RoutedDiagnoseProvider({
|
|
|
876
888
|
return;
|
|
877
889
|
}
|
|
878
890
|
const current = locationRef.current;
|
|
879
|
-
// The global entry remains visible
|
|
880
|
-
// there as an idempotent reveal rather than pushing another
|
|
891
|
+
// The global entry remains visible on the investigations page. Treat clicking it
|
|
892
|
+
// there as an idempotent reveal rather than pushing another page
|
|
881
893
|
// entry and replacing the page that Close should return to.
|
|
882
894
|
if (isInvestigationWorkspacePath(current.pathname)) {
|
|
883
895
|
setMaximized(true);
|
|
@@ -927,14 +939,14 @@ function RoutedDiagnoseProvider({
|
|
|
927
939
|
unavailableRunIDsRef.current.clear();
|
|
928
940
|
setActiveRunId(null);
|
|
929
941
|
setView("home");
|
|
930
|
-
|
|
942
|
+
setStartFailure(null);
|
|
931
943
|
writeFocusedRunID(null, false);
|
|
932
944
|
}, [writeFocusedRunID]);
|
|
933
945
|
const dismissForNavigation = useCallback(() => {
|
|
934
946
|
unavailableRunIDsRef.current.clear();
|
|
935
947
|
setOpen(false);
|
|
936
948
|
setView("home");
|
|
937
|
-
|
|
949
|
+
setStartFailure(null);
|
|
938
950
|
}, []);
|
|
939
951
|
const close = useCallback(() => {
|
|
940
952
|
unavailableRunIDsRef.current.clear();
|
|
@@ -999,7 +1011,7 @@ function RoutedDiagnoseProvider({
|
|
|
999
1011
|
setOpen(true);
|
|
1000
1012
|
if (!activeRunIdRef.current) setView("home");
|
|
1001
1013
|
}, []);
|
|
1002
|
-
const dismissError = useCallback(() =>
|
|
1014
|
+
const dismissError = useCallback(() => setStartFailure(null), []);
|
|
1003
1015
|
|
|
1004
1016
|
// Reserve a right gutter on the CONTENT area (not the navbar/rail — those stay
|
|
1005
1017
|
// global and static) so docked content reflows beside the panel. Wide viewports
|
|
@@ -1029,7 +1041,8 @@ function RoutedDiagnoseProvider({
|
|
|
1029
1041
|
// pendingTarget is set ONLY when the current agent's consent is missing, and
|
|
1030
1042
|
// cleared on approve/cancel — so its presence is exactly "consent needed now".
|
|
1031
1043
|
needsConsent: !!pendingTarget,
|
|
1032
|
-
startError,
|
|
1044
|
+
startError: startFailure?.message ?? null,
|
|
1045
|
+
startRefusal: startFailure?.refusal ?? null,
|
|
1033
1046
|
consentError,
|
|
1034
1047
|
openInvestigation,
|
|
1035
1048
|
openRun,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The right-docked shell of the AI surface. Two layouts:
|
|
2
2
|
// - docked: a single-pane right column (app reflows left via the provider's push)
|
|
3
|
-
// - expanded: a master-detail
|
|
3
|
+
// - expanded: the investigations page, a master-detail layout that fills ONLY the content area (does
|
|
4
4
|
// not cover the left nav rail or top bar) — recent list on the left, the
|
|
5
5
|
// selected investigation/report on the right.
|
|
6
6
|
import {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type DiagnoseView,
|
|
39
39
|
} from "./DiagnoseContext";
|
|
40
40
|
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
41
|
+
import { InvestigationRefusalAction } from "./InvestigationRefusalAction";
|
|
41
42
|
import { useNavCustomization } from "../../context/NavCustomization";
|
|
42
43
|
import { InvestigationView } from "./InvestigationView";
|
|
43
44
|
import type { InvestigationTimelineScope } from "./InvestigationEvidencePane";
|
|
@@ -419,7 +420,7 @@ export function DiagnoseSurface({
|
|
|
419
420
|
panelWidthKey: widthKey,
|
|
420
421
|
} = useDiagnoseLayout();
|
|
421
422
|
// Home has no detail pane competing for width, so its history is always part
|
|
422
|
-
// of the
|
|
423
|
+
// of the page. Only a focused run collapses history into a drawer when
|
|
423
424
|
// the two-pane layout would become cramped.
|
|
424
425
|
const persistentHistory = investigationHistoryIsPersistent({
|
|
425
426
|
maximized,
|
|
@@ -604,12 +605,15 @@ export function DiagnoseSurface({
|
|
|
604
605
|
) : d.startError ? (
|
|
605
606
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
606
607
|
<p className="text-sm text-theme-text-secondary">{d.startError}</p>
|
|
607
|
-
<
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
608
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
609
|
+
<InvestigationRefusalAction refusal={d.startRefusal} />
|
|
610
|
+
<button
|
|
611
|
+
onClick={d.dismissError}
|
|
612
|
+
className="rounded-lg border border-theme-border px-3 py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
|
|
613
|
+
>
|
|
614
|
+
Dismiss
|
|
615
|
+
</button>
|
|
616
|
+
</div>
|
|
613
617
|
</div>
|
|
614
618
|
) : setupPending ? (
|
|
615
619
|
<div className="flex-1 overflow-y-auto">
|
|
@@ -204,7 +204,7 @@ export function InvestigationEvidencePane({
|
|
|
204
204
|
: undefined;
|
|
205
205
|
|
|
206
206
|
// A source link is a navigation request, not a disclosure preference. Open
|
|
207
|
-
// whichever collection owns the source first, then tell the
|
|
207
|
+
// whichever collection owns the source first, then tell the investigation view that
|
|
208
208
|
// its double-rAF focus/scroll can safely run outside an inert subtree.
|
|
209
209
|
useLayoutEffect(() => {
|
|
210
210
|
if (
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import { DiagnoseError, investigationRefusal } from "../../api/diagnose";
|
|
4
|
+
import { DiagnoseCustomizationProvider } from "../../context/DiagnoseCustomization";
|
|
5
|
+
import {
|
|
6
|
+
InvestigationFailureNotice,
|
|
7
|
+
InvestigationStartErrorAlert,
|
|
8
|
+
InvestigationStatusCheckNotice,
|
|
9
|
+
} from "./InvestigationView";
|
|
10
|
+
|
|
11
|
+
const quota = new DiagnoseError(
|
|
12
|
+
402,
|
|
13
|
+
"Your organization has used this month's investigations.",
|
|
14
|
+
{
|
|
15
|
+
code: "ai_quota_exhausted",
|
|
16
|
+
reason: "allowance_used_free",
|
|
17
|
+
action: "upgrade",
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("investigationRefusal", () => {
|
|
22
|
+
it("carries a host refusal's code, reason and action with its sentence", () => {
|
|
23
|
+
expect(investigationRefusal(quota)).toEqual({
|
|
24
|
+
status: 402,
|
|
25
|
+
message: "Your organization has used this month's investigations.",
|
|
26
|
+
code: "ai_quota_exhausted",
|
|
27
|
+
reason: "allowance_used_free",
|
|
28
|
+
action: "upgrade",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("is null for Radar's own errors and for anything that isn't a DiagnoseError", () => {
|
|
33
|
+
expect(
|
|
34
|
+
investigationRefusal(
|
|
35
|
+
new DiagnoseError(409, "Too many investigations running."),
|
|
36
|
+
),
|
|
37
|
+
).toBeNull();
|
|
38
|
+
expect(investigationRefusal(new Error("network down"))).toBeNull();
|
|
39
|
+
expect(investigationRefusal(undefined)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("InvestigationStartErrorAlert", () => {
|
|
44
|
+
const alert = (refusal = investigationRefusal(quota)) => (
|
|
45
|
+
<InvestigationStartErrorAlert
|
|
46
|
+
error={quota.message}
|
|
47
|
+
refusal={refusal}
|
|
48
|
+
onDismiss={() => {}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it("shows the host's action beside the refusal", () => {
|
|
53
|
+
const html = renderToString(
|
|
54
|
+
<DiagnoseCustomizationProvider
|
|
55
|
+
value={undefined}
|
|
56
|
+
renderRefusalAction={(r) => (
|
|
57
|
+
<a href="/plans">{`Upgrade (${r.reason})`}</a>
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{alert()}
|
|
61
|
+
</DiagnoseCustomizationProvider>,
|
|
62
|
+
);
|
|
63
|
+
expect(html).toContain("this month's investigations");
|
|
64
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("shows only the sentence when the host offers no action", () => {
|
|
68
|
+
const html = renderToString(alert());
|
|
69
|
+
expect(html).toContain("this month's investigations");
|
|
70
|
+
expect(html).not.toContain("Upgrade");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("asks the host for nothing when the error carries no refusal", () => {
|
|
74
|
+
let asked = false;
|
|
75
|
+
renderToString(
|
|
76
|
+
<DiagnoseCustomizationProvider
|
|
77
|
+
value={undefined}
|
|
78
|
+
renderRefusalAction={() => {
|
|
79
|
+
asked = true;
|
|
80
|
+
return null;
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{alert(null)}
|
|
84
|
+
</DiagnoseCustomizationProvider>,
|
|
85
|
+
);
|
|
86
|
+
expect(asked).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const withHost = (node: React.ReactNode) =>
|
|
91
|
+
renderToString(
|
|
92
|
+
<DiagnoseCustomizationProvider
|
|
93
|
+
value={undefined}
|
|
94
|
+
renderRefusalAction={(r) => (
|
|
95
|
+
<a href="/plans">{`Upgrade (${r.reason})`}</a>
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{node}
|
|
99
|
+
</DiagnoseCustomizationProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
describe("InvestigationStatusCheckNotice (Findings)", () => {
|
|
103
|
+
it("says a refused status check couldn't run, with the host's action", () => {
|
|
104
|
+
const html = withHost(
|
|
105
|
+
<InvestigationStatusCheckNotice
|
|
106
|
+
message={quota.message}
|
|
107
|
+
uncertainOnly={false}
|
|
108
|
+
refusal={investigationRefusal(quota)}
|
|
109
|
+
onCheck={() => {}}
|
|
110
|
+
disabled={false}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
expect(html).toContain("Couldn't check status:");
|
|
114
|
+
expect(html).not.toContain("Verification did not complete");
|
|
115
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps the verification wording, and no action, for a check that ran and failed", () => {
|
|
119
|
+
const html = withHost(
|
|
120
|
+
<InvestigationStatusCheckNotice
|
|
121
|
+
message="The agent stopped before checking."
|
|
122
|
+
uncertainOnly={false}
|
|
123
|
+
refusal={null}
|
|
124
|
+
onCheck={() => {}}
|
|
125
|
+
disabled={false}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(html).toContain("Verification did not complete:");
|
|
129
|
+
expect(html).not.toContain("Upgrade");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("InvestigationFailureNotice (Activity)", () => {
|
|
134
|
+
it("keeps a status warning first and shows a newer refused follow-up, with its action, beside it", () => {
|
|
135
|
+
const html = withHost(
|
|
136
|
+
<InvestigationFailureNotice
|
|
137
|
+
message="Radar couldn't confirm whether the apply request completed."
|
|
138
|
+
refusal={null}
|
|
139
|
+
statusCheck={{
|
|
140
|
+
label: "Check current status",
|
|
141
|
+
onCheck: () => {},
|
|
142
|
+
disabled: false,
|
|
143
|
+
}}
|
|
144
|
+
followUp={{
|
|
145
|
+
message: quota.message,
|
|
146
|
+
refusal: investigationRefusal(quota),
|
|
147
|
+
}}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
expect(
|
|
151
|
+
html.indexOf("couldn't confirm whether the apply"),
|
|
152
|
+
).toBeLessThan(html.indexOf("this month's investigations"));
|
|
153
|
+
expect(html).toContain("Check current status");
|
|
154
|
+
expect(html).toContain("Upgrade (allowance_used_free)");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InvestigationRefusal } from "../../api/diagnose";
|
|
2
|
+
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
3
|
+
|
|
4
|
+
/** The host's action for a refused start or follow-up, or nothing when the
|
|
5
|
+
* refusal carries no code, reason or action, or the host offers none. */
|
|
6
|
+
export function InvestigationRefusalAction({
|
|
7
|
+
refusal,
|
|
8
|
+
}: {
|
|
9
|
+
refusal: InvestigationRefusal | null;
|
|
10
|
+
}) {
|
|
11
|
+
const { renderRefusalAction } = useDiagnoseCustomization();
|
|
12
|
+
if (!refusal || !renderRefusalAction) return null;
|
|
13
|
+
return <>{renderRefusalAction(refusal)}</>;
|
|
14
|
+
}
|