diff --git a/frontend/src/components/common/Resource/RestartButton.stories.tsx b/frontend/src/components/common/Resource/RestartButton.stories.tsx
index 3376f84913f..6c10d052043 100644
--- a/frontend/src/components/common/Resource/RestartButton.stories.tsx
+++ b/frontend/src/components/common/Resource/RestartButton.stories.tsx
@@ -43,6 +43,7 @@ const mockDeployment = new Deployment({
},
spec: {
replicas: 3,
+ selector: { matchLabels: { app: 'headlamp' } },
template: {
spec: {
nodeName: 'mock-node',
diff --git a/frontend/src/components/common/Resource/ScaleButton.stories.tsx b/frontend/src/components/common/Resource/ScaleButton.stories.tsx
index b7408a4ccd7..d5c2c810332 100644
--- a/frontend/src/components/common/Resource/ScaleButton.stories.tsx
+++ b/frontend/src/components/common/Resource/ScaleButton.stories.tsx
@@ -45,6 +45,7 @@ const mockDeployment = new Deployment({
},
spec: {
replicas: 3,
+ selector: { matchLabels: { app: 'headlamp' } },
template: {
spec: {
nodeName: 'mock-node',
diff --git a/frontend/src/components/cronjob/Details.tsx b/frontend/src/components/cronjob/Details.tsx
index 421fb3c5fda..ff3c3f35c09 100644
--- a/frontend/src/components/cronjob/Details.tsx
+++ b/frontend/src/components/cronjob/Details.tsx
@@ -31,6 +31,7 @@ import { useParams } from 'react-router-dom';
import { apply } from '../../lib/k8s/api/v1/apply';
import CronJob from '../../lib/k8s/cronJob';
import Job from '../../lib/k8s/job';
+import { localeDate } from '../../lib/util';
import { clusterAction } from '../../redux/clusterActionSlice';
import { AppDispatch } from '../../redux/stores/store';
import ActionButton from '../common/ActionButton';
@@ -236,6 +237,16 @@ export default function CronJobDetails(props: {
name: t('Schedule'),
value: getSchedule(item, i18n.language),
},
+ {
+ name: t('Time Zone'),
+ value: item.spec.timeZone,
+ hide: !item.spec.timeZone,
+ },
+ {
+ name: t('Concurrency Policy'),
+ value: item.spec.concurrencyPolicy,
+ hide: !item.spec.concurrencyPolicy,
+ },
{
name: t('translation|Suspend'),
value: (item.spec?.suspend ?? false).toString(),
@@ -245,10 +256,32 @@ export default function CronJobDetails(props: {
value: `${item.spec.startingDeadlineSeconds}s`,
hide: !item.spec.startingDeadlineSeconds,
},
+ {
+ name: t('Successful Jobs History Limit'),
+ value: item.spec.successfulJobsHistoryLimit,
+ hide: item.spec.successfulJobsHistoryLimit === undefined,
+ },
+ {
+ name: t('Failed Jobs History Limit'),
+ value: item.spec.failedJobsHistoryLimit,
+ hide: item.spec.failedJobsHistoryLimit === undefined,
+ },
{
name: t('Last Schedule'),
value: getLastScheduleTime(item),
},
+ {
+ name: t('Last Successful Time'),
+ value: item.status?.lastSuccessfulTime
+ ? localeDate(item.status.lastSuccessfulTime)
+ : '',
+ hide: !item.status?.lastSuccessfulTime,
+ },
+ {
+ name: t('Active Jobs'),
+ value: item.status?.active?.length,
+ hide: !item.status?.active?.length,
+ },
]
}
extraSections={cronJob =>
diff --git a/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryAst.stories.storyshot b/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryAst.stories.storyshot
index 1b5f704c419..1e6eb440a8c 100644
--- a/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryAst.stories.storyshot
+++ b/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryAst.stories.storyshot
@@ -223,6 +223,20 @@
*
+
+ Concurrency Policy
+
+
+
+ Allow
+
+
@@ -237,6 +251,26 @@
false
+
+ Successful Jobs History Limit
+
+
+ 3
+
+
+ Failed Jobs History Limit
+
+
+ 1
+
diff --git a/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryMinute.stories.storyshot b/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryMinute.stories.storyshot
index 84edc0605c0..c53302aba23 100644
--- a/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryMinute.stories.storyshot
+++ b/frontend/src/components/cronjob/__snapshots__/CronJobDetails.EveryMinute.stories.storyshot
@@ -223,6 +223,20 @@
* * * * *
+
+ Concurrency Policy
+
+
+
+ Allow
+
+
@@ -237,6 +251,26 @@
false
+
+ Successful Jobs History Limit
+
+
+ 3
+
+
+ Failed Jobs History Limit
+
+
+ 1
+
diff --git a/frontend/src/components/horizontalPodAutoscaler/Details.tsx b/frontend/src/components/horizontalPodAutoscaler/Details.tsx
index ef495be5115..b0803fc2fbd 100644
--- a/frontend/src/components/horizontalPodAutoscaler/Details.tsx
+++ b/frontend/src/components/horizontalPodAutoscaler/Details.tsx
@@ -17,6 +17,7 @@
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import HPA from '../../lib/k8s/hpa';
+import { localeDate } from '../../lib/util';
import Link from '../common/Link';
import { ConditionsSection } from '../common/Resource';
import { DetailsGrid } from '../common/Resource';
@@ -71,6 +72,11 @@ export default function HpaDetails(props: { name?: string; namespace?: string; c
desiredReplicas: item.status.desiredReplicas,
}),
},
+ {
+ name: t('translation|Last Scale Time'),
+ value: item.status.lastScaleTime ? localeDate(item.status.lastScaleTime) : '',
+ hide: !item.status.lastScaleTime,
+ },
]
}
extraSections={item =>
diff --git a/frontend/src/components/horizontalPodAutoscaler/__snapshots__/HPADetails.Default.stories.storyshot b/frontend/src/components/horizontalPodAutoscaler/__snapshots__/HPADetails.Default.stories.storyshot
index 094ba5f50ae..855f2e57d86 100644
--- a/frontend/src/components/horizontalPodAutoscaler/__snapshots__/HPADetails.Default.stories.storyshot
+++ b/frontend/src/components/horizontalPodAutoscaler/__snapshots__/HPADetails.Default.stories.storyshot
@@ -310,12 +310,12 @@
10
Deployment pods
+
+ Last Scale Time
+
+
+
+ 2022-10-21T11:15:12.000Z
+
+
diff --git a/frontend/src/components/ingress/Details.tsx b/frontend/src/components/ingress/Details.tsx
index bff6624f739..8c89f0999a6 100644
--- a/frontend/src/components/ingress/Details.tsx
+++ b/frontend/src/components/ingress/Details.tsx
@@ -217,8 +217,17 @@ export default function IngressDetails(props: {
namespace={namespace}
cluster={cluster}
withEvents
- extraInfo={ingress =>
- ingress && [
+ extraInfo={ingress => {
+ if (!ingress) {
+ return null;
+ }
+ const addresses = ingress.getAddresses();
+ return [
+ {
+ name: t('translation|Address'),
+ value: addresses,
+ hide: !addresses,
+ },
{
name: t('Default Backend'),
value: getDefaultBackend(ingress),
@@ -250,8 +259,8 @@ export default function IngressDetails(props: {
) : null,
},
- ]
- }
+ ];
+ }}
extraSections={item => [
{
id: 'headlamp.ingress-rules',
diff --git a/frontend/src/components/job/List.tsx b/frontend/src/components/job/List.tsx
index 0f5222cd08d..a2442fb1045 100644
--- a/frontend/src/components/job/List.tsx
+++ b/frontend/src/components/job/List.tsx
@@ -114,9 +114,9 @@ export function JobsListRenderer(props: JobsListRendererProps) {
}
function sortByCompletions(job1: Job, job2: Job) {
- const parallelismSorted = job1.spec.parallelism - job2.spec.parallelism;
+ const parallelismSorted = (job1.spec.parallelism ?? 0) - (job2.spec.parallelism ?? 0);
if (parallelismSorted === 0) {
- return job1.spec.completions - job2.spec.completions;
+ return (job1.spec.completions ?? 0) - (job2.spec.completions ?? 0);
}
return parallelismSorted;
}
diff --git a/frontend/src/components/networkpolicy/Details.tsx b/frontend/src/components/networkpolicy/Details.tsx
index 8f7faf6bb95..5181709b877 100644
--- a/frontend/src/components/networkpolicy/Details.tsx
+++ b/frontend/src/components/networkpolicy/Details.tsx
@@ -65,8 +65,8 @@ export function NetworkPolicyDetails(props: {
function PodSelector(props: { networkPolicy: NetworkPolicy }) {
const { networkPolicy } = props;
return prepareMatchLabelsAndExpressions(
- networkPolicy.jsonData.spec?.podSelector?.matchLabels,
- networkPolicy.jsonData.spec?.podSelector?.matchExpressions
+ networkPolicy.spec?.podSelector?.matchLabels,
+ networkPolicy.spec?.podSelector?.matchExpressions
);
}
@@ -243,17 +243,22 @@ export function NetworkPolicyDetails(props: {
name: t('Pod Selector'),
value: ,
},
+ {
+ name: t('Policy Types'),
+ value: item.policyTypes.join(', '),
+ hide: !item.policyTypes.length,
+ },
]
}
extraSections={item =>
item && [
{
id: 'networkpolicy-ingress',
- section: ,
+ section: ,
},
{
id: 'networkpolicy-egress',
- section: ,
+ section: ,
},
]
}
diff --git a/frontend/src/components/networkpolicy/__snapshots__/Details.Default.stories.storyshot b/frontend/src/components/networkpolicy/__snapshots__/Details.Default.stories.storyshot
index dfe7abbd4bd..2e3212e9d7f 100644
--- a/frontend/src/components/networkpolicy/__snapshots__/Details.Default.stories.storyshot
+++ b/frontend/src/components/networkpolicy/__snapshots__/Details.Default.stories.storyshot
@@ -178,12 +178,12 @@
Pod Selector
+
+ Policy Types
+
+
+
+ Ingress, Egress
+
+
diff --git a/frontend/src/components/node/Details.tsx b/frontend/src/components/node/Details.tsx
index 62da23633f6..48bf3f15317 100644
--- a/frontend/src/components/node/Details.tsx
+++ b/frontend/src/components/node/Details.tsx
@@ -18,7 +18,7 @@ import { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
-import _ from 'lodash';
+import { cloneDeep, isEmpty } from 'lodash';
import { useSnackbar } from 'notistack';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -47,7 +47,12 @@ import ActionButton from '../common/ActionButton';
import ConfirmDialog from '../common/ConfirmDialog';
import { StatusLabelProps } from '../common/Label';
import { HeaderLabel, StatusLabel, ValueLabel } from '../common/Label';
-import { ConditionsSection, DetailsGrid, OwnedPodsSection } from '../common/Resource';
+import {
+ ConditionsSection,
+ DetailsGrid,
+ MetadataDictGrid,
+ OwnedPodsSection,
+} from '../common/Resource';
import AuthVisible from '../common/Resource/AuthVisible';
import { SectionBox } from '../common/SectionBox';
import { NameValueTable } from '../common/SimpleTable';
@@ -104,7 +109,7 @@ export default function NodeDetails(props: { name?: string; cluster?: string })
function handleNodeScheduleState(node: Node, cordon: boolean) {
setisUpdatingNodeScheduleProperty(true);
- const cloneNode = _.cloneDeep(node);
+ const cloneNode = cloneDeep(node);
cloneNode.spec.unschedulable = !cordon;
dispatch(
@@ -152,7 +157,7 @@ export default function NodeDetails(props: { name?: string; cluster?: string })
getDrainNodeStatus(cluster, nodeName);
return;
}
- const cloneNode = _.cloneDeep(node);
+ const cloneNode = cloneDeep(node);
cloneNode!.spec.unschedulable = !node!.spec.unschedulable;
setNode(cloneNode);
@@ -277,8 +282,22 @@ export default function NodeDetails(props: { name?: string; cluster?: string })
},
];
}}
- extraInfo={item =>
- item && [
+ extraInfo={item => {
+ if (!item) return [];
+ const roles = item.getRoles();
+ // The keys of interest are reported by the API in kebab-case.
+ const reportedKeys = ['cpu', 'memory', 'pods', 'ephemeral-storage'];
+ const pickResources = (res: { [key: string]: string } = {}) =>
+ Object.fromEntries(reportedKeys.filter(key => res[key]).map(key => [key, res[key]]));
+ const capacity = pickResources(item.status?.capacity);
+ const allocatable = pickResources(item.status?.allocatable);
+
+ return [
+ {
+ name: t('translation|Roles'),
+ value: roles.join(', '),
+ hide: roles.length === 0,
+ },
{
name: t('translation|Taints'),
value: ,
@@ -296,8 +315,18 @@ export default function NodeDetails(props: { name?: string; cluster?: string })
value: item.spec.podCIDR,
},
...getAddresses(item),
- ]
- }
+ {
+ name: t('Capacity'),
+ value: ,
+ hide: isEmpty(capacity),
+ },
+ {
+ name: t('Allocatable'),
+ value: ,
+ hide: isEmpty(allocatable),
+ },
+ ];
+ }}
extraSections={item =>
item && [
{
diff --git a/frontend/src/components/pod/Details.tsx b/frontend/src/components/pod/Details.tsx
index c37eca16f0b..f689874338a 100644
--- a/frontend/src/components/pod/Details.tsx
+++ b/frontend/src/components/pod/Details.tsx
@@ -23,23 +23,26 @@ import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
import { styled } from '@mui/system';
import { Terminal as XTerminal } from '@xterm/xterm';
-import _ from 'lodash';
+import { debounce, isEmpty } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { getDefaultContainer } from '../../helpers/podContainer';
import { KubeContainerStatus } from '../../lib/k8s/cluster';
import Pod from '../../lib/k8s/pod';
+import { localeDate } from '../../lib/util';
import { DefaultHeaderAction } from '../../redux/actionButtonsSlice';
import { EventStatus, HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice';
import { Activity } from '../activity/Activity';
import ActionButton from '../common/ActionButton';
import Link from '../common/Link';
import { LogViewer, LogViewerProps } from '../common/LogViewer';
+import { NameValueTableRow } from '../common/NameValueTable';
import {
ConditionsSection,
ContainersSection,
DetailsGrid,
+ MetadataDictGrid,
VolumeSection,
} from '../common/Resource';
import AuthVisible from '../common/Resource/AuthVisible';
@@ -133,7 +136,7 @@ export function PodLogViewer(props: PodLogViewerProps) {
}
}
- const debouncedSetState = _.debounce(setLogsDebounced, 500, options);
+ const debouncedSetState = debounce(setLogsDebounced, 500, options);
React.useEffect(() => {
const next = getDefaultContainer(item);
if (next && !container) {
@@ -587,11 +590,7 @@ export default function PodDetails(props: PodDetailsProps) {
}, [podItem, launchTerminal, autoLaunchView]);
function prepareExtraInfo(item: Pod | null) {
- let extraInfo: {
- name: string;
- value: React.ReactNode;
- hideLabel?: boolean;
- }[] = [];
+ let extraInfo: (NameValueTableRow & { hideLabel?: boolean })[] = [];
if (item) {
extraInfo = [
{
@@ -672,6 +671,61 @@ export default function PodDetails(props: PodDetailsProps) {
name: t('Priority'),
value: item.spec.priority,
},
+ {
+ name: t('Priority Class'),
+ value: item.spec.priorityClassName ? (
+
+ {item.spec.priorityClassName}
+
+ ) : (
+ ''
+ ),
+ hide: !item.spec.priorityClassName,
+ },
+ {
+ name: t('Runtime Class'),
+ value: item.spec.runtimeClassName,
+ hide: !item.spec.runtimeClassName,
+ },
+ {
+ name: t('Nominated Node'),
+ value: item.status.nominatedNodeName,
+ hide: !item.status.nominatedNodeName,
+ },
+ {
+ name: t('Start Time'),
+ value: item.status.startTime ? localeDate(item.status.startTime) : '',
+ hide: !item.status.startTime,
+ },
+ {
+ name: t('Termination Grace Period'),
+ value:
+ item.spec.terminationGracePeriodSeconds !== undefined
+ ? t('translation|{{ seconds }}s', {
+ seconds: item.spec.terminationGracePeriodSeconds,
+ })
+ : '',
+ hide: item.spec.terminationGracePeriodSeconds === undefined,
+ },
+ {
+ name: t('translation|Reason'),
+ value: item.status.reason,
+ hide: !item.status.reason,
+ },
+ {
+ name: t('translation|Message'),
+ value: item.status.message,
+ hide: !item.status.message,
+ },
+ {
+ name: t('Node Selectors'),
+ value: ,
+ hide: isEmpty(item.spec.nodeSelector),
+ },
];
}
return extraInfo;
diff --git a/frontend/src/components/pod/List.tsx b/frontend/src/components/pod/List.tsx
index f5534eeb344..08ca79d6f25 100644
--- a/frontend/src/components/pod/List.tsx
+++ b/frontend/src/components/pod/List.tsx
@@ -42,7 +42,7 @@ function getPodStatus(pod: Pod) {
if (phase === 'Failed') {
status = 'error';
} else if (phase === 'Succeeded' || phase === 'Running') {
- const readyCondition = pod.status.conditions.find(condition => condition.type === 'Ready');
+ const readyCondition = pod.status.conditions?.find(condition => condition.type === 'Ready');
if (readyCondition?.status === 'True' || phase === 'Succeeded') {
status = 'success';
} else {
diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/ServiceGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/ServiceGlance.tsx
index 4f7704d8aa0..97ee7488905 100644
--- a/frontend/src/components/resourceMap/KubeObjectGlance/ServiceGlance.tsx
+++ b/frontend/src/components/resourceMap/KubeObjectGlance/ServiceGlance.tsx
@@ -36,11 +36,15 @@ export function ServiceGlance({ service }: { service: Service }) {
{t('glossary|External IP')}: {externalIP}
)}
- {service.spec?.ports?.map(it => (
-
- {it.protocol}:{it.port}
-
- ))}
+ {service.spec?.ports?.map(it => {
+ // protocol defaults to TCP per the Kubernetes Service API.
+ const proto = it.protocol ?? 'TCP';
+ return (
+
+ {proto}:{it.port}
+
+ );
+ })}
);
}
diff --git a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
index f721e4367bd..0f7045b30db 100644
--- a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
+++ b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx
@@ -30,7 +30,7 @@ function getPodStatus(pod: Pod): KubeObjectStatus {
if (phase === 'Failed') {
return 'error';
} else if (phase === 'Succeeded' || phase === 'Running') {
- const readyCondition = pod.status.conditions.find(condition => condition.type === 'Ready');
+ const readyCondition = pod.status.conditions?.find(condition => condition.type === 'Ready');
if (readyCondition?.status === 'True' || phase === 'Succeeded') {
return 'success';
} else {
diff --git a/frontend/src/components/resourceMap/sources/definitions/relations.tsx b/frontend/src/components/resourceMap/sources/definitions/relations.tsx
index 5cdbff11cae..3ec4abaec66 100644
--- a/frontend/src/components/resourceMap/sources/definitions/relations.tsx
+++ b/frontend/src/components/resourceMap/sources/definitions/relations.tsx
@@ -187,7 +187,7 @@ const ingressToSecret = makeRelation(Ingress, Secret, (ingress, secret) =>
);
const networkPolicyToPod = makeRelation(NetworkPolicy, Pod, (np, pod) =>
- matchesLabels(np.jsonData.spec.podSelector.matchLabels, pod)
+ matchesLabels(np.spec.podSelector.matchLabels ?? {}, pod)
);
const roleBindingsToRole = makeRelation(
diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx
index 64400bca447..1ebfcaae6c0 100644
--- a/frontend/src/components/service/Details.tsx
+++ b/frontend/src/components/service/Details.tsx
@@ -16,13 +16,13 @@
import { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
-import _ from 'lodash';
+import { isEmpty, isEqual } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import Endpoint from '../../lib/k8s/endpoints';
import EndpointSlice from '../../lib/k8s/endpointSlices';
-import Service from '../../lib/k8s/service';
+import Service, { KubeServicePort } from '../../lib/k8s/service';
import Empty from '../common/EmptyContent';
import { ValueLabel } from '../common/Label';
import Link from '../common/Link';
@@ -71,10 +71,73 @@ export default function ServiceDetails(props: {
name: t('Cluster IP'),
value: item.spec.clusterIP,
},
+ {
+ name: t('Cluster IPs'),
+ value: item.spec.clusterIPs?.join(', '),
+ // Hide when redundant with the Cluster IP row above (single entry, same value).
+ hide:
+ !item.spec.clusterIPs?.length ||
+ isEqual(item.spec.clusterIPs, [item.spec.clusterIP]),
+ },
{
name: t('External IP'),
value: item.getExternalAddresses(),
- hide: _.isEmpty,
+ hide: isEmpty,
+ },
+ {
+ name: t('External Name'),
+ value: item.spec.externalName,
+ hide: isEmpty,
+ },
+ {
+ name: t('IP Families'),
+ value: item.spec.ipFamilies?.join(', '),
+ hide: isEmpty,
+ },
+ {
+ name: t('IP Family Policy'),
+ value: item.spec.ipFamilyPolicy,
+ hide: isEmpty,
+ },
+ {
+ name: t('Session Affinity'),
+ value:
+ item.spec.sessionAffinity === 'ClientIP' &&
+ item.spec.sessionAffinityConfig?.clientIP?.timeoutSeconds
+ ? `${item.spec.sessionAffinity} (${item.spec.sessionAffinityConfig.clientIP.timeoutSeconds}s)`
+ : item.spec.sessionAffinity,
+ // Match kubectl describe: skip the row for the default 'None' affinity.
+ hide: !item.spec.sessionAffinity || item.spec.sessionAffinity === 'None',
+ },
+ {
+ name: t('External Traffic Policy'),
+ value: item.spec.externalTrafficPolicy,
+ hide: isEmpty,
+ },
+ {
+ name: t('Internal Traffic Policy'),
+ value: item.spec.internalTrafficPolicy,
+ hide: isEmpty,
+ },
+ {
+ name: t('Health Check Node Port'),
+ value: item.spec.healthCheckNodePort,
+ hide: value => !value,
+ },
+ {
+ name: t('Load Balancer Class'),
+ value: item.spec.loadBalancerClass,
+ hide: isEmpty,
+ },
+ {
+ name: t('Load Balancer Source Ranges'),
+ value: item.spec.loadBalancerSourceRanges?.join(', '),
+ hide: isEmpty,
+ },
+ {
+ name: t('Traffic Distribution'),
+ value: item.spec.trafficDistribution,
+ hide: isEmpty,
},
{
name: t('Selector'),
@@ -108,6 +171,23 @@ export default function ServiceDetails(props: {
>
),
},
+ ...(item.spec.ports?.some(p => p.nodePort)
+ ? [
+ {
+ label: t('Node Port'),
+ getter: ({ nodePort }: KubeServicePort) =>
+ nodePort ? {nodePort} : '-',
+ },
+ ]
+ : []),
+ ...(item.spec.ports?.some(p => p.appProtocol)
+ ? [
+ {
+ label: t('App Protocol'),
+ getter: ({ appProtocol }: KubeServicePort) => appProtocol ?? '-',
+ },
+ ]
+ : []),
]}
reflectInURL="ports"
/>
diff --git a/frontend/src/components/service/ServiceDetails.stories.tsx b/frontend/src/components/service/ServiceDetails.stories.tsx
index 7058097ceda..bc186200539 100644
--- a/frontend/src/components/service/ServiceDetails.stories.tsx
+++ b/frontend/src/components/service/ServiceDetails.stories.tsx
@@ -78,6 +78,97 @@ const serviceMockWithA8RAnnotations = {
},
};
+/** Service mock for a NodePort with full spec fields */
+const serviceMockNodePort = {
+ ...serviceMock,
+ metadata: {
+ ...serviceMock.metadata,
+ name: 'nodeport-service',
+ },
+ spec: {
+ type: 'NodePort',
+ clusterIP: '10.96.0.200',
+ clusterIPs: ['10.96.0.200'],
+ externalIPs: [],
+ ipFamilies: ['IPv4'],
+ ipFamilyPolicy: 'SingleStack',
+ sessionAffinity: 'ClientIP',
+ sessionAffinityConfig: {
+ clientIP: { timeoutSeconds: 10800 },
+ },
+ externalTrafficPolicy: 'Cluster',
+ internalTrafficPolicy: 'Cluster',
+ ports: [
+ {
+ name: 'http',
+ protocol: 'TCP',
+ port: 80,
+ targetPort: 8080,
+ nodePort: 31080,
+ appProtocol: 'http',
+ },
+ ],
+ selector: { app: 'example' },
+ },
+ status: {},
+};
+
+/** Service mock for a LoadBalancer with health-check and source ranges */
+const serviceMockLoadBalancer = {
+ ...serviceMock,
+ metadata: {
+ ...serviceMock.metadata,
+ name: 'lb-service',
+ },
+ spec: {
+ type: 'LoadBalancer',
+ clusterIP: '10.96.0.300',
+ clusterIPs: ['10.96.0.300', 'fd00::1'],
+ externalIPs: [],
+ ipFamilies: ['IPv4', 'IPv6'],
+ ipFamilyPolicy: 'PreferDualStack',
+ externalTrafficPolicy: 'Local',
+ healthCheckNodePort: 32123,
+ loadBalancerClass: 'example.com/lb',
+ loadBalancerSourceRanges: ['10.0.0.0/8', '192.168.0.0/16'],
+ trafficDistribution: 'PreferClose',
+ sessionAffinity: 'None',
+ ports: [
+ {
+ name: 'https',
+ protocol: 'TCP',
+ port: 443,
+ targetPort: 8443,
+ nodePort: 31443,
+ },
+ ],
+ selector: { app: 'example' },
+ },
+ status: {
+ loadBalancer: {
+ ingress: [{ ip: '203.0.113.10' }],
+ },
+ },
+};
+
+/** Service mock for an ExternalName service */
+const serviceMockExternalName = {
+ ...serviceMock,
+ metadata: {
+ ...serviceMock.metadata,
+ name: 'externalname-service',
+ },
+ spec: {
+ type: 'ExternalName',
+ clusterIP: '',
+ externalIPs: [],
+ externalName: 'api.example.com',
+ selector: {},
+ ports: [],
+ },
+ status: {},
+};
+
/** Service mock with only a8r.io/owner annotation */
const serviceMockWithA8ROwnerOnly = {
...serviceMock,
@@ -182,6 +273,18 @@ const TemplateA8ROwnerOnly: StoryFn = () => {
return ;
};
+const TemplateNodePort: StoryFn = () => {
+ return ;
+};
+
+const TemplateLoadBalancer: StoryFn = () => {
+ return ;
+};
+
+const TemplateExternalName: StoryFn = () => {
+ return ;
+};
+
export const Default = Template.bind({});
Default.parameters = {
msw: {
@@ -315,3 +418,41 @@ WithA8ROwnerOnly.parameters = {
},
},
};
+
+function makeServiceHandlers(name: string, mock: object) {
+ return [
+ http.get('http://localhost:4466/api/v1/namespaces/default/events', () =>
+ HttpResponse.json({ kind: 'EventList', items: [], metadata: {} })
+ ),
+ http.get('http://localhost:4466/api/v1/namespaces/default/services', () =>
+ HttpResponse.error()
+ ),
+ http.get(`http://localhost:4466/api/v1/namespaces/default/services/${name}`, () =>
+ HttpResponse.json(mock)
+ ),
+ http.get('http://localhost:4466/api/v1/namespaces/default/endpoints', () =>
+ HttpResponse.json({ kind: 'List', items: [], metadata: {} })
+ ),
+ http.get(
+ 'http://localhost:4466/apis/discovery.k8s.io/v1/namespaces/default/endpointslices',
+ () => HttpResponse.json({ kind: 'List', items: [], metadata: {} })
+ ),
+ ];
+}
+
+export const NodePort = TemplateNodePort.bind({});
+NodePort.parameters = {
+ msw: { handlers: { story: makeServiceHandlers('nodeport-service', serviceMockNodePort) } },
+};
+
+export const LoadBalancer = TemplateLoadBalancer.bind({});
+LoadBalancer.parameters = {
+ msw: { handlers: { story: makeServiceHandlers('lb-service', serviceMockLoadBalancer) } },
+};
+
+export const ExternalName = TemplateExternalName.bind({});
+ExternalName.parameters = {
+ msw: {
+ handlers: { story: makeServiceHandlers('externalname-service', serviceMockExternalName) },
+ },
+};
diff --git a/frontend/src/components/service/__snapshots__/ServiceDetails.ExternalName.stories.storyshot b/frontend/src/components/service/__snapshots__/ServiceDetails.ExternalName.stories.storyshot
new file mode 100644
index 00000000000..0d0fdfd8f9a
--- /dev/null
+++ b/frontend/src/components/service/__snapshots__/ServiceDetails.ExternalName.stories.storyshot
@@ -0,0 +1,440 @@
+
+
+
+
+
+
+
+
+
+
+ Service: externalname-service
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ externalname-service
+
+
+
+ Namespace
+
+
+
+ default
+
+
+
+ Creation
+
+
+
+ 2022-10-25T11:48:48.000Z
+
+
+
+ Type
+
+
+
+ ExternalName
+
+
+
+ Cluster IP
+
+
+
+
+
+ External Name
+
+
+
+ api.example.com
+
+
+
+ Selector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Endpoint Slices
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/service/__snapshots__/ServiceDetails.LoadBalancer.stories.storyshot b/frontend/src/components/service/__snapshots__/ServiceDetails.LoadBalancer.stories.storyshot
new file mode 100644
index 00000000000..625d7889ef8
--- /dev/null
+++ b/frontend/src/components/service/__snapshots__/ServiceDetails.LoadBalancer.stories.storyshot
@@ -0,0 +1,625 @@
+
+
+
+
+
+
+
+
+
+
+ Service: lb-service
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ lb-service
+
+
+
+ Namespace
+
+
+
+ default
+
+
+
+ Creation
+
+
+
+ 2022-10-25T11:48:48.000Z
+
+
+
+ Type
+
+
+
+ LoadBalancer
+
+
+
+ Cluster IP
+
+
+
+ 10.96.0.300
+
+
+
+ Cluster IPs
+
+
+
+ 10.96.0.300, fd00::1
+
+
+
+ External IP
+
+
+
+ 203.0.113.10
+
+
+
+ IP Families
+
+
+
+ IPv4, IPv6
+
+
+
+ IP Family Policy
+
+
+
+ PreferDualStack
+
+
+
+ External Traffic Policy
+
+
+
+ Local
+
+
+
+ Health Check Node Port
+
+
+ 32123
+
+
+ Load Balancer Class
+
+
+
+ example.com/lb
+
+
+
+ Load Balancer Source Ranges
+
+
+
+ 10.0.0.0/8, 192.168.0.0/16
+
+
+
+ Traffic Distribution
+
+
+
+ PreferClose
+
+
+
+ Selector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Protocol
+
+
+ Name
+
+
+ Ports
+
+
+ Node Port
+
+
+
+
+
+
+ TCP
+
+
+ https
+
+
+
+ 443
+
+
+ 8443
+
+
+
+
+ 31443
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Endpoint Slices
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/service/__snapshots__/ServiceDetails.NodePort.stories.storyshot b/frontend/src/components/service/__snapshots__/ServiceDetails.NodePort.stories.storyshot
new file mode 100644
index 00000000000..862ec47f1bd
--- /dev/null
+++ b/frontend/src/components/service/__snapshots__/ServiceDetails.NodePort.stories.storyshot
@@ -0,0 +1,584 @@
+
+
+
+
+
+
+
+
+
+
+ Service: nodeport-service
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ nodeport-service
+
+
+
+ Namespace
+
+
+
+ default
+
+
+
+ Creation
+
+
+
+ 2022-10-25T11:48:48.000Z
+
+
+
+ Type
+
+
+
+ NodePort
+
+
+
+ Cluster IP
+
+
+
+ 10.96.0.200
+
+
+
+ IP Families
+
+
+
+ IPv4
+
+
+
+ IP Family Policy
+
+
+
+ SingleStack
+
+
+
+ Session Affinity
+
+
+
+ ClientIP (10800s)
+
+
+
+ External Traffic Policy
+
+
+
+ Cluster
+
+
+
+ Internal Traffic Policy
+
+
+
+ Cluster
+
+
+
+ Selector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Protocol
+
+
+ Name
+
+
+ Ports
+
+
+ Node Port
+
+
+ App Protocol
+
+
+
+
+
+
+ TCP
+
+
+ http
+
+
+
+ 80
+
+
+ 8080
+
+
+
+
+ 31080
+
+
+
+ http
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Endpoint Slices
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data to be shown.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/serviceaccount/Details.tsx b/frontend/src/components/serviceaccount/Details.tsx
index 2f362aad945..c35449340eb 100644
--- a/frontend/src/components/serviceaccount/Details.tsx
+++ b/frontend/src/components/serviceaccount/Details.tsx
@@ -21,6 +21,23 @@ import ServiceAccount from '../../lib/k8s/serviceAccount';
import Link from '../common/Link';
import { DetailsGrid } from '../common/Resource';
+function SecretLinks(props: { items?: { name: string }[]; namespace: string }) {
+ const { items, namespace } = props;
+ if (!items?.length) return null;
+ return (
+
+ {items.map(({ name }, index) => (
+
+
+ {name}
+
+ {index !== items.length - 1 && ', '}
+
+ ))}
+
+ );
+}
+
export default function ServiceAccountDetails(props: {
name?: string;
namespace?: string;
@@ -41,18 +58,23 @@ export default function ServiceAccountDetails(props: {
item && [
{
name: t('Secrets'),
- value: (
-
- {item.secrets?.map(({ name }, index) => (
-
-
- {name}
-
- {index !== item.secrets.length - 1 && ','}
-
- ))}
-
- ),
+ value: ,
+ hide: !item.secrets?.length,
+ },
+ {
+ name: t('Image Pull Secrets'),
+ value: ,
+ hide: !item.imagePullSecrets?.length,
+ },
+ {
+ name: t('Automount Service Account Token'),
+ value:
+ item.automountServiceAccountToken === undefined
+ ? ''
+ : item.automountServiceAccountToken
+ ? t('translation|Yes')
+ : t('translation|No'),
+ hide: item.automountServiceAccountToken === undefined,
},
]
}
diff --git a/frontend/src/components/statefulset/Details.tsx b/frontend/src/components/statefulset/Details.tsx
index 3f8ba2a155e..28d7a252618 100644
--- a/frontend/src/components/statefulset/Details.tsx
+++ b/frontend/src/components/statefulset/Details.tsx
@@ -27,6 +27,7 @@ import {
RevisionHistorySection,
RollbackButton,
} from '../common/Resource';
+import { statefulSetExtraInfo } from '../workload/extraInfo';
export default function StatefulSetDetails(props: {
name?: string;
@@ -59,9 +60,11 @@ export default function StatefulSetDetails(props: {
}}
extraInfo={item =>
item && [
+ ...statefulSetExtraInfo(item, t),
{
name: t('Update Strategy'),
- value: item.spec.updateStrategy.type,
+ value: item.spec.updateStrategy?.type,
+ hide: !item.spec.updateStrategy?.type,
},
{
name: t('Selector'),
diff --git a/frontend/src/components/statefulset/__snapshots__/Details.Default.stories.storyshot b/frontend/src/components/statefulset/__snapshots__/Details.Default.stories.storyshot
index 1be0674197a..1e6401558a5 100644
--- a/frontend/src/components/statefulset/__snapshots__/Details.Default.stories.storyshot
+++ b/frontend/src/components/statefulset/__snapshots__/Details.Default.stories.storyshot
@@ -232,6 +232,23 @@
2025-05-31T17:15:53.298Z
+
+ Service Name
+
+
+
+ mock-service
+
+
diff --git a/frontend/src/components/statefulset/__snapshots__/Details.WithComplexSelector.stories.storyshot b/frontend/src/components/statefulset/__snapshots__/Details.WithComplexSelector.stories.storyshot
index 2efe2b5474d..740e7710cbb 100644
--- a/frontend/src/components/statefulset/__snapshots__/Details.WithComplexSelector.stories.storyshot
+++ b/frontend/src/components/statefulset/__snapshots__/Details.WithComplexSelector.stories.storyshot
@@ -232,6 +232,23 @@
2025-05-31T17:15:53.298Z
+
+ Service Name
+
+
+
+ mock-service
+
+
diff --git a/frontend/src/components/statefulset/__snapshots__/Details.WithMultipleContainers.stories.storyshot b/frontend/src/components/statefulset/__snapshots__/Details.WithMultipleContainers.stories.storyshot
index beb9fe047f8..e406b13def7 100644
--- a/frontend/src/components/statefulset/__snapshots__/Details.WithMultipleContainers.stories.storyshot
+++ b/frontend/src/components/statefulset/__snapshots__/Details.WithMultipleContainers.stories.storyshot
@@ -232,6 +232,23 @@
2025-05-31T17:15:53.298Z
+
+ Service Name
+
+
+
+ mock-service
+
+
diff --git a/frontend/src/components/statefulset/__snapshots__/Details.WithOnDeleteStrategy.stories.storyshot b/frontend/src/components/statefulset/__snapshots__/Details.WithOnDeleteStrategy.stories.storyshot
index fd33b71e171..361f830c103 100644
--- a/frontend/src/components/statefulset/__snapshots__/Details.WithOnDeleteStrategy.stories.storyshot
+++ b/frontend/src/components/statefulset/__snapshots__/Details.WithOnDeleteStrategy.stories.storyshot
@@ -232,6 +232,23 @@
2025-05-31T17:15:53.298Z
+
+ Service Name
+
+
+
+ mock-service
+
+
diff --git a/frontend/src/components/storage/ClaimDetails.tsx b/frontend/src/components/storage/ClaimDetails.tsx
index 1c931ba1d26..ce11a6a1bb3 100644
--- a/frontend/src/components/storage/ClaimDetails.tsx
+++ b/frontend/src/components/storage/ClaimDetails.tsx
@@ -22,8 +22,8 @@ import { DetailsGrid } from '../common/Resource';
import { StatusLabelByPhase } from './utils';
export function makePVCStatusLabel(item: PersistentVolumeClaim) {
- const status = item.status!.phase;
- return StatusLabelByPhase(status!);
+ const status = item.status?.phase;
+ return StatusLabelByPhase(status);
}
export default function VolumeClaimDetails(props: {
@@ -48,30 +48,57 @@ export default function VolumeClaimDetails(props: {
name: t('translation|Status'),
value: makePVCStatusLabel(item),
},
+ {
+ name: t('Volume'),
+ value: item.spec?.volumeName ? (
+
+ {item.spec.volumeName}
+
+ ) : (
+ ''
+ ),
+ hide: !item.spec?.volumeName,
+ },
+ {
+ name: t('Requested'),
+ value: item.spec?.resources?.requests?.storage,
+ hide: !item.spec?.resources?.requests?.storage,
+ },
{
name: t('Capacity'),
- value: item.spec!.resources!.requests.storage,
+ value: item.status?.capacity?.storage ?? item.spec?.resources?.requests?.storage,
+ hide: value => !value,
},
{
name: t('Access Modes'),
- value: item.spec!.accessModes!.join(', '),
+ value: (item.status?.accessModes ?? item.spec?.accessModes ?? []).join(', '),
+ hide: value => !value,
},
{
name: t('Volume Mode'),
- value: item.spec!.volumeMode,
+ value: item.spec?.volumeMode,
+ hide: !item.spec?.volumeMode,
},
{
name: t('Storage Class'),
- value: (
+ value: item.spec?.storageClassName ? (
- {item.spec!.storageClassName}
+ {item.spec.storageClassName}
+ ) : (
+ ''
),
+ hide: !item.spec?.storageClassName,
},
]
}
diff --git a/frontend/src/components/storage/ClassDetails.tsx b/frontend/src/components/storage/ClassDetails.tsx
index ed65b2b4caa..94ac207518c 100644
--- a/frontend/src/components/storage/ClassDetails.tsx
+++ b/frontend/src/components/storage/ClassDetails.tsx
@@ -14,11 +14,12 @@
* limitations under the License.
*/
+import { isEmpty } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import StorageClass from '../../lib/k8s/storageClass';
-import { DetailsGrid } from '../common/Resource';
+import { DetailsGrid, MetadataDictGrid } from '../common/Resource';
export default function StorageClassDetails(props: { name?: string; cluster?: string }) {
const params = useParams<{ name: string }>();
@@ -33,6 +34,10 @@ export default function StorageClassDetails(props: { name?: string; cluster?: st
withEvents
extraInfo={item =>
item && [
+ {
+ name: t('Provisioner'),
+ value: item.provisioner,
+ },
{
name: t('Reclaim Policy'),
value: item.reclaimPolicy,
@@ -42,8 +47,23 @@ export default function StorageClassDetails(props: { name?: string; cluster?: st
value: item.volumeBindingMode,
},
{
- name: t('Provisioner'),
- value: item.provisioner,
+ name: t('Default'),
+ value: item.isDefault ? t('translation|Yes') : t('translation|No'),
+ },
+ {
+ name: t('Allow Volume Expansion'),
+ value: item.allowVolumeExpansion ? t('translation|Yes') : t('translation|No'),
+ hide: item.allowVolumeExpansion === undefined,
+ },
+ {
+ name: t('Parameters'),
+ value: ,
+ hide: isEmpty(item.parameters),
+ },
+ {
+ name: t('Mount Options'),
+ value: item.mountOptions?.join(', '),
+ hide: !item.mountOptions?.length,
},
]
}
diff --git a/frontend/src/components/storage/VolumeDetails.stories.tsx b/frontend/src/components/storage/VolumeDetails.stories.tsx
index 3982484b5ba..ccded458c22 100644
--- a/frontend/src/components/storage/VolumeDetails.stories.tsx
+++ b/frontend/src/components/storage/VolumeDetails.stories.tsx
@@ -17,8 +17,8 @@
import { Meta, StoryFn } from '@storybook/react';
import { http, HttpResponse } from 'msw';
import { TestContext } from '../../test';
-import Details from './ClaimDetails';
import { BASE_PV } from './storyHelper';
+import Details from './VolumeDetails';
export default {
title: 'PersistentVolume/DetailsView',
@@ -44,10 +44,10 @@ Base.parameters = {
msw: {
handlers: {
story: [
- http.get('http://localhost:4466/api/v1/persistentvolumeclaims/my-pv', () =>
+ http.get('http://localhost:4466/api/v1/persistentvolumes/my-pv', () =>
HttpResponse.json(BASE_PV)
),
- http.get('http://localhost:4466/api/v1/persistentvolumeclaims', () => HttpResponse.error()),
+ http.get('http://localhost:4466/api/v1/persistentvolumes', () => HttpResponse.error()),
http.get('http://localhost:4466/api/v1/namespaces/default/events', () =>
HttpResponse.json({
kind: 'EventList',
diff --git a/frontend/src/components/storage/VolumeDetails.tsx b/frontend/src/components/storage/VolumeDetails.tsx
index b834e8a8c51..3c23951cfcb 100644
--- a/frontend/src/components/storage/VolumeDetails.tsx
+++ b/frontend/src/components/storage/VolumeDetails.tsx
@@ -14,18 +14,38 @@
* limitations under the License.
*/
+import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
-import PersistentVolume from '../../lib/k8s/persistentVolume';
+import PersistentVolume, { KubeClaimRef } from '../../lib/k8s/persistentVolume';
import Link from '../common/Link';
import { DetailsGrid } from '../common/Resource';
import { StatusLabelByPhase } from './utils';
export function makePVStatusLabel(item: PersistentVolume) {
- const status = item.status!.phase;
+ const status = item.status?.phase;
return StatusLabelByPhase(status);
}
+function renderClaimRef(claimRef: KubeClaimRef, cluster?: string): ReactNode {
+ if (claimRef.kind === 'PersistentVolumeClaim' && claimRef.name && claimRef.namespace) {
+ return (
+
+ {`${claimRef.namespace}/${claimRef.name}`}
+
+ );
+ }
+ if (claimRef.name) {
+ return `${claimRef.namespace ? `${claimRef.namespace}/` : ''}${claimRef.name}`;
+ }
+ return '';
+}
+
export default function VolumeDetails(props: { name?: string; cluster?: string }) {
const params = useParams<{ name: string }>();
const { name = params.name, cluster } = props;
@@ -37,39 +57,74 @@ export default function VolumeDetails(props: { name?: string; cluster?: string }
name={name}
cluster={cluster}
withEvents
- extraInfo={item =>
- item && [
+ extraInfo={item => {
+ if (!item) return [];
+ const { spec, status } = item;
+ const claimRef = spec?.claimRef;
+ const sourceType = item.getSourceType();
+
+ return [
{
name: t('translation|Status'),
value: makePVStatusLabel(item),
},
{
name: t('Capacity'),
- value: item.spec!.capacity.storage,
+ value: spec?.capacity?.storage,
},
{
name: t('Access Modes'),
- value: item.spec!.accessModes.join(', '),
+ value: spec?.accessModes?.join(', '),
+ hide: !spec?.accessModes?.length,
+ },
+ {
+ name: t('Volume Mode'),
+ value: spec?.volumeMode,
+ hide: !spec?.volumeMode,
},
{
name: t('Reclaim Policy'),
- value: item.spec!.persistentVolumeReclaimPolicy,
+ value: spec?.persistentVolumeReclaimPolicy,
+ hide: !spec?.persistentVolumeReclaimPolicy,
},
{
name: t('Storage Class'),
- value: (
+ value: spec?.storageClassName ? (
- {item.spec!.storageClassName}
+ {spec.storageClassName}
+ ) : (
+ ''
),
+ hide: !spec?.storageClassName,
+ },
+ {
+ name: t('Claim'),
+ value: claimRef ? renderClaimRef(claimRef, item.cluster) : '',
+ hide: !claimRef,
+ },
+ {
+ name: t('Source'),
+ value: sourceType,
+ hide: !sourceType,
+ },
+ {
+ name: t('translation|Reason'),
+ value: status?.reason,
+ hide: !status?.reason,
+ },
+ {
+ name: t('translation|Message'),
+ value: status?.message,
+ hide: !status?.message,
},
- ]
- }
+ ];
+ }}
/>
);
}
diff --git a/frontend/src/components/storage/VolumeList.tsx b/frontend/src/components/storage/VolumeList.tsx
index 05bd60661f8..926cb811a7d 100644
--- a/frontend/src/components/storage/VolumeList.tsx
+++ b/frontend/src/components/storage/VolumeList.tsx
@@ -59,7 +59,7 @@ export default function VolumeList() {
{
id: 'capacity',
label: t('Capacity'),
- getValue: volume => volume.spec.capacity.storage,
+ getValue: volume => volume.spec.capacity?.storage,
},
{
id: 'accessModes',
diff --git a/frontend/src/components/storage/__snapshots__/ClaimDetails.Base.stories.storyshot b/frontend/src/components/storage/__snapshots__/ClaimDetails.Base.stories.storyshot
index 57fc4d4ded9..a87132d632c 100644
--- a/frontend/src/components/storage/__snapshots__/ClaimDetails.Base.stories.storyshot
+++ b/frontend/src/components/storage/__snapshots__/ClaimDetails.Base.stories.storyshot
@@ -191,6 +191,37 @@
Bound
+
+ Volume
+
+
+
+ pvc-abc-1234
+
+
+
+ Requested
+
+
+
+ 8Gi
+
+
diff --git a/frontend/src/components/storage/__snapshots__/ClassDetails.Base.stories.storyshot b/frontend/src/components/storage/__snapshots__/ClassDetails.Base.stories.storyshot
index 1f144ec57fa..818bc1862ee 100644
--- a/frontend/src/components/storage/__snapshots__/ClassDetails.Base.stories.storyshot
+++ b/frontend/src/components/storage/__snapshots__/ClassDetails.Base.stories.storyshot
@@ -162,6 +162,20 @@
2023-04-27T20:31:27.000Z
+
+ Provisioner
+
+
+
+ csi.test
+
+
@@ -190,10 +204,24 @@
WaitForFirstConsumer
+
+ Default
+
+
+
+ No
+
+
- Provisioner
+ Allow Volume Expansion
- csi.test
+ Yes
diff --git a/frontend/src/components/storage/__snapshots__/VolumeDetails.Base.stories.storyshot b/frontend/src/components/storage/__snapshots__/VolumeDetails.Base.stories.storyshot
index 06faaf5a34f..6f6afd66559 100644
--- a/frontend/src/components/storage/__snapshots__/VolumeDetails.Base.stories.storyshot
+++ b/frontend/src/components/storage/__snapshots__/VolumeDetails.Base.stories.storyshot
@@ -201,7 +201,9 @@
>
+ >
+ 8Gi
+
Storage Class
+
+ Reason
+
+
+
+ test
+
+
+
+ Message
+
+
+
+ test
+
+
diff --git a/frontend/src/components/storage/utils.tsx b/frontend/src/components/storage/utils.tsx
index 0a9bb7dd44f..aeff4f53763 100644
--- a/frontend/src/components/storage/utils.tsx
+++ b/frontend/src/components/storage/utils.tsx
@@ -16,12 +16,12 @@
import { StatusLabel } from '../common/Label';
-export function StatusLabelByPhase(phase: string) {
+export function StatusLabelByPhase(phase?: string) {
return (
- {phase}
+ {phase ?? ''}
);
}
diff --git a/frontend/src/components/workload/Details.tsx b/frontend/src/components/workload/Details.tsx
index bea441cf220..2b01ebd63c7 100644
--- a/frontend/src/components/workload/Details.tsx
+++ b/frontend/src/components/workload/Details.tsx
@@ -16,8 +16,7 @@
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
-import { WorkloadClass } from '../../lib/k8s/Workload';
-import { Workload } from '../../lib/k8s/Workload';
+import { Workload, WorkloadClass } from '../../lib/k8s/Workload';
import {
ConditionsSection,
ContainersSection,
@@ -28,6 +27,7 @@ import {
RevisionHistorySection,
RollbackButton,
} from '../common/Resource';
+import { KIND_EXTRA_INFO } from './extraInfo';
interface WorkloadDetailsProps {
workloadKind: T;
@@ -130,8 +130,11 @@ export default function WorkloadDetails(props: Workload
return actions;
}}
- extraInfo={item =>
- item && [
+ extraInfo={item => {
+ if (!item) return [];
+ const extraInfoFn = KIND_EXTRA_INFO[workloadKind.kind];
+ const extraRows = extraInfoFn ? extraInfoFn(item, t) : [];
+ return [
{
name: t('Strategy Type'),
value: renderUpdateStrategy(item),
@@ -158,8 +161,9 @@ export default function WorkloadDetails(props: Workload
value: renderReplicas(item),
hide: !showReplicas(item),
},
- ]
- }
+ ...extraRows,
+ ];
+ }}
extraSections={item => {
if (!item) return [];
const sections = [
diff --git a/frontend/src/components/workload/extraInfo.tsx b/frontend/src/components/workload/extraInfo.tsx
new file mode 100644
index 00000000000..673146eeda0
--- /dev/null
+++ b/frontend/src/components/workload/extraInfo.tsx
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TFunction } from 'i18next';
+import Deployment from '../../lib/k8s/deployment';
+import Job from '../../lib/k8s/job';
+import StatefulSet from '../../lib/k8s/statefulSet';
+import { localeDate } from '../../lib/util';
+import Link from '../common/Link';
+import { NameValueTableRow } from '../common/NameValueTable';
+
+export function formatDuration(ms: number): string {
+ if (ms < 0) return '';
+ const seconds = Math.floor(ms / 1000);
+ if (seconds < 60) return `${seconds}s`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m${seconds % 60 ? ` ${seconds % 60}s` : ''}`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h${minutes % 60 ? ` ${minutes % 60}m` : ''}`;
+ const days = Math.floor(hours / 24);
+ return `${days}d${hours % 24 ? ` ${hours % 24}h` : ''}`;
+}
+
+export function deploymentExtraInfo(item: Deployment, t: TFunction): NameValueTableRow[] {
+ const spec = item.spec ?? ({} as Deployment['spec']);
+ return [
+ {
+ name: t('glossary|Min Ready Seconds'),
+ value: spec.minReadySeconds !== undefined ? `${spec.minReadySeconds}s` : '',
+ hide: spec.minReadySeconds === undefined,
+ },
+ {
+ name: t('glossary|Progress Deadline'),
+ value: spec.progressDeadlineSeconds !== undefined ? `${spec.progressDeadlineSeconds}s` : '',
+ hide: spec.progressDeadlineSeconds === undefined,
+ },
+ {
+ name: t('glossary|Revision History Limit'),
+ value: spec.revisionHistoryLimit,
+ hide: spec.revisionHistoryLimit === undefined,
+ },
+ ];
+}
+
+export function statefulSetExtraInfo(item: StatefulSet, t: TFunction): NameValueTableRow[] {
+ const spec = item.spec ?? ({} as StatefulSet['spec']);
+ return [
+ {
+ name: t('glossary|Service Name'),
+ value: spec.serviceName ? (
+
+ {spec.serviceName}
+
+ ) : (
+ ''
+ ),
+ hide: !spec.serviceName,
+ },
+ {
+ name: t('glossary|Pod Management Policy'),
+ value: spec.podManagementPolicy,
+ hide: !spec.podManagementPolicy,
+ },
+ ];
+}
+
+export function jobExtraInfo(item: Job, t: TFunction): NameValueTableRow[] {
+ const status = item.status ?? ({} as Job['status']);
+ const spec = item.spec ?? ({} as Job['spec']);
+ const counts = [
+ status.active !== undefined && t('translation|Active: {{ n }}', { n: status.active }),
+ status.ready !== undefined && t('translation|Ready: {{ n }}', { n: status.ready }),
+ status.succeeded !== undefined && t('translation|Succeeded: {{ n }}', { n: status.succeeded }),
+ status.failed !== undefined && t('translation|Failed: {{ n }}', { n: status.failed }),
+ ]
+ .filter(Boolean)
+ .join(', ');
+ const duration = formatDuration(item.getDuration());
+
+ return [
+ {
+ name: t('glossary|Completions'),
+ value:
+ spec.completions !== undefined
+ ? `${status.succeeded ?? 0}/${spec.completions}`
+ : status.succeeded !== undefined
+ ? `${status.succeeded}`
+ : '',
+ hide: spec.completions === undefined && status.succeeded === undefined,
+ },
+ {
+ name: t('glossary|Parallelism'),
+ value: spec.parallelism,
+ hide: spec.parallelism === undefined,
+ },
+ {
+ name: t('glossary|Completion Mode'),
+ value: spec.completionMode,
+ hide: !spec.completionMode || spec.completionMode === 'NonIndexed',
+ },
+ {
+ name: t('translation|Suspend'),
+ value: spec.suspend !== undefined ? String(spec.suspend) : '',
+ hide: spec.suspend === undefined,
+ },
+ {
+ name: t('glossary|Backoff Limit'),
+ value: spec.backoffLimit,
+ hide: spec.backoffLimit === undefined,
+ },
+ {
+ name: t('glossary|Active Deadline'),
+ value: spec.activeDeadlineSeconds ? `${spec.activeDeadlineSeconds}s` : '',
+ hide: !spec.activeDeadlineSeconds,
+ },
+ {
+ name: t('glossary|TTL After Finished'),
+ value: spec.ttlSecondsAfterFinished ? `${spec.ttlSecondsAfterFinished}s` : '',
+ hide: spec.ttlSecondsAfterFinished === undefined,
+ },
+ {
+ name: t('glossary|Pods Status'),
+ value: counts,
+ hide: !counts,
+ },
+ {
+ name: t('glossary|Start Time'),
+ value: status.startTime ? localeDate(status.startTime) : '',
+ hide: !status.startTime,
+ },
+ {
+ name: t('glossary|Completion Time'),
+ value: status.completionTime ? localeDate(status.completionTime) : '',
+ hide: !status.completionTime,
+ },
+ {
+ name: t('glossary|Duration'),
+ value: duration,
+ hide: !duration,
+ },
+ {
+ name: t('glossary|Completed Indexes'),
+ value: status.completedIndexes,
+ hide: !status.completedIndexes,
+ },
+ ];
+}
+
+export const KIND_EXTRA_INFO: Partial<
+ Record NameValueTableRow[]>
+> = {
+ Deployment: deploymentExtraInfo,
+ StatefulSet: statefulSetExtraInfo,
+ Job: jobExtraInfo,
+};
diff --git a/frontend/src/lib/k8s/cronJob.ts b/frontend/src/lib/k8s/cronJob.ts
index 5a627ef66dc..9de8d395e10 100644
--- a/frontend/src/lib/k8s/cronJob.ts
+++ b/frontend/src/lib/k8s/cronJob.ts
@@ -27,12 +27,13 @@ import { KubeObject, type KubeObjectInterface } from './KubeObject';
*/
export interface KubeCronJob extends KubeObjectInterface {
spec: {
- suspend: boolean;
+ suspend?: boolean;
schedule: string;
+ timeZone?: string;
startingDeadlineSeconds?: number;
- successfulJobsHistoryLimit: number;
- failedJobsHistoryLimit: number;
- concurrencyPolicy: string;
+ successfulJobsHistoryLimit?: number;
+ failedJobsHistoryLimit?: number;
+ concurrencyPolicy?: string;
jobTemplate: {
spec: {
metadata?: Partial;
@@ -47,6 +48,9 @@ export interface KubeCronJob extends KubeObjectInterface {
[otherProps: string]: any;
};
status: {
+ active?: { name: string }[];
+ lastScheduleTime?: string;
+ lastSuccessfulTime?: string;
[otherProps: string]: any;
};
}
diff --git a/frontend/src/lib/k8s/deployment.ts b/frontend/src/lib/k8s/deployment.ts
index 777127c6042..838b1fd651e 100644
--- a/frontend/src/lib/k8s/deployment.ts
+++ b/frontend/src/lib/k8s/deployment.ts
@@ -28,7 +28,7 @@ export type { RollbackResult };
export interface KubeDeployment extends KubeObjectInterface {
spec: {
- selector?: LabelSelector;
+ selector: LabelSelector;
strategy?: {
type: string;
[otherProps: string]: any;
@@ -37,6 +37,9 @@ export interface KubeDeployment extends KubeObjectInterface {
metadata?: KubeMetadata;
spec: KubePodSpec;
};
+ minReadySeconds?: number;
+ progressDeadlineSeconds?: number;
+ revisionHistoryLimit?: number;
[otherProps: string]: any;
};
status: {
diff --git a/frontend/src/lib/k8s/ingress.ts b/frontend/src/lib/k8s/ingress.ts
index 392c95e3728..95d6ff9b002 100644
--- a/frontend/src/lib/k8s/ingress.ts
+++ b/frontend/src/lib/k8s/ingress.ts
@@ -16,6 +16,7 @@
import type { KubeObjectInterface } from './KubeObject';
import { KubeObject } from './KubeObject';
+import type { KubeLoadBalancerIngress } from './service';
interface LegacyIngressRule {
host: string;
@@ -82,6 +83,11 @@ export interface KubeIngress extends KubeObjectInterface {
};
[key: string]: any;
};
+ status?: {
+ loadBalancer?: {
+ ingress?: KubeLoadBalancerIngress[];
+ };
+ };
}
class Ingress extends KubeObject {
@@ -130,6 +136,14 @@ class Ingress extends KubeObject {
return this.jsonData.spec;
}
+ getAddresses(): string {
+ const ingressEntries = this.jsonData.status?.loadBalancer?.ingress ?? [];
+ return ingressEntries
+ .map(entry => entry.hostname || entry.ip)
+ .filter(Boolean)
+ .join(', ');
+ }
+
getHosts() {
return this.spec.rules?.map(({ host }) => host).join(' | ');
}
diff --git a/frontend/src/lib/k8s/job.ts b/frontend/src/lib/k8s/job.ts
index b63e13b8a4b..2a4a46aebdb 100644
--- a/frontend/src/lib/k8s/job.ts
+++ b/frontend/src/lib/k8s/job.ts
@@ -27,9 +27,23 @@ export interface KubeJob extends KubeObjectInterface {
metadata?: KubeMetadata;
spec: KubePodSpec;
};
+ completions?: number;
+ parallelism?: number;
+ completionMode?: string;
+ suspend?: boolean;
+ backoffLimit?: number;
+ activeDeadlineSeconds?: number;
+ ttlSecondsAfterFinished?: number;
[otherProps: string]: any;
};
status: {
+ active?: number;
+ ready?: number;
+ succeeded?: number;
+ failed?: number;
+ startTime?: string;
+ completionTime?: string;
+ completedIndexes?: string;
[otherProps: string]: any;
};
}
diff --git a/frontend/src/lib/k8s/networkpolicy.tsx b/frontend/src/lib/k8s/networkpolicy.tsx
index 79a7dc8b2ea..6128448435c 100644
--- a/frontend/src/lib/k8s/networkpolicy.tsx
+++ b/frontend/src/lib/k8s/networkpolicy.tsx
@@ -46,10 +46,12 @@ export interface NetworkPolicyIngressRule {
}
export interface KubeNetworkPolicy extends KubeObjectInterface {
- egress: NetworkPolicyEgressRule[];
- ingress: NetworkPolicyIngressRule[];
- podSelector: LabelSelector;
- policyTypes: string[];
+ spec: {
+ egress?: NetworkPolicyEgressRule[];
+ ingress?: NetworkPolicyIngressRule[];
+ podSelector: LabelSelector;
+ policyTypes?: string[];
+ };
}
class NetworkPolicy extends KubeObject {
@@ -58,46 +60,56 @@ class NetworkPolicy extends KubeObject {
static apiVersion = 'networking.k8s.io/v1';
static isNamespaced = true;
+ get spec(): KubeNetworkPolicy['spec'] {
+ return this.jsonData.spec;
+ }
+
+ get policyTypes(): string[] {
+ return this.spec?.policyTypes ?? [];
+ }
+
static getBaseObject(): KubeNetworkPolicy {
const baseObject = super.getBaseObject() as KubeNetworkPolicy;
- baseObject.egress = [
- {
- ports: [
- {
- port: 80,
- protocol: 'TCP',
- },
- ],
- to: [
- {
- podSelector: {
- matchLabels: { app: 'headlamp' },
+ baseObject.spec = {
+ egress: [
+ {
+ ports: [
+ {
+ port: 80,
+ protocol: 'TCP',
},
- },
- ],
- },
- ];
- baseObject.ingress = [
- {
- ports: [
- {
- port: 80,
- protocol: 'TCP',
- },
- ],
- from: [
- {
- podSelector: {
- matchLabels: { app: 'headlamp' },
+ ],
+ to: [
+ {
+ podSelector: {
+ matchLabels: { app: 'headlamp' },
+ },
+ },
+ ],
+ },
+ ],
+ ingress: [
+ {
+ ports: [
+ {
+ port: 80,
+ protocol: 'TCP',
+ },
+ ],
+ from: [
+ {
+ podSelector: {
+ matchLabels: { app: 'headlamp' },
+ },
},
- },
- ],
+ ],
+ },
+ ],
+ podSelector: {
+ matchLabels: { app: 'headlamp' },
},
- ];
- baseObject.podSelector = {
- matchLabels: { app: 'headlamp' },
+ policyTypes: ['Ingress', 'Egress'],
};
- baseObject.policyTypes = ['Ingress', 'Egress'];
return baseObject;
}
diff --git a/frontend/src/lib/k8s/node.ts b/frontend/src/lib/k8s/node.ts
index 7b3c3c865dd..3f2f4fb9a5f 100644
--- a/frontend/src/lib/k8s/node.ts
+++ b/frontend/src/lib/k8s/node.ts
@@ -30,22 +30,12 @@ export interface KubeNode extends KubeObjectInterface {
address: string;
type: string;
}[];
- allocatable?: {
- cpu: any;
- memory: any;
- ephemeralStorage: any;
- hugepages_1Gi: any;
- hugepages_2Mi: any;
- pods: any;
- };
- capacity?: {
- cpu: any;
- memory: any;
- ephemeralStorage: any;
- hugepages_1Gi: any;
- hugepages_2Mi: any;
- pods: any;
- };
+ /**
+ * Resource quantities keyed by their k8s name (e.g. cpu, memory, pods, ephemeral-storage).
+ * Note: keys are kebab-case as returned by the API, not camelCase.
+ */
+ allocatable?: { [key: string]: string };
+ capacity?: { [key: string]: string };
conditions?: (Omit & {
lastHeartbeatTime: string;
})[];
@@ -137,6 +127,19 @@ class Node extends KubeObject {
getInternalIP(): string {
return this.status.addresses?.find(address => address.type === 'InternalIP')?.address || '';
}
+
+ /**
+ * Roles derived from the conventional `node-role.kubernetes.io/` labels.
+ *
+ * @see {@link https://kubernetes.io/docs/reference/labels-annotations-taints/#node-role-kubernetes-io}
+ */
+ getRoles(): string[] {
+ const labels = this.metadata?.labels ?? {};
+ const rolePrefix = 'node-role.kubernetes.io/';
+ return Object.keys(labels)
+ .filter(key => key.startsWith(rolePrefix))
+ .map(key => key.slice(rolePrefix.length));
+ }
}
export default Node;
diff --git a/frontend/src/lib/k8s/persistentVolume.ts b/frontend/src/lib/k8s/persistentVolume.ts
index d2240a55399..35c81aec635 100644
--- a/frontend/src/lib/k8s/persistentVolume.ts
+++ b/frontend/src/lib/k8s/persistentVolume.ts
@@ -17,17 +17,62 @@
import type { KubeObjectInterface } from './KubeObject';
import { KubeObject } from './KubeObject';
+export interface KubeClaimRef {
+ apiVersion?: string;
+ kind?: string;
+ name?: string;
+ namespace?: string;
+ uid?: string;
+}
+
+/**
+ * Volume source keys recognized on a PersistentVolume spec, in the order they should be reported.
+ *
+ * @see {@link https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes}
+ */
+export const PV_SOURCE_TYPES = [
+ 'csi',
+ 'hostPath',
+ 'nfs',
+ 'local',
+ 'iscsi',
+ 'cephfs',
+ 'rbd',
+ 'glusterfs',
+ 'awsElasticBlockStore',
+ 'gcePersistentDisk',
+ 'azureDisk',
+ 'azureFile',
+ 'cinder',
+ 'fc',
+ 'flexVolume',
+ 'flocker',
+ 'photonPersistentDisk',
+ 'portworxVolume',
+ 'quobyte',
+ 'scaleIO',
+ 'storageos',
+ 'vsphereVolume',
+] as const;
+
+export type KubePersistentVolumeSourceKey = (typeof PV_SOURCE_TYPES)[number];
+
export interface KubePersistentVolume extends KubeObjectInterface {
spec: {
- capacity: {
- storage: string;
+ capacity?: {
+ storage?: string;
};
+ accessModes?: string[];
+ volumeMode?: string;
+ persistentVolumeReclaimPolicy?: string;
+ storageClassName?: string;
+ claimRef?: KubeClaimRef;
[other: string]: any;
};
status: {
- message: string;
- phase: string;
- reason: string;
+ message?: string;
+ phase?: string;
+ reason?: string;
};
}
@@ -63,6 +108,11 @@ class PersistentVolume extends KubeObject {
get status() {
return this.jsonData.status;
}
+
+ /** First volume-source key set on this PV's spec, e.g. 'csi', 'hostPath'. */
+ getSourceType(): KubePersistentVolumeSourceKey | undefined {
+ return PV_SOURCE_TYPES.find(key => (this.spec as Record)?.[key]);
+ }
}
export default PersistentVolume;
diff --git a/frontend/src/lib/k8s/pod.ts b/frontend/src/lib/k8s/pod.ts
index 63f88951408..07127e39682 100644
--- a/frontend/src/lib/k8s/pod.ts
+++ b/frontend/src/lib/k8s/pod.ts
@@ -29,7 +29,7 @@ export interface KubeVolume {
export interface KubePodSpec {
containers: KubeContainer[];
- nodeName: string;
+ nodeName?: string;
nodeSelector?: {
[key: string]: string;
};
@@ -41,7 +41,10 @@ export interface KubePodSpec {
volumes?: KubeVolume[];
serviceAccountName?: string;
serviceAccount?: string;
- priority?: string;
+ priority?: number;
+ priorityClassName?: string;
+ runtimeClassName?: string;
+ terminationGracePeriodSeconds?: number;
tolerations?: any[];
restartPolicy?: string;
}
@@ -49,18 +52,19 @@ export interface KubePodSpec {
export interface KubePod extends KubeObjectInterface {
spec: KubePodSpec;
status: {
- conditions: KubeCondition[];
- containerStatuses: KubeContainerStatus[];
+ conditions?: KubeCondition[];
+ containerStatuses?: KubeContainerStatus[];
initContainerStatuses?: KubeContainerStatus[];
ephemeralContainerStatuses?: KubeContainerStatus[];
hostIP?: string;
hostIPs?: { ip: string }[];
podIPs?: { ip: string }[];
message?: string;
- phase: string;
+ phase?: string;
qosClass?: string;
reason?: string;
- startTime: Time;
+ nominatedNodeName?: string;
+ startTime?: Time;
[other: string]: any;
};
}
@@ -390,7 +394,7 @@ class Pod extends KubeObject {
let lastRestartDate = new Date(0);
let lastRestartableInitContainerRestartDate = new Date(0);
- let reason = this.status.reason || this.status.phase;
+ let reason = this.status.reason || this.status.phase || '';
const initContainers: Record = {};
let totalContainers = (this.spec.containers ?? []).length;
@@ -467,7 +471,7 @@ class Pod extends KubeObject {
lastRestartDate = lastRestartableInitContainerRestartDate;
let hasRunning = false;
for (let i = (this.status?.containerStatuses?.length || 0) - 1; i >= 0; i--) {
- const container = this.status?.containerStatuses[i];
+ const container = this.status!.containerStatuses![i];
restarts += container.restartCount;
lastRestartDate = this.getLastRestartDate(container, lastRestartDate);
diff --git a/frontend/src/lib/k8s/service.ts b/frontend/src/lib/k8s/service.ts
index 95ea6ce1a6f..bc0c042ee68 100644
--- a/frontend/src/lib/k8s/service.ts
+++ b/frontend/src/lib/k8s/service.ts
@@ -31,18 +31,38 @@ export interface KubeLoadBalancerIngress {
ports?: KubePortStatus[];
}
+export interface KubeServicePort {
+ name?: string;
+ nodePort?: number;
+ port: number;
+ protocol?: string;
+ targetPort?: number | string;
+ appProtocol?: string;
+}
+
export interface KubeService extends KubeObjectInterface {
spec: {
- clusterIP: string;
- ports?: {
- name: string;
- nodePort: number;
- port: number;
- protocol: string;
- targetPort: number | string;
- }[];
- type: string;
- externalIPs: string[];
+ clusterIP?: string;
+ clusterIPs?: string[];
+ ports?: KubeServicePort[];
+ type?: string;
+ externalIPs?: string[];
+ externalName?: string;
+ externalTrafficPolicy?: string;
+ internalTrafficPolicy?: string;
+ healthCheckNodePort?: number;
+ sessionAffinity?: string;
+ sessionAffinityConfig?: {
+ clientIP?: {
+ timeoutSeconds?: number;
+ };
+ };
+ ipFamilies?: string[];
+ ipFamilyPolicy?: string;
+ loadBalancerClass?: string;
+ loadBalancerIP?: string;
+ loadBalancerSourceRanges?: string[];
+ trafficDistribution?: string;
selector: {
[key: string]: string;
};
diff --git a/frontend/src/lib/k8s/serviceAccount.ts b/frontend/src/lib/k8s/serviceAccount.ts
index 55236b6fd6f..ce654cf0f45 100644
--- a/frontend/src/lib/k8s/serviceAccount.ts
+++ b/frontend/src/lib/k8s/serviceAccount.ts
@@ -18,7 +18,7 @@ import type { KubeObjectInterface } from './KubeObject';
import { KubeObject } from './KubeObject';
export interface KubeServiceAccount extends KubeObjectInterface {
- secrets: {
+ secrets?: {
apiVersion: string;
fieldPath: string;
kind: string;
@@ -26,6 +26,8 @@ export interface KubeServiceAccount extends KubeObjectInterface {
namespace: string;
uid: string;
}[];
+ imagePullSecrets?: { name: string }[];
+ automountServiceAccountToken?: boolean;
}
class ServiceAccount extends KubeObject {
@@ -47,6 +49,14 @@ class ServiceAccount extends KubeObject {
get secrets(): KubeServiceAccount['secrets'] {
return this.jsonData.secrets;
}
+
+ get imagePullSecrets(): KubeServiceAccount['imagePullSecrets'] {
+ return this.jsonData.imagePullSecrets;
+ }
+
+ get automountServiceAccountToken(): KubeServiceAccount['automountServiceAccountToken'] {
+ return this.jsonData.automountServiceAccountToken;
+ }
}
export default ServiceAccount;
diff --git a/frontend/src/lib/k8s/statefulSet.ts b/frontend/src/lib/k8s/statefulSet.ts
index 59688748217..7ad8e81e63c 100644
--- a/frontend/src/lib/k8s/statefulSet.ts
+++ b/frontend/src/lib/k8s/statefulSet.ts
@@ -27,11 +27,13 @@ import type { RevisionInfo, RollbackResult } from './rollback';
export interface KubeStatefulSet extends KubeObjectInterface {
spec: {
selector: LabelSelector;
- updateStrategy: {
- rollingUpdate: {
- partition: number;
+ serviceName?: string;
+ podManagementPolicy?: 'OrderedReady' | 'Parallel';
+ updateStrategy?: {
+ rollingUpdate?: {
+ partition?: number;
};
- type: string;
+ type?: string;
};
template: {
metadata?: KubeMetadata;
diff --git a/frontend/src/lib/k8s/storageClass.ts b/frontend/src/lib/k8s/storageClass.ts
index 27b53cade03..a3c1230f357 100644
--- a/frontend/src/lib/k8s/storageClass.ts
+++ b/frontend/src/lib/k8s/storageClass.ts
@@ -19,9 +19,11 @@ import { KubeObject } from './KubeObject';
export interface KubeStorageClass extends KubeObjectInterface {
provisioner: string;
- reclaimPolicy: string;
- volumeBindingMode: string;
+ reclaimPolicy?: string;
+ volumeBindingMode?: string;
allowVolumeExpansion?: boolean;
+ parameters?: { [key: string]: string };
+ mountOptions?: string[];
}
class StorageClass extends KubeObject {
@@ -63,6 +65,14 @@ class StorageClass extends KubeObject {
return this.jsonData.allowVolumeExpansion;
}
+ get parameters() {
+ return this.jsonData.parameters;
+ }
+
+ get mountOptions() {
+ return this.jsonData.mountOptions;
+ }
+
static get listRoute() {
return 'storageClasses';
}
diff --git a/frontend/src/plugin/__snapshots__/pluginLib.snapshot b/frontend/src/plugin/__snapshots__/pluginLib.snapshot
index fc66de18da9..06797cdbed7 100644
--- a/frontend/src/plugin/__snapshots__/pluginLib.snapshot
+++ b/frontend/src/plugin/__snapshots__/pluginLib.snapshot
@@ -420,6 +420,30 @@
"default": [Function],
},
"persistentVolume": {
+ "PV_SOURCE_TYPES": [
+ "csi",
+ "hostPath",
+ "nfs",
+ "local",
+ "iscsi",
+ "cephfs",
+ "rbd",
+ "glusterfs",
+ "awsElasticBlockStore",
+ "gcePersistentDisk",
+ "azureDisk",
+ "azureFile",
+ "cinder",
+ "fc",
+ "flexVolume",
+ "flocker",
+ "photonPersistentDisk",
+ "portworxVolume",
+ "quobyte",
+ "scaleIO",
+ "storageos",
+ "vsphereVolume",
+ ],
"default": [Function],
},
"persistentVolumeClaim": {
@@ -463,6 +487,7 @@
},
"Lodash": Function {
"default": [Function],
+ "module.exports": [Function],
},
"MonacoEditor": "Present",
"MuiCore": "Present",