@skyhook-io/k8s-ui 1.16.3 → 1.17.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 +1 -1
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/primitives.tsx +3 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +5 -12
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +2 -5
- package/src/components/issues/issue-timing.ts +2 -2
- package/src/components/issues/issues.test.ts +1 -1
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +5 -0
- package/src/components/logs/useLogStream.ts +11 -0
- package/src/components/resources/ResourcesView.tsx +7 -8
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +2 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +24 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +1 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +2 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.tsx +6 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.tsx +21 -7
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +8 -21
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-cnpg.ts +0 -4
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +17 -33
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +9 -20
- package/src/components/shared/ResourceRendererDispatch.tsx +1 -1
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +2 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -10
- package/src/components/topology/TopologyGraph.tsx +5 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/drawer-components.test.tsx +66 -1
- package/src/components/ui/drawer-components.tsx +43 -29
- package/src/components/workload/WorkloadView.deleted.test.tsx +58 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.tsx +120 -43
- package/src/types/capacity.ts +10 -0
- package/src/types/core.ts +32 -1
- package/src/utils/format.test.ts +40 -2
- package/src/utils/format.ts +5 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/navigation.test.ts +13 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/resource-hierarchy.test.ts +32 -0
- package/src/utils/resource-hierarchy.ts +28 -2
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { WorkloadView } from './WorkloadView'
|
|
5
|
+
|
|
6
|
+
const pod = {
|
|
7
|
+
apiVersion: 'v1',
|
|
8
|
+
kind: 'Pod',
|
|
9
|
+
metadata: { name: 'web-1', namespace: 'shop' },
|
|
10
|
+
spec: { containers: [{ name: 'app', image: 'nginx' }] },
|
|
11
|
+
status: { phase: 'Running', containerStatuses: [{ name: 'app', ready: true, state: { running: {} } }] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function render(resourceError: unknown) {
|
|
15
|
+
return renderToStaticMarkup(
|
|
16
|
+
<WorkloadView
|
|
17
|
+
kind="pods"
|
|
18
|
+
namespace="shop"
|
|
19
|
+
name="web-1"
|
|
20
|
+
onBack={vi.fn()}
|
|
21
|
+
resource={pod}
|
|
22
|
+
resourceError={resourceError}
|
|
23
|
+
relationships={{ deployment: { kind: 'Deployment', namespace: 'shop', name: 'web' } }}
|
|
24
|
+
onNavigateToResource={vi.fn()}
|
|
25
|
+
actionsBarProps={{ canExec: true, canViewLogs: true, onOpenTerminal: vi.fn(), onOpenLogs: vi.fn(), onDelete: vi.fn(), renderDiagnose: () => 'Diagnose' }}
|
|
26
|
+
hasOperationalIssues
|
|
27
|
+
renderOverviewLead={() => 'Host issues panel'}
|
|
28
|
+
renderOverviewExtra={() => 'Host reachability panel'}
|
|
29
|
+
renderDiagnoseTab={() => 'Host reachability tab'}
|
|
30
|
+
reachableVia={[{ kind: 'Service', namespace: 'shop', name: 'web' }]}
|
|
31
|
+
/>,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('WorkloadView for an object deleted while open', () => {
|
|
36
|
+
it('says it is gone, names its owner, and offers nothing that acts on it', () => {
|
|
37
|
+
const html = render(Object.assign(new Error('pods "web-1" not found'), { status: 404 }))
|
|
38
|
+
expect(html).toContain('This Pod no longer exists in the cluster')
|
|
39
|
+
expect(html).toContain('Go to its Deployment web')
|
|
40
|
+
expect(html).toContain('>Deleted<')
|
|
41
|
+
expect(html).not.toContain('Terminal')
|
|
42
|
+
expect(html).not.toContain('Diagnose')
|
|
43
|
+
expect(html).not.toContain('Host issues panel')
|
|
44
|
+
expect(html).not.toContain('Host reachability panel')
|
|
45
|
+
expect(html).not.toContain('Reachability')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('shows the live view when the fetch failed for another reason', () => {
|
|
49
|
+
const html = render(Object.assign(new Error('boom'), { status: 500 }))
|
|
50
|
+
expect(html).not.toContain('no longer exists')
|
|
51
|
+
expect(html).not.toContain('>Deleted<')
|
|
52
|
+
expect(html).toContain('Terminal')
|
|
53
|
+
expect(html).toContain('Diagnose')
|
|
54
|
+
expect(html).toContain('Host issues panel')
|
|
55
|
+
expect(html).toContain('Host reachability panel')
|
|
56
|
+
expect(html).toContain('Reachability')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { WorkloadView } from './WorkloadView'
|
|
7
|
+
|
|
8
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
9
|
+
|
|
10
|
+
const deployment = {
|
|
11
|
+
apiVersion: 'apps/v1',
|
|
12
|
+
kind: 'Deployment',
|
|
13
|
+
metadata: { name: 'web', namespace: 'shop' },
|
|
14
|
+
spec: { replicas: 1, template: { spec: { containers: [{ name: 'app', image: 'nginx' }] } } },
|
|
15
|
+
status: { replicas: 1, readyReplicas: 1, availableReplicas: 1 },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('closes an open Delete dialog when the object is deleted elsewhere', async () => {
|
|
19
|
+
const element = document.createElement('div')
|
|
20
|
+
document.body.append(element)
|
|
21
|
+
const root = createRoot(element)
|
|
22
|
+
const render = (resourceError: unknown) =>
|
|
23
|
+
root.render(
|
|
24
|
+
<WorkloadView
|
|
25
|
+
kind="deployments"
|
|
26
|
+
namespace="shop"
|
|
27
|
+
name="web"
|
|
28
|
+
onBack={vi.fn()}
|
|
29
|
+
resource={deployment}
|
|
30
|
+
resourceError={resourceError}
|
|
31
|
+
actionsBarProps={{ onDelete: vi.fn() }}
|
|
32
|
+
/>,
|
|
33
|
+
)
|
|
34
|
+
const dialogOpen = () => document.body.textContent?.includes('Are you sure you want to delete') ?? false
|
|
35
|
+
|
|
36
|
+
await act(async () => render(null))
|
|
37
|
+
await act(async () => { element.querySelector<HTMLButtonElement>('button:has(svg.lucide-trash-2)')!.click() })
|
|
38
|
+
expect(dialogOpen()).toBe(true)
|
|
39
|
+
|
|
40
|
+
await act(async () => render(Object.assign(new Error('deployments.apps "web" not found'), { status: 404 })))
|
|
41
|
+
expect(dialogOpen()).toBe(false)
|
|
42
|
+
|
|
43
|
+
await act(async () => root.unmount())
|
|
44
|
+
element.remove()
|
|
45
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { WorkloadView } from './WorkloadView'
|
|
7
|
+
|
|
8
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
9
|
+
|
|
10
|
+
const secret = {
|
|
11
|
+
apiVersion: 'v1',
|
|
12
|
+
kind: 'Secret',
|
|
13
|
+
metadata: { name: 'db', namespace: 'shop', resourceVersion: '7' },
|
|
14
|
+
type: 'Opaque',
|
|
15
|
+
data: { password: btoa('old') },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function button(label: string): HTMLButtonElement | undefined {
|
|
19
|
+
return [...document.querySelectorAll('button')].find(b => b.textContent?.trim() === label || b.title === label)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('saves a Secret value against the version on screen, and offers no edit once the Secret is gone', async () => {
|
|
23
|
+
const element = document.createElement('div')
|
|
24
|
+
document.body.append(element)
|
|
25
|
+
const root = createRoot(element)
|
|
26
|
+
const onUpdateResource = vi.fn().mockResolvedValue(undefined)
|
|
27
|
+
const render = (resourceError: unknown) =>
|
|
28
|
+
root.render(
|
|
29
|
+
<WorkloadView
|
|
30
|
+
kind="secrets"
|
|
31
|
+
namespace="shop"
|
|
32
|
+
name="db"
|
|
33
|
+
onBack={vi.fn()}
|
|
34
|
+
resource={secret}
|
|
35
|
+
resourceError={resourceError}
|
|
36
|
+
canUpdateSecrets
|
|
37
|
+
onUpdateResource={onUpdateResource}
|
|
38
|
+
/>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await act(async () => render(null))
|
|
42
|
+
await act(async () => button('Reveal')!.click())
|
|
43
|
+
await act(async () => button('Edit value')!.click())
|
|
44
|
+
await act(async () => button('Save')!.click())
|
|
45
|
+
await act(async () => button('Update')!.click())
|
|
46
|
+
expect(onUpdateResource).toHaveBeenCalledWith(expect.objectContaining({
|
|
47
|
+
kind: 'secrets',
|
|
48
|
+
namespace: 'shop',
|
|
49
|
+
name: 'db',
|
|
50
|
+
reviewedResourceVersion: '7',
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
await act(async () => button('Edit value')!.click())
|
|
54
|
+
expect(element.querySelector('textarea')).not.toBeNull()
|
|
55
|
+
await act(async () => render(Object.assign(new Error('secrets "db" not found'), { status: 404 })))
|
|
56
|
+
expect(element.textContent).toContain('This Secret no longer exists in the cluster')
|
|
57
|
+
expect(element.querySelector('textarea')).toBeNull()
|
|
58
|
+
expect(button('Edit value')).toBeUndefined()
|
|
59
|
+
|
|
60
|
+
await act(async () => root.unmount())
|
|
61
|
+
element.remove()
|
|
62
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeResourceRefs } from '../../utils/resource-refs'
|
|
1
2
|
import { PodRow, PodListFrame, PodListToggle, workloadPodDetail } from './PodList'
|
|
2
3
|
export { workloadPodDetail, podStatusLabel, podStatusClass, podDotClass } from './PodList'
|
|
3
4
|
import { useState, useMemo, useEffect, useRef, useCallback, type ComponentType, type ReactNode } from 'react'
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
} from 'lucide-react'
|
|
38
39
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
39
40
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
41
|
+
import { isFetchError } from '../../types/fetch-error'
|
|
40
42
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
41
43
|
import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
|
|
42
44
|
import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
|
|
@@ -76,6 +78,7 @@ import { ServicePortCards, type ServicePortRenderProps } from '../resources/rend
|
|
|
76
78
|
import { rolloutMayAdvanceAutomatically, type WorkloadRolloutActivity } from '../../utils/workload-rollout'
|
|
77
79
|
import { WorkloadRolloutNotice } from './WorkloadRolloutNotice'
|
|
78
80
|
import { isCoreBatchJob } from '../../utils/api-resources'
|
|
81
|
+
import { formatRelativeAgeTime } from '../../utils/format'
|
|
79
82
|
|
|
80
83
|
export type WorkloadTabType = 'overview' | 'spec' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'reachability' | 'cost' | 'yaml'
|
|
81
84
|
type TabType = WorkloadTabType
|
|
@@ -281,6 +284,8 @@ interface WorkloadViewProps {
|
|
|
281
284
|
* resource rather than from owner labels on a managed object.
|
|
282
285
|
*/
|
|
283
286
|
onNavigateGitOpsPath?: (path: string) => void
|
|
287
|
+
/** Opens the live traffic view focused on this workload. */
|
|
288
|
+
onOpenLiveTraffic?: () => void
|
|
284
289
|
|
|
285
290
|
// ── Render props for platform-specific content ───────────────────────────
|
|
286
291
|
/** Render the logs tab content */
|
|
@@ -494,6 +499,7 @@ export function WorkloadView({
|
|
|
494
499
|
helmOwnerSource,
|
|
495
500
|
onOpenHelmRelease,
|
|
496
501
|
onNavigateGitOpsPath,
|
|
502
|
+
onOpenLiveTraffic,
|
|
497
503
|
}: WorkloadViewProps) {
|
|
498
504
|
// The live object preserves exact CRD capitalization (RayJob, TFJob); the URL
|
|
499
505
|
// plural remains the API resource name used by fetches and mutations.
|
|
@@ -688,6 +694,11 @@ export function WorkloadView({
|
|
|
688
694
|
setTimeout(() => setCopied(null), 2000)
|
|
689
695
|
}, [])
|
|
690
696
|
|
|
697
|
+
// The edit is applied on top of the copy on screen, so the server is asked to
|
|
698
|
+
// reject it if the Secret was deleted or changed since that copy loaded.
|
|
699
|
+
// Otherwise the save would recreate a deleted Secret, or put back the old
|
|
700
|
+
// value of a key someone else changed.
|
|
701
|
+
const resourceVersion: string | undefined = resource?.metadata?.resourceVersion
|
|
691
702
|
const handleSaveSecretValue = useCallback(async (yaml: string) => {
|
|
692
703
|
if (!onUpdateResource) return
|
|
693
704
|
try {
|
|
@@ -697,12 +708,14 @@ export function WorkloadView({
|
|
|
697
708
|
name,
|
|
698
709
|
yaml,
|
|
699
710
|
force: true,
|
|
711
|
+
reviewedResourceVersion: resourceVersion,
|
|
700
712
|
})
|
|
701
713
|
setTimeout(() => refetch(), 1000)
|
|
702
|
-
} catch {
|
|
703
|
-
|
|
714
|
+
} catch (error) {
|
|
715
|
+
refetch()
|
|
716
|
+
throw error
|
|
704
717
|
}
|
|
705
|
-
}, [onUpdateResource, apiKind, namespace, name, refetch])
|
|
718
|
+
}, [onUpdateResource, apiKind, namespace, name, resourceVersion, refetch])
|
|
706
719
|
|
|
707
720
|
const handleSaved = useCallback(() => {
|
|
708
721
|
setSaveSuccess(true)
|
|
@@ -765,7 +778,32 @@ export function WorkloadView({
|
|
|
765
778
|
? getWorkloadDisplayStatus(resource, apiKind, workloadPods)
|
|
766
779
|
: null
|
|
767
780
|
const rolloutActivity = rolloutDisplay?.activity ?? null
|
|
768
|
-
|
|
781
|
+
// A 404 with a cached copy: the object was deleted while this view was
|
|
782
|
+
// open. Everything below that copy describes the past.
|
|
783
|
+
const resourceGone = Boolean(resource) && isFetchError(resourceError) && resourceError.status === 404
|
|
784
|
+
const status = resourceGone
|
|
785
|
+
? { text: 'Deleted', color: 'status-unknown' }
|
|
786
|
+
: rolloutDisplay?.status ?? getResourceStatus(apiKind, resource)
|
|
787
|
+
// The host's actions, and the host's renderers with their per-container
|
|
788
|
+
// terminal, logs, scale and env controls, would act on an object that no
|
|
789
|
+
// longer exists. The plain renderers show the last copy without them.
|
|
790
|
+
const actionsBarPropsForState = resourceGone ? undefined : effectiveActionsBarProps
|
|
791
|
+
const rendererOverridesForState = resourceGone ? undefined : rendererOverrides
|
|
792
|
+
// The host's overview panels and Reachability tab diagnose the live object,
|
|
793
|
+
// and the reachability panel polls for it every few seconds.
|
|
794
|
+
const renderOverviewLeadForState = resourceGone ? undefined : renderOverviewLead
|
|
795
|
+
const renderOverviewExtraForState = resourceGone ? undefined : renderOverviewExtra
|
|
796
|
+
const renderDiagnoseTabForState = resourceGone ? undefined : renderDiagnoseTab
|
|
797
|
+
// A remount closes any dialog the bar had open: once its actions are gone,
|
|
798
|
+
// a confirm button left on screen would do nothing.
|
|
799
|
+
const actionsBarKey = resourceGone ? 'gone' : 'live'
|
|
800
|
+
const deletedNotice = resourceGone ? (
|
|
801
|
+
<DeletedResourceNotice
|
|
802
|
+
kindLabel={displayKindName(apiKind, resource?.kind)}
|
|
803
|
+
owner={relationships?.deployment ?? relationships?.owner}
|
|
804
|
+
onNavigateToResource={onNavigateToResource}
|
|
805
|
+
/>
|
|
806
|
+
) : null
|
|
769
807
|
const rolloutMayAutoAdvance = rolloutActivity ? rolloutMayAdvanceAutomatically(rolloutActivity) : false
|
|
770
808
|
useEffect(() => {
|
|
771
809
|
if (!recentImageSave) return
|
|
@@ -778,7 +816,9 @@ export function WorkloadView({
|
|
|
778
816
|
const timeout = window.setTimeout(() => setRecentImageSave(false), 15000)
|
|
779
817
|
return () => window.clearTimeout(timeout)
|
|
780
818
|
}, [recentImageSave, rolloutActivity?.phase])
|
|
781
|
-
|
|
819
|
+
// The rollout state above is the last copy fetched before a 404; a workload
|
|
820
|
+
// the server says is gone has nothing left to roll out.
|
|
821
|
+
useProgressiveRefresh((rolloutMayAutoAdvance || recentImageSave) && !resourceGone, refetchProp)
|
|
782
822
|
const showOwnershipHeading = kind === 'Pod' && Boolean(ownershipContext)
|
|
783
823
|
const headerImage = metadata.find(m => m.label === 'Image')?.value
|
|
784
824
|
|
|
@@ -790,7 +830,7 @@ export function WorkloadView({
|
|
|
790
830
|
const renderDiagnose = actionsBarProps?.renderDiagnose as
|
|
791
831
|
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
792
832
|
| undefined
|
|
793
|
-
const diagnoseAction = renderDiagnose?.({
|
|
833
|
+
const diagnoseAction = resourceGone ? null : renderDiagnose?.({
|
|
794
834
|
kind: resource?.kind ?? knownKindForPluralWithGroup(apiKind, group ?? '') ?? apiKind,
|
|
795
835
|
group,
|
|
796
836
|
namespace,
|
|
@@ -827,7 +867,7 @@ export function WorkloadView({
|
|
|
827
867
|
id: 'reachability',
|
|
828
868
|
label: 'Reachability',
|
|
829
869
|
icon: <Stethoscope className="w-4 h-4" />,
|
|
830
|
-
hidden: !(
|
|
870
|
+
hidden: !(renderDiagnoseTabForState && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
|
|
831
871
|
},
|
|
832
872
|
{ id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
|
|
833
873
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
@@ -929,19 +969,20 @@ export function WorkloadView({
|
|
|
929
969
|
</Tooltip>
|
|
930
970
|
</div>
|
|
931
971
|
<p className="text-sm text-theme-text-tertiary">{namespace}</p>
|
|
932
|
-
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
|
|
972
|
+
{(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
|
|
933
973
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
934
974
|
{gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
|
|
935
975
|
{helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
|
|
936
976
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
937
977
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
938
978
|
)}
|
|
979
|
+
{onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
|
|
939
980
|
</div>
|
|
940
981
|
)}
|
|
941
982
|
</div>
|
|
942
983
|
|
|
943
984
|
{/* Actions bar */}
|
|
944
|
-
<ResourceActionsBar resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...
|
|
985
|
+
<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} onClose={onClose} showYaml={showYaml} onToggleYaml={() => switchView(!showYaml)} {...actionsBarPropsForState} />
|
|
945
986
|
</div>
|
|
946
987
|
|
|
947
988
|
{/* Success animation overlay */}
|
|
@@ -956,6 +997,7 @@ export function WorkloadView({
|
|
|
956
997
|
damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
|
|
957
998
|
place. */}
|
|
958
999
|
<div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
1000
|
+
{deletedNotice && <div className="px-4 pt-4">{deletedNotice}</div>}
|
|
959
1001
|
{!resource ? (
|
|
960
1002
|
// Fill the drawer body so the loading logo centers in it, not in a
|
|
961
1003
|
// 128px box pinned to the top (matches the splash/PaneLoader centering).
|
|
@@ -1007,9 +1049,9 @@ export function WorkloadView({
|
|
|
1007
1049
|
drawerSummary
|
|
1008
1050
|
) : (
|
|
1009
1051
|
<>
|
|
1010
|
-
{
|
|
1052
|
+
{renderOverviewLeadForState && hasOperationalIssues && (
|
|
1011
1053
|
<div className="px-4 pt-4">
|
|
1012
|
-
{
|
|
1054
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
1013
1055
|
</div>
|
|
1014
1056
|
)}
|
|
1015
1057
|
<ResourceRendererDispatch
|
|
@@ -1023,9 +1065,9 @@ export function WorkloadView({
|
|
|
1023
1065
|
onCopy={copyToClipboard}
|
|
1024
1066
|
copied={copied}
|
|
1025
1067
|
onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
|
|
1026
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1068
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
1027
1069
|
isSavingSecret={isUpdatingResource}
|
|
1028
|
-
rendererOverrides={
|
|
1070
|
+
rendererOverrides={rendererOverridesForState}
|
|
1029
1071
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
1030
1072
|
renderMetrics={renderMetricsTab}
|
|
1031
1073
|
events={resourceFocusedK8sEvents}
|
|
@@ -1033,7 +1075,7 @@ export function WorkloadView({
|
|
|
1033
1075
|
updates={resourceFocusedUpdates}
|
|
1034
1076
|
eventsError={resourceFocusedK8sError}
|
|
1035
1077
|
updatesError={resourceFocusedUpdatesError}
|
|
1036
|
-
mainFooter={
|
|
1078
|
+
mainFooter={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'drawer' })}
|
|
1037
1079
|
/>
|
|
1038
1080
|
</>
|
|
1039
1081
|
)}
|
|
@@ -1115,6 +1157,7 @@ export function WorkloadView({
|
|
|
1115
1157
|
{gitOpsResourcePath && onNavigateGitOpsPath && (
|
|
1116
1158
|
<OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
|
|
1117
1159
|
)}
|
|
1160
|
+
{onOpenLiveTraffic && <LiveTrafficChip onClick={onOpenLiveTraffic} />}
|
|
1118
1161
|
{relationships?.owner && !showOwnershipHeading && (
|
|
1119
1162
|
<span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
|
|
1120
1163
|
)}
|
|
@@ -1157,12 +1200,13 @@ export function WorkloadView({
|
|
|
1157
1200
|
activeTab={effectiveTab}
|
|
1158
1201
|
onTabChange={handleSetTab}
|
|
1159
1202
|
scopeControls={scopeControls}
|
|
1160
|
-
tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...
|
|
1203
|
+
tabStripEnd={<ResourceActionsBar key={actionsBarKey} resource={selectedResource} data={resource} hideLogs {...actionsBarPropsForState} />}
|
|
1161
1204
|
overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
|
|
1162
1205
|
compactHeader={compactHeader}
|
|
1163
1206
|
>
|
|
1164
1207
|
<div className="flex h-full min-h-0 flex-col">
|
|
1165
|
-
{
|
|
1208
|
+
{deletedNotice && <div className="shrink-0 px-4 pt-4">{deletedNotice}</div>}
|
|
1209
|
+
{!resourceGone && effectiveTab !== 'overview' && rolloutActivity && (rolloutActivity.phase !== 'idle' || recentImageSave) && (
|
|
1166
1210
|
<div className="shrink-0 px-4 pt-4">
|
|
1167
1211
|
<WorkloadRolloutNotice activity={rolloutActivity} recentImageSave={recentImageSave} />
|
|
1168
1212
|
</div>
|
|
@@ -1173,9 +1217,9 @@ export function WorkloadView({
|
|
|
1173
1217
|
<div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
|
|
1174
1218
|
) : effectiveTab === 'overview' && expandedOverview ? (
|
|
1175
1219
|
<div className="h-full min-h-0">
|
|
1176
|
-
{hasOperationalIssues &&
|
|
1220
|
+
{hasOperationalIssues && renderOverviewLeadForState && (
|
|
1177
1221
|
<div className="px-4 pt-4">
|
|
1178
|
-
{
|
|
1222
|
+
{renderOverviewLeadForState({ kind, namespace, name })}
|
|
1179
1223
|
</div>
|
|
1180
1224
|
)}
|
|
1181
1225
|
{expandedOverview}
|
|
@@ -1199,22 +1243,22 @@ export function WorkloadView({
|
|
|
1199
1243
|
onNavigate={onNavigateToResource}
|
|
1200
1244
|
onCopy={copyToClipboard}
|
|
1201
1245
|
copied={copied}
|
|
1202
|
-
onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
|
|
1246
|
+
onSaveSecretValue={canUpdateSecrets && !resourceGone ? handleSaveSecretValue : undefined}
|
|
1203
1247
|
isSavingSecret={isUpdatingResource}
|
|
1204
1248
|
onOpenLogs={handleOpenLogs}
|
|
1205
1249
|
onSwitchToTimeline={() => handleSetTab('timeline')}
|
|
1206
1250
|
onSwitchToLogs={logsTabVisible ? () => handleSetTab('logs') : undefined}
|
|
1207
1251
|
onSwitchToTopology={!topologyTabHidden ? () => handleSetTab('topology') : undefined}
|
|
1208
|
-
rendererOverrides={
|
|
1252
|
+
rendererOverrides={rendererOverridesForState}
|
|
1209
1253
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
1210
1254
|
events={overviewEvents}
|
|
1211
1255
|
eventsLoading={overviewEventsLoading}
|
|
1212
1256
|
updates={resourceFocusedUpdates}
|
|
1213
1257
|
eventsError={overviewEventsError}
|
|
1214
1258
|
updatesError={resourceFocusedUpdatesError}
|
|
1215
|
-
extraContent={
|
|
1259
|
+
extraContent={renderOverviewExtraForState && renderOverviewExtraForState({ kind, namespace, name, group, context: 'expanded' })}
|
|
1216
1260
|
introContent={overviewIntro}
|
|
1217
|
-
leadContent={hasOperationalIssues &&
|
|
1261
|
+
leadContent={hasOperationalIssues && renderOverviewLeadForState ? renderOverviewLeadForState({ kind, namespace, name }) : undefined}
|
|
1218
1262
|
onEvaluateCapacity={onEvaluateCapacity}
|
|
1219
1263
|
recentImageSave={recentImageSave}
|
|
1220
1264
|
/>
|
|
@@ -1273,9 +1317,9 @@ export function WorkloadView({
|
|
|
1273
1317
|
{renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
|
|
1274
1318
|
</div>
|
|
1275
1319
|
)}
|
|
1276
|
-
{effectiveTab === 'reachability' &&
|
|
1320
|
+
{effectiveTab === 'reachability' && renderDiagnoseTabForState && (
|
|
1277
1321
|
<div className="flex h-full min-h-0 flex-col p-3">
|
|
1278
|
-
{
|
|
1322
|
+
{renderDiagnoseTabForState({ kind: resource?.kind || kind, namespace, name })}
|
|
1279
1323
|
</div>
|
|
1280
1324
|
)}
|
|
1281
1325
|
{effectiveTab === 'cost' && renderCostTab && (
|
|
@@ -1406,6 +1450,34 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
|
|
|
1406
1450
|
// SUB-COMPONENTS
|
|
1407
1451
|
// ============================================================================
|
|
1408
1452
|
|
|
1453
|
+
function DeletedResourceNotice({
|
|
1454
|
+
kindLabel,
|
|
1455
|
+
owner,
|
|
1456
|
+
onNavigateToResource,
|
|
1457
|
+
}: {
|
|
1458
|
+
kindLabel: string
|
|
1459
|
+
owner?: ResourceRef
|
|
1460
|
+
onNavigateToResource?: NavigateToResource
|
|
1461
|
+
}) {
|
|
1462
|
+
return (
|
|
1463
|
+
<AlertBanner
|
|
1464
|
+
variant="warning"
|
|
1465
|
+
title={`This ${kindLabel} no longer exists in the cluster`}
|
|
1466
|
+
message="You're looking at the last version Radar loaded before it was deleted."
|
|
1467
|
+
>
|
|
1468
|
+
{owner && onNavigateToResource && (
|
|
1469
|
+
<button
|
|
1470
|
+
type="button"
|
|
1471
|
+
onClick={() => onNavigateToResource(refToSelectedResource(owner))}
|
|
1472
|
+
className="mt-2 text-xs text-accent-text hover:underline"
|
|
1473
|
+
>
|
|
1474
|
+
Go to its {displayKindName(owner.kind, owner.kind)} {owner.name}
|
|
1475
|
+
</button>
|
|
1476
|
+
)}
|
|
1477
|
+
</AlertBanner>
|
|
1478
|
+
)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1409
1481
|
function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
1410
1482
|
return (
|
|
1411
1483
|
<Tooltip content="Open this resource in the GitOps tab (tree + insights + ops)" delay={150}>
|
|
@@ -1421,6 +1493,21 @@ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
|
|
|
1421
1493
|
)
|
|
1422
1494
|
}
|
|
1423
1495
|
|
|
1496
|
+
function LiveTrafficChip({ onClick }: { onClick: () => void }) {
|
|
1497
|
+
return (
|
|
1498
|
+
<Tooltip content="What this workload talks to, from the cluster's traffic source" delay={150}>
|
|
1499
|
+
<button
|
|
1500
|
+
type="button"
|
|
1501
|
+
onClick={onClick}
|
|
1502
|
+
className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-elevated px-1.5 py-0.5 text-[11px] font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
|
|
1503
|
+
>
|
|
1504
|
+
Live traffic
|
|
1505
|
+
<ArrowRight className="h-3 w-3 shrink-0" />
|
|
1506
|
+
</button>
|
|
1507
|
+
</Tooltip>
|
|
1508
|
+
)
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1424
1511
|
function OwnershipHeading({
|
|
1425
1512
|
podName,
|
|
1426
1513
|
context,
|
|
@@ -2439,8 +2526,8 @@ function buildSummaryMetrics(
|
|
|
2439
2526
|
},
|
|
2440
2527
|
{
|
|
2441
2528
|
label: 'Latest run',
|
|
2442
|
-
value: status.lastScheduleTime ?
|
|
2443
|
-
detail: status.lastSuccessfulTime ? `last success ${
|
|
2529
|
+
value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never',
|
|
2530
|
+
detail: status.lastSuccessfulTime ? `last success ${formatRelativeAgeTime(status.lastSuccessfulTime)}` : 'no successful run recorded',
|
|
2444
2531
|
icon: Activity,
|
|
2445
2532
|
},
|
|
2446
2533
|
{
|
|
@@ -2556,8 +2643,8 @@ function getReadinessSummary(resource: any, apiKind: string): ReadinessSummary {
|
|
|
2556
2643
|
{ label: 'Schedule', value: spec.schedule || '-' },
|
|
2557
2644
|
{ label: 'Active jobs', value: active, tone: active > 0 ? 'warn' : 'default' },
|
|
2558
2645
|
{ label: 'Suspend', value: suspended ? 'Yes' : 'No', tone: suspended ? 'warn' : 'default' },
|
|
2559
|
-
{ label: 'Last schedule', value: status.lastScheduleTime ?
|
|
2560
|
-
{ label: 'Last success', value: status.lastSuccessfulTime ?
|
|
2646
|
+
{ label: 'Last schedule', value: status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : '-' },
|
|
2647
|
+
{ label: 'Last success', value: status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : '-' },
|
|
2561
2648
|
],
|
|
2562
2649
|
}
|
|
2563
2650
|
}
|
|
@@ -2656,8 +2743,8 @@ function getScalingSummary(
|
|
|
2656
2743
|
label: phase,
|
|
2657
2744
|
detail: '',
|
|
2658
2745
|
stats: [
|
|
2659
|
-
{ label: 'Started', value: status.startTime ?
|
|
2660
|
-
{ label: 'Finished', value: status.completionTime ?
|
|
2746
|
+
{ label: 'Started', value: status.startTime ? formatRelativeAgeTime(status.startTime) : '-' },
|
|
2747
|
+
{ label: 'Finished', value: status.completionTime ? formatRelativeAgeTime(status.completionTime) : phase === 'Running' ? 'Running' : '-' },
|
|
2661
2748
|
{ label: 'Parallelism', value: spec.parallelism ?? 1 },
|
|
2662
2749
|
{ label: 'Completions', value: spec.completions ?? 1 },
|
|
2663
2750
|
{ label: 'Completion mode', value: spec.completionMode ?? 'NonIndexed' },
|
|
@@ -3297,8 +3384,8 @@ function CronJobRunsCard({
|
|
|
3297
3384
|
>
|
|
3298
3385
|
<div className="mb-3 grid gap-x-8 gap-y-3 sm:grid-cols-2">
|
|
3299
3386
|
<OverviewStat label="Active jobs" value={activeJobs.length} tone={activeJobs.length > 0 ? 'warn' : 'default'} />
|
|
3300
|
-
<OverviewStat label="Last schedule" value={status.lastScheduleTime ?
|
|
3301
|
-
<OverviewStat label="Last success" value={status.lastSuccessfulTime ?
|
|
3387
|
+
<OverviewStat label="Last schedule" value={status.lastScheduleTime ? formatRelativeAgeTime(status.lastScheduleTime) : 'Never'} />
|
|
3388
|
+
<OverviewStat label="Last success" value={status.lastSuccessfulTime ? formatRelativeAgeTime(status.lastSuccessfulTime) : 'Never'} />
|
|
3302
3389
|
<OverviewStat label="Retained history" value={`${spec.successfulJobsHistoryLimit ?? 3} ok / ${spec.failedJobsHistoryLimit ?? 1} failed`} />
|
|
3303
3390
|
</div>
|
|
3304
3391
|
|
|
@@ -3755,7 +3842,7 @@ function ActivityCard({
|
|
|
3755
3842
|
<div className="min-w-0 flex-1">
|
|
3756
3843
|
<div className="flex items-center justify-between gap-2">
|
|
3757
3844
|
<span className="truncate text-sm font-medium text-theme-text-primary">{event.reason || event.eventType || 'update'}</span>
|
|
3758
|
-
<span className="shrink-0 text-xs text-theme-text-tertiary">{
|
|
3845
|
+
<span className="shrink-0 text-xs text-theme-text-tertiary">{formatRelativeAgeTime(event.timestamp)}</span>
|
|
3759
3846
|
</div>
|
|
3760
3847
|
<div className="mt-0.5 line-clamp-2 text-xs text-theme-text-secondary">
|
|
3761
3848
|
{event.message || event.diff?.summary || `${event.kind} ${event.name}`}
|
|
@@ -3878,16 +3965,6 @@ function summarizeServingExposure(
|
|
|
3878
3965
|
return { label: 'No serving path detected', badge: 'None', severity: 'neutral' }
|
|
3879
3966
|
}
|
|
3880
3967
|
|
|
3881
|
-
function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
3882
|
-
const seen = new Set<string>()
|
|
3883
|
-
return refs.filter((ref) => {
|
|
3884
|
-
const key = resourceRefId(ref)
|
|
3885
|
-
if (seen.has(key)) return false
|
|
3886
|
-
seen.add(key)
|
|
3887
|
-
return true
|
|
3888
|
-
})
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
3968
|
function resourceRefId(ref: ResourceRef): string {
|
|
3892
3969
|
return `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
3893
3970
|
}
|
package/src/types/capacity.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Issue } from "../components/issues/types";
|
|
2
|
+
import type { NodeLifecycleObservation } from "../utils/node-lifecycle";
|
|
2
3
|
|
|
3
4
|
export const CAPACITY_SCHEMA_VERSION = "v1alpha1" as const;
|
|
4
5
|
|
|
@@ -232,6 +233,14 @@ export interface CapacityClaimLifecycleSummary {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
export interface CapacityNodeLifecycleSummary {
|
|
236
|
+
operational?: {
|
|
237
|
+
total: number;
|
|
238
|
+
ready: number;
|
|
239
|
+
notReady: number;
|
|
240
|
+
cordoned: number;
|
|
241
|
+
removing: number;
|
|
242
|
+
removingUnhealthy: number;
|
|
243
|
+
};
|
|
235
244
|
total: number;
|
|
236
245
|
ready: number;
|
|
237
246
|
notReady: number;
|
|
@@ -325,6 +334,7 @@ export type CapacityClaimStage =
|
|
|
325
334
|
| "unknown";
|
|
326
335
|
|
|
327
336
|
export interface CapacityNodeMember {
|
|
337
|
+
lifecycle?: NodeLifecycleObservation;
|
|
328
338
|
ready?: boolean;
|
|
329
339
|
cordoned: boolean;
|
|
330
340
|
conditions: CapacityCondition[];
|
package/src/types/core.ts
CHANGED
|
@@ -167,6 +167,9 @@ export interface FeatureCapabilities {
|
|
|
167
167
|
podEnvironment?: boolean
|
|
168
168
|
policyResource?: boolean
|
|
169
169
|
workloadHistory?: boolean
|
|
170
|
+
serviceEndpointSlices?: boolean
|
|
171
|
+
trafficRecords?: boolean
|
|
172
|
+
gitopsWriteEvidence?: boolean
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
// DeploymentMode is the closed set of topologies Radar can run in.
|
|
@@ -653,6 +656,8 @@ export interface Relationships {
|
|
|
653
656
|
consumers?: ResourceRef[]
|
|
654
657
|
scalers?: ResourceRef[]
|
|
655
658
|
storageRefs?: ResourceRef[]
|
|
659
|
+
dependencies?: ResourceRef[]
|
|
660
|
+
dependents?: ResourceRef[]
|
|
656
661
|
scaleTarget?: ResourceRef
|
|
657
662
|
pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
|
|
658
663
|
networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
|
|
@@ -817,6 +822,7 @@ export interface HelmRevision {
|
|
|
817
822
|
appVersion: string
|
|
818
823
|
description: string
|
|
819
824
|
updated: string // ISO date string
|
|
825
|
+
deleted?: string
|
|
820
826
|
}
|
|
821
827
|
|
|
822
828
|
export type HelmOperationKind = 'release_failed' | 'upgrade_failed' | 'upgrade_rolled_back' | 'rollback' | 'pending'
|
|
@@ -1284,10 +1290,12 @@ export interface MetricsDataPoint {
|
|
|
1284
1290
|
export interface TrafficEndpoint {
|
|
1285
1291
|
name: string
|
|
1286
1292
|
namespace: string
|
|
1287
|
-
kind: string // Pod, Service, External, Host, Unknown
|
|
1293
|
+
kind: string // Pod, Workload, Service, External, Host, Unknown
|
|
1288
1294
|
ip?: string
|
|
1289
1295
|
labels?: Record<string, string>
|
|
1290
1296
|
workload?: string
|
|
1297
|
+
/** Deployment, StatefulSet, CronJob, … when resolved from the pod's owners. */
|
|
1298
|
+
workloadKind?: string
|
|
1291
1299
|
port?: number
|
|
1292
1300
|
}
|
|
1293
1301
|
|
|
@@ -1453,6 +1461,12 @@ export interface TrafficFlowsResponse {
|
|
|
1453
1461
|
* nodeFlowLimit, so older traffic in the window did not fit. */
|
|
1454
1462
|
coveredSince?: string
|
|
1455
1463
|
nodeFlowLimit?: number
|
|
1464
|
+
/** Set when the source kept only its newest flowLimit flows in total; the
|
|
1465
|
+
* window is then complete only from coveredSince. */
|
|
1466
|
+
flowLimit?: number
|
|
1467
|
+
/** How many records the aggregation covers. `flows` holds only the newest of
|
|
1468
|
+
* them; a Radar that predates the cap omits this and sends every record. */
|
|
1469
|
+
flowsTotal?: number
|
|
1456
1470
|
warning?: string // Non-fatal warning (e.g., query errors)
|
|
1457
1471
|
/** 'transient' (or absent) means the condition may clear on its own and a
|
|
1458
1472
|
* retry is worthwhile. 'partial' means the flows are correct but some of
|
|
@@ -1463,6 +1477,23 @@ export interface TrafficFlowsResponse {
|
|
|
1463
1477
|
warningKind?: 'transient' | 'partial' | 'incomplete'
|
|
1464
1478
|
}
|
|
1465
1479
|
|
|
1480
|
+
// Response from GET /api/traffic/flows/records — the newest records behind
|
|
1481
|
+
// one graph selection.
|
|
1482
|
+
export interface TrafficRecordsResponse {
|
|
1483
|
+
source: string
|
|
1484
|
+
timestamp: string
|
|
1485
|
+
/** Newest first, at most the server's records limit. */
|
|
1486
|
+
flows: TrafficFlow[]
|
|
1487
|
+
/** How many records matched; more than flows.length when capped. */
|
|
1488
|
+
matched: number
|
|
1489
|
+
l7ResponsesCallerOriented?: boolean
|
|
1490
|
+
coveredSince?: string
|
|
1491
|
+
nodeFlowLimit?: number
|
|
1492
|
+
flowLimit?: number
|
|
1493
|
+
warning?: string
|
|
1494
|
+
warningKind?: 'transient' | 'partial' | 'incomplete'
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1466
1497
|
// Wizard state for traffic setup
|
|
1467
1498
|
export type TrafficWizardState = 'detecting' | 'not_found' | 'wizard' | 'checking' | 'ready'
|
|
1468
1499
|
|