@skyhook-io/radar-app 1.10.0 → 1.11.0
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 +6 -6
- package/src/App.tsx +2 -6
- package/src/api/client.ts +138 -6
- package/src/api/policy.test.ts +38 -0
- package/src/api/policy.ts +166 -2
- package/src/components/home/HomeView.tsx +15 -3
- package/src/components/home/NetworkPolicyCoverageCard.test.tsx +81 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +50 -7
- package/src/components/home/TopologyPreview.tsx +57 -11
- package/src/components/home/mcpToolCatalog.ts +15 -5
- package/src/components/resources/ResourcesView.tsx +15 -3
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +116 -1
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +227 -0
- package/src/components/resources/renderers/CNPGImageCatalogRenderer.tsx +123 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +152 -0
- package/src/components/resources/renderers/KyvernoPolicyCoverage.tsx +65 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.render.test.tsx +59 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.test.ts +99 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +184 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +24 -1
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +44 -1
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +75 -1
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +35 -1
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +37 -20
- package/src/components/traffic/TrafficFlowList.tsx +16 -2
- package/src/components/traffic/TrafficGraph.tsx +150 -58
- package/src/components/traffic/TrafficView.tsx +168 -52
- package/src/components/traffic/TrafficWizard.tsx +13 -1
- package/src/components/traffic/trafficFilters.test.ts +103 -0
- package/src/components/traffic/trafficFilters.ts +117 -0
- package/src/components/ui/DiagnosticsOverlay.test.ts +75 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +47 -2
- package/src/components/workload/WorkloadView.tsx +55 -7
- package/src/utils/navigation.ts +44 -2
- package/src/utils/network-policy-navigation.test.ts +68 -0
- package/src/utils/topology-selection.test.ts +40 -0
- package/src/utils/topology-selection.ts +39 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Workflow } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { LookupFailureNote } from '@skyhook-io/k8s-ui/components/resources/renderers/LookupFailureNote'
|
|
5
|
+
import { HEALTH_BADGE_COLORS } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
6
|
+
import { formatAge } from '@skyhook-io/k8s-ui/components/resources/resource-utils'
|
|
7
|
+
import { isForbiddenError } from '../../../api/client'
|
|
8
|
+
import { isKyvernoMutateExistingEnabled } from '@skyhook-io/k8s-ui/components/resources/resource-utils-kyverno-modern'
|
|
9
|
+
import { usePolicyQueued } from '../../../api/policy'
|
|
10
|
+
|
|
11
|
+
/** A request sitting this long is not mid-flight. Kyverno retries on every
|
|
12
|
+
* reconcile, so past this point a backlog grows rather than drains. */
|
|
13
|
+
const STALLED_MINUTES = 5
|
|
14
|
+
|
|
15
|
+
/** Enough at once to be worth raising even while it is still moving. */
|
|
16
|
+
const BACKLOG_SIZE = 25
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The whole banner, or null when a queue is doing what a queue does. Headline
|
|
20
|
+
* and body are decided together — split across two places, the body went on
|
|
21
|
+
* diagnosing a stalled controller under a headline that had stopped claiming one.
|
|
22
|
+
*
|
|
23
|
+
* Only the first case has measured that the work stopped. The second says how
|
|
24
|
+
* much is waiting and nothing more: everything in it is younger than the stall
|
|
25
|
+
* threshold, so it is as likely to be a burst draining normally, and diagnosing
|
|
26
|
+
* a controller from that is a claim the evidence argues against.
|
|
27
|
+
*/
|
|
28
|
+
export function queueBanner(
|
|
29
|
+
pending: number,
|
|
30
|
+
stalledMinutes: number,
|
|
31
|
+
oldestAge: string,
|
|
32
|
+
): { title: string; message: string } | null {
|
|
33
|
+
if (stalledMinutes >= STALLED_MINUTES) {
|
|
34
|
+
return {
|
|
35
|
+
title: `Queued work has not moved for ${oldestAge}`,
|
|
36
|
+
message:
|
|
37
|
+
'Requests build up when the background controller cannot keep up or cannot reach what it needs. They are retried on every reconcile, so a backlog grows rather than drains.',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (pending >= BACKLOG_SIZE) {
|
|
41
|
+
return {
|
|
42
|
+
title: `${pending} requests are queued`,
|
|
43
|
+
message: `Nothing here has been waiting longer than ${STALLED_MINUTES} minutes, so this may be a burst still draining. Worth watching: if the count holds or the oldest keeps ageing, the controller is not keeping up.`,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether this policy can queue background work at all.
|
|
51
|
+
*
|
|
52
|
+
* Decides whether a denial is worth mentioning. A validate-only policy never
|
|
53
|
+
* queues anything, so "you can't see the queue" there is noise on the majority
|
|
54
|
+
* of policy pages. A policy that generates, or mutates resources that already
|
|
55
|
+
* exist, does queue — and for those a denial hides a backlog that looks exactly
|
|
56
|
+
* like a healthy policy with nothing pending.
|
|
57
|
+
*/
|
|
58
|
+
export function policyQueuesWork(data: any): boolean {
|
|
59
|
+
if (data?.kind === 'GeneratingPolicy') return true
|
|
60
|
+
if (isKyvernoMutateExistingEnabled(data)) return true
|
|
61
|
+
return (data?.spec?.rules ?? []).some(
|
|
62
|
+
(r: any) => !!r?.generate || Array.isArray(r?.mutate?.targets),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a queued request belongs to this policy.
|
|
68
|
+
*
|
|
69
|
+
* Exactly one form matches, never both. A namespaced Policy is recorded as
|
|
70
|
+
* `namespace/name` and a cluster-scoped one bare, and Kyverno permits the two
|
|
71
|
+
* to share a name — so accepting the bare form as a fallback hands a namespaced
|
|
72
|
+
* policy the backlog of a ClusterPolicy it has nothing to do with. The coverage
|
|
73
|
+
* lookup refuses the same fallback for the same reason.
|
|
74
|
+
*/
|
|
75
|
+
export function requestBelongsTo(request: any, name: string, namespace: string): boolean {
|
|
76
|
+
const policy = request?.spec?.policy
|
|
77
|
+
if (!policy || !name) return false
|
|
78
|
+
return policy === (namespace ? `${namespace}/${name}` : name)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What this policy has queued and not finished.
|
|
83
|
+
*
|
|
84
|
+
* A generate or mutate-existing rule does its work through queued requests, and
|
|
85
|
+
* the way that fails in practice is a pile-up rather than one bad request:
|
|
86
|
+
* upstream reports describe thousands stuck in Pending, never cleaned up, taken
|
|
87
|
+
* up again on every reconcile. That is a count, and a count belongs next to the
|
|
88
|
+
* policy — the requests have their own page, but nobody watches it.
|
|
89
|
+
*
|
|
90
|
+
* Silent when the policy has queued nothing, so it never adds an empty section
|
|
91
|
+
* to the majority of policies that only validate.
|
|
92
|
+
*/
|
|
93
|
+
export function KyvernoPolicyQueued({ data }: { data: any }) {
|
|
94
|
+
const name = data?.metadata?.name ?? ''
|
|
95
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
96
|
+
|
|
97
|
+
// Server-side, because the answer's scope is not the subject's: Kyverno keeps
|
|
98
|
+
// these in its own namespace, and the generic resource list would have
|
|
99
|
+
// answered for whatever namespaces the reader happens to be viewing.
|
|
100
|
+
const { data: queued, error } = usePolicyQueued(name, namespace, !!name)
|
|
101
|
+
|
|
102
|
+
const total = queued?.requests ?? 0
|
|
103
|
+
if (total === 0) {
|
|
104
|
+
// An empty list and an unreadable one are not the same answer. A denial is
|
|
105
|
+
// cluster-static, so disclosing it on every policy page would be noise on the
|
|
106
|
+
// majority that only validate and never queue anything — but staying silent
|
|
107
|
+
// about it on a policy that DOES queue hides a backlog behind a page that
|
|
108
|
+
// looks like a healthy one with nothing pending. So it is disclosed exactly
|
|
109
|
+
// where it can cost something.
|
|
110
|
+
if (!error) return null
|
|
111
|
+
if (isForbiddenError(error) && !policyQueuesWork(data)) return null
|
|
112
|
+
return (
|
|
113
|
+
<Section title="Queued Work" icon={Workflow}>
|
|
114
|
+
<LookupFailureNote errors={[error]} what="what this policy has queued" />
|
|
115
|
+
</Section>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const byState = queued?.byState ?? {}
|
|
120
|
+
const pending = byState['Pending'] ?? 0
|
|
121
|
+
const failed = byState['Failed'] ?? 0
|
|
122
|
+
const messages = queued?.messages ?? []
|
|
123
|
+
const oldestPending = queued?.oldestPending
|
|
124
|
+
|
|
125
|
+
const stalledMinutes = oldestPending
|
|
126
|
+
? Math.floor((Date.now() - new Date(oldestPending).getTime()) / 60000)
|
|
127
|
+
: 0
|
|
128
|
+
const banner = queueBanner(pending, stalledMinutes, oldestPending ? formatAge(oldestPending) : '')
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
{banner && (
|
|
133
|
+
<AlertBanner variant="warning" title={banner.title} message={banner.message} />
|
|
134
|
+
)}
|
|
135
|
+
<Section title="Queued Work" icon={Workflow} defaultExpanded={pending > 0 || failed > 0}>
|
|
136
|
+
<PropertyList>
|
|
137
|
+
<Property label="Requests" value={String(total)} />
|
|
138
|
+
{Object.entries(byState).map(([state, n]) => (
|
|
139
|
+
<Property
|
|
140
|
+
key={state}
|
|
141
|
+
label={state}
|
|
142
|
+
value={
|
|
143
|
+
<span
|
|
144
|
+
className={clsx(
|
|
145
|
+
'badge',
|
|
146
|
+
HEALTH_BADGE_COLORS[
|
|
147
|
+
(state === 'Failed'
|
|
148
|
+
? 'unhealthy'
|
|
149
|
+
: state === 'Pending'
|
|
150
|
+
? 'degraded'
|
|
151
|
+
: state === 'Completed'
|
|
152
|
+
? 'healthy'
|
|
153
|
+
: 'unknown') as keyof typeof HEALTH_BADGE_COLORS
|
|
154
|
+
],
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{n}
|
|
158
|
+
</span>
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
{oldestPending && (
|
|
163
|
+
<Property label="Oldest Pending" value={`${formatAge(oldestPending)} ago`} />
|
|
164
|
+
)}
|
|
165
|
+
</PropertyList>
|
|
166
|
+
{messages.length > 0 && (
|
|
167
|
+
<div className="mt-2 pt-2 border-t border-theme-border space-y-1">
|
|
168
|
+
{/* Kyverno writes why it could not complete a request into
|
|
169
|
+
status.message, and it is the only diagnosis this object
|
|
170
|
+
carries. A count without it says something is wrong and leaves
|
|
171
|
+
you to go and find out what. */}
|
|
172
|
+
{messages.map((m, i) => (
|
|
173
|
+
<div key={i} className="text-xs text-warning-text">{m}</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
|
|
178
|
+
These are deleted seconds after they complete, so this counts what is in flight right now
|
|
179
|
+
rather than everything this policy has ever done.
|
|
180
|
+
</div>
|
|
181
|
+
</Section>
|
|
182
|
+
</>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
import { RolloutRenderer as BaseRolloutRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RolloutRenderer'
|
|
2
|
+
import { useRolloutAction, useRolloutCapabilities, type RolloutAction } from '../../../api/client'
|
|
3
|
+
|
|
4
|
+
interface RolloutRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
|
|
10
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
11
|
+
const name = data?.metadata?.name ?? ''
|
|
12
|
+
const { data: capabilities } = useRolloutCapabilities(namespace, name)
|
|
13
|
+
const action = useRolloutAction()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<BaseRolloutRenderer
|
|
17
|
+
data={data}
|
|
18
|
+
onNavigate={onNavigate}
|
|
19
|
+
capabilities={capabilities}
|
|
20
|
+
onAction={(next: RolloutAction) => action.mutate({ action: next, namespace, name })}
|
|
21
|
+
pendingAction={action.isPending ? action.variables?.action ?? null : null}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -1 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
import { VeleroBSLRenderer as BaseVeleroBSLRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/VeleroBSLRenderer'
|
|
2
|
+
import { LookupFailureNote } from '@skyhook-io/k8s-ui/components/resources/renderers/LookupFailureNote'
|
|
3
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { useVeleroStoredBackups } from '../../../api/policy'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Host wrapper adding the reverse lookup the package renderer cannot do: which
|
|
8
|
+
* Backups this storage location holds.
|
|
9
|
+
*
|
|
10
|
+
* Without it the page states a phase and stops. "Unavailable" is a fact about a
|
|
11
|
+
* bucket; what an operator came to find out is what it costs them, and that is
|
|
12
|
+
* the list of backups they cannot restore from until it recovers — every one of
|
|
13
|
+
* which Velero still reports as Completed.
|
|
14
|
+
*/
|
|
15
|
+
export function VeleroBSLRenderer({
|
|
16
|
+
data,
|
|
17
|
+
onNavigate,
|
|
18
|
+
}: {
|
|
19
|
+
data: any
|
|
20
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
21
|
+
}) {
|
|
22
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
23
|
+
const name = data?.metadata?.name ?? ''
|
|
24
|
+
const stored = useVeleroStoredBackups(namespace, name, !!namespace && !!name)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<BaseVeleroBSLRenderer
|
|
28
|
+
data={data}
|
|
29
|
+
// Undefined while unresolved: an empty list would say this location holds
|
|
30
|
+
// nothing, which is a different answer from not having looked yet.
|
|
31
|
+
storedBackups={stored.isLoading || stored.error ? undefined : (stored.data?.backups ?? [])}
|
|
32
|
+
storedTotal={stored.data?.stored}
|
|
33
|
+
restorableTotal={stored.data?.restorable}
|
|
34
|
+
expiredTotal={stored.data?.expired}
|
|
35
|
+
listTruncated={stored.data?.truncated}
|
|
36
|
+
lookupNote={
|
|
37
|
+
stored.error ? (
|
|
38
|
+
<LookupFailureNote errors={[stored.error]} what="which backups are stored here" />
|
|
39
|
+
) : undefined
|
|
40
|
+
}
|
|
41
|
+
onNavigate={onNavigate}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -1 +1,75 @@
|
|
|
1
|
-
|
|
1
|
+
import { LookupFailureNote } from '@skyhook-io/k8s-ui/components/resources/renderers/LookupFailureNote'
|
|
2
|
+
import { VeleroBackupRenderer as BaseVeleroBackupRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/VeleroBackupRenderer'
|
|
3
|
+
import { resolveBackupStorageLocation } from '@skyhook-io/k8s-ui/components/resources/resource-utils-velero'
|
|
4
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { useResources } from '../../../api/client'
|
|
6
|
+
import { useVeleroRunMessages } from '../../../api/policy'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Host wrapper adding the two things a Backup cannot answer from its own status:
|
|
10
|
+
* whether the storage location holding it is reachable, and what the errors and
|
|
11
|
+
* warnings it counted actually say.
|
|
12
|
+
*
|
|
13
|
+
* The backup's own status says Completed and goes on saying it after the bucket
|
|
14
|
+
* behind it goes Unavailable. This is the page someone opens to decide whether
|
|
15
|
+
* they can restore to this point, so the answer belongs here and not one screen
|
|
16
|
+
* away.
|
|
17
|
+
*
|
|
18
|
+
* Namespace is explicit — storage locations live alongside the backups that name
|
|
19
|
+
* them, in Velero's own namespace. Omitting it would inherit the reader's
|
|
20
|
+
* namespace view filter, which is a browsing preference and not the scope of
|
|
21
|
+
* this question.
|
|
22
|
+
*/
|
|
23
|
+
export function VeleroBackupRenderer({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
24
|
+
const messages = useRunMessages('backups', data)
|
|
25
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
26
|
+
|
|
27
|
+
const locations = useResources<any>('backupstoragelocations', namespace, 'velero.io', {
|
|
28
|
+
enabled: !!namespace,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Everything below is gated on the lookup having answered. A location we have
|
|
32
|
+
// not read is not a healthy one, and neither is a name we have not resolved:
|
|
33
|
+
// an unset spec.storageLocation means whichever location carries spec.default,
|
|
34
|
+
// so resolving it against a list that has not loaded produces the literal
|
|
35
|
+
// "default", which is wrong on any install that renamed it — and wrong
|
|
36
|
+
// permanently when the list errors rather than for a moment.
|
|
37
|
+
const answered = !locations.isLoading && !locations.error && locations.data !== undefined
|
|
38
|
+
const resolved = answered ? resolveBackupStorageLocation(data, locations.data) : undefined
|
|
39
|
+
|
|
40
|
+
const match = answered
|
|
41
|
+
? (locations.data ?? []).find((l: any) => l?.metadata?.name === resolved)
|
|
42
|
+
: undefined
|
|
43
|
+
const phase = match?.status?.phase
|
|
44
|
+
|
|
45
|
+
// The location the backup names is gone. Velero restores from the location
|
|
46
|
+
// recorded on the backup, so this is not restorable — and it is invisible
|
|
47
|
+
// otherwise, because a location that does not exist has no phase to report.
|
|
48
|
+
const missing = answered && !!resolved && match === undefined
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<BaseVeleroBackupRenderer
|
|
52
|
+
data={data}
|
|
53
|
+
storageLocationPhase={phase}
|
|
54
|
+
storageLocationName={resolved}
|
|
55
|
+
storageLocationMissing={missing}
|
|
56
|
+
messages={messages}
|
|
57
|
+
onNavigate={onNavigate}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The messages behind the counts. Nothing is fetched until the operator asks:
|
|
63
|
+
// reading them makes Velero create a DownloadRequest and pulls an object out of
|
|
64
|
+
// storage, which is not work to do on every drawer open.
|
|
65
|
+
function useRunMessages(kind: 'backups' | 'restores', data: any) {
|
|
66
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
67
|
+
const name = data?.metadata?.name ?? ''
|
|
68
|
+
const fetcher = useVeleroRunMessages(kind, namespace, name)
|
|
69
|
+
return {
|
|
70
|
+
messages: fetcher.data,
|
|
71
|
+
loading: fetcher.isPending,
|
|
72
|
+
lookupNote: fetcher.error ? <LookupFailureNote errors={[fetcher.error]} what="the messages behind these counts" /> : undefined,
|
|
73
|
+
onFetch: namespace && name ? () => fetcher.mutate() : undefined,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
import { LookupFailureNote } from '@skyhook-io/k8s-ui/components/resources/renderers/LookupFailureNote'
|
|
2
|
+
import { VeleroRestoreRenderer as BaseVeleroRestoreRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/VeleroRestoreRenderer'
|
|
3
|
+
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { useVeleroRunMessages } from '../../../api/policy'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Host wrapper adding the messages behind the run's error and warning counts.
|
|
8
|
+
*
|
|
9
|
+
* The counts are on the Restore; the text is not. It lives in a results file in
|
|
10
|
+
* object storage that only Velero's controller can hand out a link to — so the
|
|
11
|
+
* page showed a number an operator could worry about but not act on. A stuck
|
|
12
|
+
* restore is the more urgent of the two kinds, because someone is waiting on
|
|
13
|
+
* their data.
|
|
14
|
+
*
|
|
15
|
+
* Fetched on click, not on open: it creates a DownloadRequest and pulls an
|
|
16
|
+
* object out of storage. Same bargain as the network trace's probes.
|
|
17
|
+
*/
|
|
18
|
+
export function VeleroRestoreRenderer({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
19
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
20
|
+
const name = data?.metadata?.name ?? ''
|
|
21
|
+
const fetcher = useVeleroRunMessages('restores', namespace, name)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<BaseVeleroRestoreRenderer
|
|
25
|
+
data={data}
|
|
26
|
+
onNavigate={onNavigate}
|
|
27
|
+
messages={{
|
|
28
|
+
messages: fetcher.data,
|
|
29
|
+
loading: fetcher.isPending,
|
|
30
|
+
lookupNote: fetcher.error ? <LookupFailureNote errors={[fetcher.error]} what="the messages behind these counts" /> : undefined,
|
|
31
|
+
onFetch: namespace && name ? () => fetcher.mutate() : undefined,
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -11,6 +11,7 @@ export { CronWorkflowRenderer } from './CronWorkflowRenderer'
|
|
|
11
11
|
export { HPARenderer } from './HPARenderer'
|
|
12
12
|
export { NodeRenderer } from './NodeRenderer'
|
|
13
13
|
export { PVCRenderer } from './PVCRenderer'
|
|
14
|
+
export { KyvernoPolicyCoverage } from './KyvernoPolicyCoverage'
|
|
14
15
|
export { RolloutRenderer } from './RolloutRenderer'
|
|
15
16
|
export { CertificateRenderer } from './CertificateRenderer'
|
|
16
17
|
export { WorkflowRenderer } from './WorkflowRenderer'
|
|
@@ -17,15 +17,7 @@ import type { AddonMode } from './TrafficView'
|
|
|
17
17
|
import { getNamespaceColor } from '../../utils/traffic-colors'
|
|
18
18
|
import { Tooltip } from '../ui/Tooltip'
|
|
19
19
|
import { Input } from '@skyhook-io/k8s-ui'
|
|
20
|
-
|
|
21
|
-
// Connection threshold options
|
|
22
|
-
const CONNECTION_THRESHOLDS = [
|
|
23
|
-
{ value: 0, label: 'All traffic' },
|
|
24
|
-
{ value: 100, label: '100+ connections' },
|
|
25
|
-
{ value: 1000, label: '1K+ connections' },
|
|
26
|
-
{ value: 10000, label: '10K+ connections' },
|
|
27
|
-
{ value: 100000, label: '100K+ connections' },
|
|
28
|
-
]
|
|
20
|
+
import { volumeThresholds } from './trafficFilters'
|
|
29
21
|
|
|
30
22
|
// Time range options
|
|
31
23
|
const TIME_RANGES = [
|
|
@@ -63,7 +55,19 @@ interface TrafficFilterSidebarProps {
|
|
|
63
55
|
setTimeRange: (v: string) => void
|
|
64
56
|
|
|
65
57
|
// L7 filters (Hubble-only)
|
|
66
|
-
|
|
58
|
+
/** Whether the flows carry any L7 detail at all. Not "is this Hubble": Beyla and
|
|
59
|
+
* Istio report L7 too, and gating on the source name hid these filters from them. */
|
|
60
|
+
showL7Filters?: boolean
|
|
61
|
+
/** Status buckets that can actually match. A source reporting no status
|
|
62
|
+
* distribution still reports an error rate, so 5xx can be offered on the strength
|
|
63
|
+
* of that while the others would match nothing. */
|
|
64
|
+
availableStatusRanges?: string[]
|
|
65
|
+
availableVerdicts?: string[]
|
|
66
|
+
hasDNSQueries?: boolean
|
|
67
|
+
availableHTTPMethods?: string[]
|
|
68
|
+
/** The active source measures rates rather than counting events, which changes
|
|
69
|
+
* both the unit and the useful scale of the volume filter. */
|
|
70
|
+
isRateBased?: boolean
|
|
67
71
|
l7Protocol: string // 'all' | 'HTTP' | 'DNS' | 'TCP'
|
|
68
72
|
setL7Protocol: (v: string) => void
|
|
69
73
|
l7Methods: Set<string>
|
|
@@ -154,7 +158,12 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
154
158
|
setDetectServices,
|
|
155
159
|
timeRange,
|
|
156
160
|
setTimeRange,
|
|
157
|
-
|
|
161
|
+
showL7Filters,
|
|
162
|
+
availableStatusRanges = [],
|
|
163
|
+
availableVerdicts = [],
|
|
164
|
+
hasDNSQueries,
|
|
165
|
+
availableHTTPMethods = [],
|
|
166
|
+
isRateBased,
|
|
158
167
|
l7Protocol,
|
|
159
168
|
setL7Protocol,
|
|
160
169
|
l7Methods,
|
|
@@ -209,7 +218,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
209
218
|
onChange={(e) => setMinConnections(Number(e.target.value))}
|
|
210
219
|
className="flex-1 bg-theme-elevated text-theme-text-primary text-xs rounded px-2 py-1.5 border border-theme-border focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
211
220
|
>
|
|
212
|
-
{
|
|
221
|
+
{volumeThresholds(isRateBased).map(({ value, label }) => (
|
|
213
222
|
<option key={value} value={value}>{label}</option>
|
|
214
223
|
))}
|
|
215
224
|
</select>
|
|
@@ -316,8 +325,10 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
316
325
|
</div>
|
|
317
326
|
</div>
|
|
318
327
|
|
|
319
|
-
{/* L7
|
|
320
|
-
|
|
328
|
+
{/* L7 filters, shown whenever the flows carry L7 detail. Every control below
|
|
329
|
+
is gated on data that exists, so the panel never offers one that cannot
|
|
330
|
+
return anything. */}
|
|
331
|
+
{showL7Filters && (
|
|
321
332
|
<div className="space-y-2 px-3 py-2 border-t border-theme-border">
|
|
322
333
|
<div className="flex items-center gap-1.5">
|
|
323
334
|
<Filter className="w-3 h-3 text-theme-text-tertiary" />
|
|
@@ -346,12 +357,13 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
346
357
|
</div>
|
|
347
358
|
|
|
348
359
|
{/* HTTP sub-filters (visible when protocol is All or HTTP) */}
|
|
349
|
-
{(l7Protocol === 'all' || l7Protocol === 'HTTP') && (
|
|
360
|
+
{(l7Protocol === 'all' || l7Protocol === 'HTTP') && (availableHTTPMethods.length > 0 || availableStatusRanges.length > 0) && (
|
|
350
361
|
<>
|
|
362
|
+
{availableHTTPMethods.length > 0 && (
|
|
351
363
|
<div>
|
|
352
364
|
<div className="text-[10px] text-theme-text-tertiary mb-1">HTTP Method</div>
|
|
353
365
|
<div className="flex flex-wrap gap-1">
|
|
354
|
-
{
|
|
366
|
+
{availableHTTPMethods.map(method => (
|
|
355
367
|
<button
|
|
356
368
|
key={method}
|
|
357
369
|
onClick={() => onToggleL7Method(method)}
|
|
@@ -367,7 +379,9 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
367
379
|
))}
|
|
368
380
|
</div>
|
|
369
381
|
</div>
|
|
382
|
+
)}
|
|
370
383
|
|
|
384
|
+
{availableStatusRanges.length > 0 && (
|
|
371
385
|
<div>
|
|
372
386
|
<div className="text-[10px] text-theme-text-tertiary mb-1">Status Code</div>
|
|
373
387
|
<div className="flex flex-wrap gap-1">
|
|
@@ -376,7 +390,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
376
390
|
{ label: '3xx', active: SEVERITY_BADGE.info },
|
|
377
391
|
{ label: '4xx', active: SEVERITY_BADGE.warning },
|
|
378
392
|
{ label: '5xx', active: SEVERITY_BADGE.error },
|
|
379
|
-
] as const).map(({ label, active }) => (
|
|
393
|
+
] as const).filter(({ label }) => availableStatusRanges.includes(label)).map(({ label, active }) => (
|
|
380
394
|
<button
|
|
381
395
|
key={label}
|
|
382
396
|
onClick={() => onToggleL7StatusRange(label)}
|
|
@@ -392,11 +406,12 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
392
406
|
))}
|
|
393
407
|
</div>
|
|
394
408
|
</div>
|
|
409
|
+
)}
|
|
395
410
|
</>
|
|
396
411
|
)}
|
|
397
412
|
|
|
398
413
|
{/* DNS sub-filter (visible when protocol is All or DNS) */}
|
|
399
|
-
{(l7Protocol === 'all' || l7Protocol === 'DNS') && (
|
|
414
|
+
{(l7Protocol === 'all' || l7Protocol === 'DNS') && hasDNSQueries && (
|
|
400
415
|
<div>
|
|
401
416
|
<div className="text-[10px] text-theme-text-tertiary mb-1">DNS Query</div>
|
|
402
417
|
<Input
|
|
@@ -408,7 +423,8 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
408
423
|
</div>
|
|
409
424
|
)}
|
|
410
425
|
|
|
411
|
-
{/* Verdict
|
|
426
|
+
{/* Verdict, limited to the verdicts the data actually contains. */}
|
|
427
|
+
{availableVerdicts.length > 0 && (
|
|
412
428
|
<div>
|
|
413
429
|
<div className="text-[10px] text-theme-text-tertiary mb-1">Verdict</div>
|
|
414
430
|
<div className="flex flex-wrap gap-1">
|
|
@@ -416,7 +432,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
416
432
|
{ label: 'forwarded', active: SEVERITY_BADGE.success },
|
|
417
433
|
{ label: 'dropped', active: SEVERITY_BADGE.error },
|
|
418
434
|
{ label: 'error', active: SEVERITY_BADGE.warning },
|
|
419
|
-
] as const).map(({ label, active }) => (
|
|
435
|
+
] as const).filter(({ label }) => availableVerdicts.includes(label)).map(({ label, active }) => (
|
|
420
436
|
<button
|
|
421
437
|
key={label}
|
|
422
438
|
onClick={() => onToggleL7Verdict(label)}
|
|
@@ -432,6 +448,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
432
448
|
))}
|
|
433
449
|
</div>
|
|
434
450
|
</div>
|
|
451
|
+
)}
|
|
435
452
|
</div>
|
|
436
453
|
)}
|
|
437
454
|
|
|
@@ -181,10 +181,24 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
|
|
|
181
181
|
</span>
|
|
182
182
|
</Tooltip>
|
|
183
183
|
|
|
184
|
-
{/* Destination
|
|
185
|
-
|
|
184
|
+
{/* Destination. An unoriented conversation has no caller and no
|
|
185
|
+
callee — the two ends are ordered arbitrarily — so it is marked
|
|
186
|
+
rather than presented as a direction the source established. */}
|
|
187
|
+
<Tooltip
|
|
188
|
+
content={
|
|
189
|
+
flow.directionUnknown
|
|
190
|
+
? 'Direction unknown: this source could not determine which end opened the conversation'
|
|
191
|
+
: flow.destination.namespace
|
|
192
|
+
? `${flow.destination.namespace}/${flow.destination.name}`
|
|
193
|
+
: flow.destination.name
|
|
194
|
+
}
|
|
195
|
+
wrapperClassName="min-w-0"
|
|
196
|
+
>
|
|
186
197
|
<span className="truncate text-theme-text-primary">
|
|
187
198
|
{flow.destination.name}
|
|
199
|
+
{flow.directionUnknown && (
|
|
200
|
+
<span className="ml-1 text-theme-text-tertiary">(direction unknown)</span>
|
|
201
|
+
)}
|
|
188
202
|
</span>
|
|
189
203
|
</Tooltip>
|
|
190
204
|
|