@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
1,077 lines • 58.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* 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 { Icon } from '@iconify/react';
import Editor from '@monaco-editor/react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton';
import Input from '@mui/material/Input';
import InputLabel from '@mui/material/InputLabel';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/system';
import { Base64 } from 'js-base64';
import { JSONPath } from 'jsonpath-plus';
import _, { ceil, has } from 'lodash';
import { useSnackbar } from 'notistack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useLocation } from 'react-router-dom';
import YAML from 'yaml';
import { labelSelectorToQuery, ResourceClasses, useCluster } from '../../../lib/k8s';
import ConfigMap from '../../../lib/k8s/configMap';
import Job from '../../../lib/k8s/job';
import Pod from '../../../lib/k8s/pod';
import { METRIC_REFETCH_INTERVAL_MS, PodMetrics } from '../../../lib/k8s/PodMetrics';
import Secret from '../../../lib/k8s/secret';
import { createRouteURL } from '../../../lib/router/createRouteURL';
import { getThemeName } from '../../../lib/themes';
import { divideK8sResources } from '../../../lib/units';
import { localeDate, useId } from '../../../lib/util';
import { HeadlampEventType, useEventCallback } from '../../../redux/headlampEventSlice';
import { useTypedSelector } from '../../../redux/hooks';
import { useHasPreviousRoute } from '../../App/RouteSwitcher';
import { SectionBox } from '../../common/SectionBox';
import SimpleTable, { NameValueTable } from '../../common/SimpleTable';
import { DefaultDetailsViewSection, } from '../../DetailsViewSection/detailsViewSectionSlice';
import { JobsListRenderer } from '../../job/List';
import { PodListRenderer } from '../../pod/List';
import { LightTooltip, Loader, ObjectEventList } from '..';
import BackLink from '../BackLink';
import Empty from '../EmptyContent';
import ErrorBoundary from '../ErrorBoundary';
import InnerTable from '../InnerTable';
import { DateLabel, HoverInfoLabel, StatusLabel, ValueLabel } from '../Label';
import Link from '../Link';
import { metadataStyles } from '.';
import A8RInfo from './A8RInfo';
import { MainInfoSection } from './MainInfoSection/MainInfoSection';
import { MainInfoHeader } from './MainInfoSection/MainInfoSectionHeader';
import { MetadataDictGrid, MetadataDisplay } from './MetadataDisplay';
import PortForward from './PortForward';
export { MainInfoSection };
export function ResourceLink(props) {
const { resource, routeName = props.resource.kind, routeParams = { ...props.resource.metadata }, name = props.resource.metadata.name, state, } = props;
if (!!resource.cluster) {
routeParams.cluster = resource.cluster;
}
return (_jsx(Link, { routeName: routeName, params: routeParams, state: state, children: name }));
}
/** Renders the different parts that constibute an actual resource's details view.
* Those are: the back link, the header, the main info section, the extra sections, and the events section.
*/
export function DetailsGrid(props) {
const { sectionsFunc, resourceType, name, namespace, cluster, children, withEvents, extraSections, onResourceUpdate, ...otherMainInfoSectionProps } = props;
const selectedCluster = useCluster();
const { t } = useTranslation();
const location = useLocation();
const hasPreviousRoute = useHasPreviousRoute();
const detailViews = useTypedSelector(state => state.detailsViewSection.detailsViewSections);
const detailViewsProcessors = useTypedSelector(state => state.detailsViewSection.detailsViewSectionsProcessors);
const dispatchHeadlampEvent = useEventCallback();
// This component used to have a MainInfoSection with all these props passed to it, so we're
// using them to accomplish the same behavior.
const { extraInfo, actions, noDefaultActions, headerStyle, backLink, title, headerSection } = otherMainInfoSectionProps;
const [item, error] = resourceType.useGet(name, namespace, {
cluster: cluster ?? selectedCluster ?? undefined,
});
const prevItemRef = React.useRef({});
React.useEffect(() => {
if (item) {
dispatchHeadlampEvent({
type: HeadlampEventType.DETAILS_VIEW,
data: {
title: item?.jsonData.kind,
resource: item,
error: error || undefined,
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item]);
React.useEffect(() => {
// We cannot call this callback more than once on each version of the item, in order to avoid
// infinite loops.
const prevItem = prevItemRef.current;
if (prevItem?.uid === item?.metadata?.uid &&
prevItem?.version === item?.metadata?.resourceVersion &&
error === prevItem.error) {
return;
}
prevItemRef.current = {
uid: item?.metadata?.uid,
version: item?.metadata?.resourceVersion,
error,
};
onResourceUpdate?.(item, error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, error]);
const actualBackLink = React.useMemo(() => {
if (!!backLink || backLink === '') {
return backLink;
}
const stateLink = location.state?.backLink || null;
if (!!stateLink) {
return generatePath(stateLink.pathname);
}
if (!!hasPreviousRoute) {
// Will make it go back to the previous route
return '';
}
let route;
if (!!item) {
route = item.listRoute;
}
else {
try {
route = new resourceType({}).listRoute;
}
catch (err) {
console.error(`Error creating route for details grid (resource type=${resourceType}): ${err}`);
// Let the MainInfoSection handle it.
return undefined;
}
}
return createRouteURL(route);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item]);
const sections = [];
// Back link
if (!!actualBackLink || actualBackLink === '') {
sections.push({
id: DefaultDetailsViewSection.BACK_LINK,
section: _jsx(BackLink, { to: actualBackLink }),
});
}
// Title / Header
sections.push({
id: DefaultDetailsViewSection.MAIN_HEADER,
section: (_jsx(MainInfoHeader, { title: title, resource: item, actions: actions, noDefaultActions: noDefaultActions, headerStyle: headerStyle })),
});
// Error / Loading or Metadata
if (item === null) {
sections.push(!!error
? {
id: DefaultDetailsViewSection.ERROR,
section: (_jsx(Paper, { variant: "outlined", children: _jsx(Empty, { color: "error", children: error.toString() }) })),
}
: {
id: DefaultDetailsViewSection.LOADING,
section: _jsx(Loader, { title: t('translation|Loading resource data') }),
});
}
else {
const mainInfoHeader = typeof headerSection === 'function' ? headerSection(item) : headerSection;
sections.push({
id: DefaultDetailsViewSection.METADATA,
section: (_jsxs(SectionBox, { "aria-busy": item === null, "aria-live": "polite", children: [mainInfoHeader, _jsx(MetadataDisplay, { resource: item, extraRows: extraInfo })] })),
});
}
// a8r.io Metadata section — rendered for any resource that carries a8r.io/* annotations
if (item) {
const annotations = item.metadata?.annotations ?? {};
const hasA8r = Object.keys(annotations).some(key => key.startsWith('a8r.io/'));
if (hasA8r) {
sections.push({
id: 'headlamp.a8r-info',
section: (_jsx(SectionBox, { title: t('a8r.io Metadata'), children: _jsx(A8RInfo, { annotations: annotations }) })),
});
}
}
// Other sections
if (!!sectionsFunc) {
console.info(`Using legacy sectionsFunc in DetailsGrid for ${title || resourceType + '/' + namespace + '/' + name}. Please use the children, or set up a details view processor.`);
sections.push({
id: 'LEGACY_SECTIONS_FUNC',
section: sectionsFunc(item),
});
}
if (!!extraSections) {
let actualExtraSections = [];
if (Array.isArray(extraSections)) {
actualExtraSections = extraSections;
}
else if (typeof extraSections === 'function') {
const extraSectionsResult = extraSections(item) || [];
if (Array.isArray(extraSectionsResult)) {
actualExtraSections = extraSectionsResult;
}
}
sections.push(...actualExtraSections);
}
// Children
if (!!children) {
sections.push({
id: DefaultDetailsViewSection.CHILDREN,
section: children,
});
}
// Plugin appended details views
if (!!detailViews) {
sections.push(...detailViews);
}
// Events
if (withEvents && item) {
sections.push({
id: DefaultDetailsViewSection.EVENTS,
section: _jsx(ObjectEventList, { object: item }),
});
}
let sectionsProcessed = [...sections];
for (const detailViewsProcessor of detailViewsProcessors) {
let processorsSections = sectionsProcessed;
try {
processorsSections = detailViewsProcessor.processor(item, sectionsProcessed);
if (!Array.isArray(processorsSections)) {
throw new Error(`Invalid return value: ${processorsSections}`);
}
}
catch (err) {
console.error(`Error processing details view sections for ${resourceType}/${namespace}/${name}: ${err}`);
continue;
}
sectionsProcessed = processorsSections;
}
return (_jsx(PageGrid, { sx: theme => ({
marginBottom: theme.spacing(2),
}), children: React.Children.toArray(sectionsProcessed.map(section => {
const Section = has(section, 'section')
? section.section
: section;
if (React.isValidElement(Section)) {
return _jsx(ErrorBoundary, { children: Section });
}
else if (Section === null) {
return null;
}
else if (typeof Section === 'function') {
return (_jsx(ErrorBoundary, { children: _jsx(Section, { resource: item }) }));
}
})) }));
}
export function PageGrid(props) {
const { sections = [], children = [], ...other } = props;
const childrenArray = React.Children.toArray(children).concat(React.Children.toArray(sections));
return (_jsx(Grid, { container: true, spacing: 1, justifyContent: "flex-start", alignItems: "stretch", ...other, children: childrenArray.map((section, i) => (_jsx(Grid, { item: true, xs: 12, children: _jsx(Box, { mt: [4, 0, 0], children: section }) }, i))) }));
}
export function SectionGrid(props) {
const { items } = props;
return (_jsx(Grid, { container: true, justifyContent: "space-between", children: items.map((item, i) => {
return (_jsx(Grid, { item: true, md: 12, xs: 12, children: item }, i));
}) }));
}
export function DataField(props) {
const { disableLabel, label, value, onSave, onChange } = props;
// Make sure we reload after a theme change
useTheme();
const themeName = getThemeName();
const [data, setData] = React.useState(value);
const handleChange = (newValue) => {
if (newValue !== undefined) {
setData(newValue);
onChange?.(newValue);
}
};
function handleEditorDidMount(editor) {
const editorElement = editor.getDomNode();
if (!editorElement) {
return;
}
const lineCount = editor.getModel()?.getLineCount() || 1;
if (lineCount < 2) {
editorElement.style.height = '3vh';
}
else if (lineCount <= 10) {
editorElement.style.height = '10vh';
}
else {
editorElement.style.height = '40vh';
}
editor.layout();
}
let language = label.split('.').pop();
if (language !== 'json') {
language = 'yaml';
}
const editorComponent = (_jsx(Editor, { value: data, language: language, onChange: handleChange, onMount: handleEditorDidMount, options: { lineNumbers: 'off', automaticLayout: true }, theme: themeName === 'dark' ? 'vs-dark' : 'light' }));
const content = (_jsxs(Box, { borderTop: 0, border: 1, children: [!disableLabel && (_jsxs(Box, { display: "flex", children: [_jsx(Box, { width: "10%", borderTop: 1, height: '1px' }), _jsx(Box, { pb: 1, mt: -1, px: 0.5, children: _jsx(InputLabel, { children: label }) }), _jsx(Box, { width: "100%", borderTop: 1, height: '1px' })] })), _jsx(Box, { mt: 1, px: 1, pb: 1, children: editorComponent })] }));
return (_jsxs(Box, { children: [content, onSave && (_jsx(Box, { mt: 1, display: "flex", justifyContent: "flex-end", children: _jsx(Button, { variant: "contained", color: "primary", onClick: () => onSave && onSave(data), children: "Save" }) }))] }));
}
export function SecretField(props) {
const { value, nameID, ...other } = props;
const [showPassword, setShowPassword] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const { t } = useTranslation();
const secret = String(value ?? '');
function handleClickShowPassword() {
setShowPassword(!showPassword);
}
async function onCopy() {
if (!secret) {
return;
}
try {
// Copy the decoded value
await navigator.clipboard.writeText(Base64.decode(secret));
setCopied(true);
setTimeout(() => setCopied(false), 1200);
}
catch (err) {
console.error(`Error copying secret to clipboard: ${err}`);
}
}
const tooltipTitle = copied ? t('translation|Copied') : t('translation|Copy to clipboard');
const copyButton = (_jsx(IconButton, { edge: "start", "aria-label": t('translation|Copy to clipboard'), onClick: onCopy, onMouseDown: e => e.preventDefault(), size: "medium", disabled: !secret, children: _jsx(Icon, { icon: 'mdi:content-copy' }) }));
return (_jsxs(Grid, { container: true, alignItems: !!other?.disableUnderline ? 'center' : 'stretch', spacing: 2, children: [_jsxs(Grid, { item: true, children: [!!secret ? _jsx(LightTooltip, { title: tooltipTitle, children: copyButton }) : copyButton, _jsx(IconButton, { edge: "end", "aria-label": t('glossary|toggle field visibility'), onClick: handleClickShowPassword, onMouseDown: event => event.preventDefault(), size: "medium", children: _jsx(Icon, { icon: showPassword ? 'mdi:eye-off' : 'mdi:eye' }) })] }), _jsx(Grid, { item: true, xs: true, children: _jsx(Input, { "aria-labelledby": nameID, readOnly: !showPassword, type: "password", fullWidth: true, multiline: showPassword, maxRows: "20", value: showPassword ? Base64.decode(value) : '••••••••', ...other }) })] }));
}
export function ConditionsTable(props) {
const { resource, showLastUpdate = true } = props;
const { t } = useTranslation(['glossary', 'translation']);
function makeStatusLabel(condition) {
let status = '';
if (condition.type === 'Available') {
status = condition.status === 'True' ? 'success' : 'error';
}
return _jsx(StatusLabel, { status: status, children: condition.type });
}
function getColumns() {
const cols = [
{
label: t('Condition'),
getter: makeStatusLabel,
},
{
label: t('translation|Status'),
getter: condition => condition.status,
},
{
label: t('Last Transition'),
getter: condition => _jsx(DateLabel, { date: condition.lastTransitionTime }),
},
{
label: t('Last Update'),
getter: condition => condition.lastUpdateTime ? _jsx(DateLabel, { date: condition.lastUpdateTime }) : '-',
hide: !showLastUpdate,
},
{
label: t('translation|Reason'),
getter: condition => condition.reason ? (_jsx(HoverInfoLabel, { label: condition.reason, hoverInfo: condition.message })) : ('-'),
},
];
// Allow to filter the columns by using a hide field
return cols.filter(col => !col.hide);
}
return (_jsx(SimpleTable, { data: (resource && resource.status && resource.status.conditions) || [], columns: getColumns() }));
}
/**
* Extracts all environment variable references from a container spec.
* This is a pure function with no hooks.
*/
function extractEnvVarReferences(container) {
const refs = [];
// Process env variables
container?.env?.forEach(item => {
if (item.value) {
refs.push({ name: item.name, type: 'value', value: item.value });
}
else if (item.valueFrom) {
const vf = item.valueFrom;
if (vf.secretKeyRef) {
refs.push({
name: item.name,
type: 'secret',
resourceName: vf.secretKeyRef.name,
key: vf.secretKeyRef.key,
optional: vf.secretKeyRef.optional,
});
}
else if (vf.configMapKeyRef) {
refs.push({
name: item.name,
type: 'configMap',
resourceName: vf.configMapKeyRef.name,
key: vf.configMapKeyRef.key,
optional: vf.configMapKeyRef.optional,
});
}
else if (vf.fieldRef) {
refs.push({
name: item.name,
type: 'field',
fieldPath: vf.fieldRef.fieldPath,
});
}
else if (vf.resourceFieldRef) {
refs.push({
name: item.name,
type: 'resourceField',
resource: vf.resourceFieldRef.resource,
containerName: vf.resourceFieldRef.containerName,
divisor: vf.resourceFieldRef.divisor,
});
}
}
});
// Process envFrom
container?.envFrom?.forEach(item => {
if (item.secretRef) {
refs.push({
name: item.secretRef.name,
type: 'secretRef',
resourceName: item.secretRef.name,
optional: item.secretRef.optional,
prefix: item.prefix,
});
}
else if (item.configMapRef) {
refs.push({
name: item.configMapRef.name,
type: 'configMapRef',
resourceName: item.configMapRef.name,
optional: item.configMapRef.optional,
prefix: item.prefix,
});
}
});
return refs;
}
/**
* Component that fetches a Secret and reports the result.
* This properly calls hooks at the top level.
*/
function SecretFetcher(props) {
const { name, namespace, onResult } = props;
const [secret, error] = Secret.useGet(name, namespace);
React.useEffect(() => {
// Only call onResult when we have a definitive result (either data or error)
if (secret || error) {
onResult(name, secret, error);
}
}, [secret, error, name, onResult]);
return null;
}
/**
* Component that fetches a ConfigMap and reports the result.
* This properly calls hooks at the top level.
*/
function ConfigMapFetcher(props) {
const { name, namespace, onResult } = props;
const [configMap, error] = ConfigMap.useGet(name, namespace);
React.useEffect(() => {
if (configMap || error) {
onResult(name, configMap, error);
}
}, [configMap, error, name, onResult]);
return null;
}
/**
* Builds environment variables from references and fetched resources.
* This is a pure function with no hooks.
*/
function buildEnvironmentVariables(references, fetchedSecrets, fetchedConfigMaps, pod, container, containerStartTimestamp) {
const variables = new Map();
// Helper to compare timestamps
const isOutOfSync = (resourceTimestamp) => {
if (!resourceTimestamp || !containerStartTimestamp)
return false;
return new Date(resourceTimestamp).getTime() > new Date(containerStartTimestamp).getTime();
};
references.forEach(ref => {
switch (ref.type) {
case 'value':
variables.set(ref.name, {
value: ref.value,
from: 'manifest',
isSecret: false,
isError: false,
isOutOfSync: false,
});
break;
case 'secret': {
const fetched = fetchedSecrets.get(ref.resourceName);
if (!fetched)
break; // Still loading
const { resource: secret, error } = fetched;
if (error) {
if (error.status === 404 && ref.optional)
break;
variables.set(ref.name, {
value: error.message,
from: secret,
isError: true,
isSecret: false,
isOutOfSync: false,
});
}
else if (secret) {
const secretData = secret.data || {};
const value = secretData[ref.key] ? atob(secretData[ref.key]) : '';
variables.set(ref.name, {
value,
from: secret,
isError: false,
isSecret: true,
isOutOfSync: isOutOfSync(secret.metadata?.creationTimestamp),
});
}
break;
}
case 'configMap': {
const fetched = fetchedConfigMaps.get(ref.resourceName);
if (!fetched)
break;
const { resource: configMap, error } = fetched;
if (error) {
if (error.status === 404 && ref.optional)
break;
variables.set(ref.name, {
value: error.message,
from: configMap,
isError: true,
isSecret: false,
isOutOfSync: false,
});
}
else if (configMap) {
const configMapData = configMap.data || {};
variables.set(ref.name, {
value: configMapData[ref.key] || '',
from: configMap,
isError: false,
isSecret: false,
isOutOfSync: isOutOfSync(configMap.metadata?.creationTimestamp),
});
}
break;
}
case 'secretRef': {
const fetched = fetchedSecrets.get(ref.resourceName);
if (!fetched)
break;
const { resource: secret, error } = fetched;
const prefix = ref.prefix || '';
if (error) {
if (error.status === 404 && ref.optional)
break;
variables.set(`${prefix}${ref.resourceName}`, {
value: error.message,
from: secret,
isError: true,
isSecret: false,
isOutOfSync: false,
});
}
else if (secret) {
const secretData = secret.data || {};
const outOfSync = isOutOfSync(secret.metadata?.creationTimestamp);
Object.entries(secretData).forEach(([key, value]) => {
variables.set(`${prefix}${key}`, {
value: atob(value),
from: secret,
isError: false,
isSecret: true,
isOutOfSync: outOfSync,
});
});
}
break;
}
case 'configMapRef': {
const fetched = fetchedConfigMaps.get(ref.resourceName);
if (!fetched)
break;
const { resource: configMap, error } = fetched;
const prefix = ref.prefix || '';
if (error) {
if (error.status === 404 && ref.optional)
break;
variables.set(`${prefix}${ref.resourceName}`, {
value: error.message,
from: configMap,
isError: true,
isSecret: false,
isOutOfSync: false,
});
}
else if (configMap) {
const configMapData = configMap.data || {};
const outOfSync = isOutOfSync(configMap.metadata?.creationTimestamp);
Object.entries(configMapData).forEach(([key, value]) => {
variables.set(`${prefix}${key}`, {
value: value,
from: configMap,
isError: false,
isSecret: false,
isOutOfSync: outOfSync,
});
});
}
break;
}
case 'field': {
let value;
let isError = false;
try {
const result = JSONPath({ path: '$.' + ref.fieldPath, json: pod });
value = Array.isArray(result) ? result[0] : result;
if (value === undefined) {
value = '';
}
else if (typeof value !== 'string') {
value = JSON.stringify(value);
}
}
catch (err) {
isError = true;
value = err instanceof Error ? `Error: ${err.message}` : 'Unknown error';
}
variables.set(ref.name, {
value,
from: `fieldRef: ${ref.fieldPath}`,
isSecret: false,
isError,
isOutOfSync: false,
});
break;
}
case 'resourceField': {
let value = '';
let isError = false;
const containerName = ref.containerName || container.name;
const resourceType = ref.resource;
let divisor = ref.divisor || '1';
if (divisor === '0') {
divisor = '1';
}
try {
const allContainers = [
...(pod.spec?.containers || []),
...(pod.spec?.initContainers || []),
...(pod.spec?.ephemeralContainers || []),
];
const templateSpec = pod.spec?.template?.spec;
if (templateSpec) {
allContainers.push(...(templateSpec.containers || []), ...(templateSpec.initContainers || []), ...(templateSpec.ephemeralContainers || []));
}
const jobTemplateSpec = pod.spec?.jobTemplate?.spec?.template?.spec;
if (jobTemplateSpec) {
allContainers.push(...(jobTemplateSpec.containers || []), ...(jobTemplateSpec.initContainers || []), ...(jobTemplateSpec.ephemeralContainers || []));
}
const targetContainer = allContainers.find(c => c.name === containerName);
if (!targetContainer) {
throw new Error(`Container ${containerName} not found`);
}
const [category, type] = resourceType.split('.');
const resourceValue = targetContainer.resources?.[category]?.[type];
if (!resourceValue) {
throw new Error(`Resource ${resourceType} not found for container ${containerName}`);
}
value = `${ceil(divideK8sResources(resourceValue, divisor, type))}`;
}
catch (err) {
isError = true;
if (err instanceof Error) {
value = err.message;
}
else {
value = 'Unknown error occurred.';
}
}
variables.set(ref.name, {
value,
from: `resourceFieldRef: ${containerName}.${resourceType} / ${divisor}`,
isSecret: false,
isError,
isOutOfSync: false,
});
break;
}
}
});
return Array.from(variables.entries())
.map(([key, value]) => ({ key, ...value }))
.sort((a, b) => a.key.localeCompare(b.key));
}
/**
* Displays environment variables for a container, fetching values from
* Secrets and ConfigMaps as needed.
*/
export function ContainerEnvironmentVariables(props) {
const { pod, container } = props;
const { t } = useTranslation();
const { enqueueSnackbar } = useSnackbar();
// State to store fetched resources
const [fetchedSecrets, setFetchedSecrets] = React.useState(new Map());
const [fetchedConfigMaps, setFetchedConfigMaps] = React.useState(new Map());
// Early return if no env vars
if ((!container?.env && !container?.envFrom) ||
!pod?.status?.containerStatuses ||
!pod?.metadata?.namespace) {
return null;
}
const namespace = pod.metadata.namespace;
const containerStartTimestamp = (() => {
let timestamp = pod.metadata?.creationTimestamp;
const containerStatus = pod.status?.containerStatuses?.find(c => c.name === container?.name);
if (containerStatus?.started && containerStatus.state?.running?.startedAt) {
timestamp = containerStatus.state.running.startedAt;
}
return timestamp;
})();
// Extract all references upfront (pure function, no hooks)
const references = extractEnvVarReferences(container);
// Get unique resource names to fetch
// eslint-disable-next-line react-hooks/rules-of-hooks
const secretsToFetch = React.useMemo(() => {
const secrets = new Set();
references.forEach(ref => {
if ((ref.type === 'secret' || ref.type === 'secretRef') && ref.resourceName) {
secrets.add(ref.resourceName);
}
});
return Array.from(secrets);
}, [references]);
// eslint-disable-next-line react-hooks/rules-of-hooks
const configMapsToFetch = React.useMemo(() => {
const configMaps = new Set();
references.forEach(ref => {
if ((ref.type === 'configMap' || ref.type === 'configMapRef') && ref.resourceName) {
configMaps.add(ref.resourceName);
}
});
return Array.from(configMaps);
}, [references]);
// Callbacks to handle fetched resources
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleSecretFetched = React.useCallback((name, resource, error) => {
setFetchedSecrets(prev => {
const next = new Map(prev);
next.set(name, { resource, error });
return next;
});
}, []);
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleConfigMapFetched = React.useCallback((name, resource, error) => {
setFetchedConfigMaps(prev => {
const next = new Map(prev);
next.set(name, { resource, error });
return next;
});
}, []);
// Copy handler using notistack
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleCopy = React.useCallback((text) => {
navigator.clipboard.writeText(text).then(() => enqueueSnackbar(t('translation|Copied'), { variant: 'success' }), err => console.error('Failed to copy: ', err));
}, [enqueueSnackbar, t]);
// Build variables from fetched resources
const variables = buildEnvironmentVariables(references, fetchedSecrets, fetchedConfigMaps, pod, container, containerStartTimestamp);
// Define columns for the table
const columns = [
{
label: t('translation|Name'),
getter: (data) => {
return (_jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(StatusLabel, { status: "", sx: { fontFamily: 'monospace' }, children: data.key }), data.isOutOfSync && (_jsx(Box, { "aria-label": "hidden", display: "flex", alignItems: "center", px: 1, children: _jsx(HoverInfoLabel, { label: "", "aria-label": "error", icon: "mdi:alert-outline", hoverInfo: t('translation|This value may differ in the container, since the pod is older than {{from}}', {
from: (() => {
if (typeof data.from === 'object' && data.from !== null) {
const o = data.from;
return `${o.kind} ${o.metadata.name}`;
}
return 'the referenced object';
})(),
}) }) }))] }));
},
},
{
label: t('translation|Value'),
getter: (data) => {
if (data.isError) {
return (_jsxs(Box, { display: "flex", alignItems: "center", px: 1, children: [_jsx(Icon, { icon: "mdi:alert-outline", "aria-label": "error" }), _jsx(Typography, { color: "error", sx: { marginLeft: 1 }, children: data.value })] }));
}
return (_jsx(Box, { display: "flex", alignItems: "center", children: data.isSecret ? (_jsx(SecretField, { disableUnderline: true, value: btoa(data.value), sx: { fontFamily: 'monospace' } })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { edge: "end", "aria-label": t('translation|Copy'), onClick: () => handleCopy(data.value), onMouseDown: event => event.preventDefault(), size: "medium", children: _jsx(Icon, { icon: "mdi:content-copy" }) }), _jsx(Typography, { sx: { fontFamily: 'monospace', ml: 1 }, children: data.value })] })) }));
},
},
{
label: t('translation|From'),
getter: (data) => {
if (typeof data.from === 'object' && data.from !== null) {
let routeName;
try {
routeName =
ResourceClasses[data.from?.kind].detailsRoute;
}
catch (e) {
console.error(`Error getting routeName for ${data.from?.kind}`, e);
return null;
}
return (_jsx(Link, { routeName: routeName, params: {
name: data.from?.metadata?.name,
namespace: data.from?.metadata?.namespace,
}, children: `${data.from?.kind}: ${data.from?.metadata?.name}` }));
}
else if (typeof data.from === 'string') {
return data.from;
}
return null;
},
},
];
return (_jsxs(_Fragment, { children: [secretsToFetch.map(name => (_jsx(SecretFetcher, { name: name, namespace: namespace, onResult: handleSecretFetched }, `secret-${name}`))), configMapsToFetch.map(name => (_jsx(ConfigMapFetcher, { name: name, namespace: namespace, onResult: handleConfigMapFetched }, `configmap-${name}`))), _jsx(InnerTable, { columns: columns, data: variables })] }));
}
export function VolumeMounts(props) {
const { mounts } = props;
const { t } = useTranslation();
if (!mounts) {
return null;
}
return (_jsx(InnerTable, { columns: [
{
label: t('translation|Mount Path'),
getter: (data) => data.mountPath,
},
{
label: t('translation|from'),
getter: (data) => data.name,
},
{
label: t('translation|I/O'),
getter: (data) => (data.readOnly ? 'ReadOnly' : 'ReadWrite'),
},
], data: mounts }));
}
export function LivenessProbes(props) {
const { liveness } = props;
function LivenessProbeItem(props) {
return props.children ? (_jsx(Box, { p: 0.5, children: _jsx(Typography, { sx: metadataStyles, display: "inline", children: props.children }) })) : null;
}
return (_jsxs(Box, { display: "flex", flexDirection: "column", children: [_jsx(LivenessProbeItem, { children: `http-get, path: ${liveness?.httpGet?.path}, port: ${liveness?.httpGet?.port},
scheme: ${liveness?.httpGet?.scheme}` }), _jsx(LivenessProbeItem, { children: liveness?.exec?.command && `exec[${liveness?.exec?.command.join(' ')}]` }), _jsx(LivenessProbeItem, { children: liveness?.successThreshold && `success = ${liveness?.successThreshold}` }), _jsx(LivenessProbeItem, { children: liveness?.failureThreshold && `failure = ${liveness?.failureThreshold}` }), _jsx(LivenessProbeItem, { children: liveness?.initialDelaySeconds && `delay = ${liveness?.initialDelaySeconds}s` }), _jsx(LivenessProbeItem, { children: liveness?.timeoutSeconds && `timeout = ${liveness?.timeoutSeconds}s` }), _jsx(LivenessProbeItem, { children: liveness?.periodSeconds && `period = ${liveness?.periodSeconds}s` })] }));
}
export function ContainerInfo(props) {
const { container, status, resource } = props;
const { t } = useTranslation(['glossary', 'translation']);
const [startedDate, finishDate, lastStateStartedDate, lastStateFinishDate] = React.useMemo(() => {
function getStartedDate(state) {
let startedDate = state?.running?.startedAt || state?.terminated?.startedAt || '';
if (!!startedDate) {
startedDate = localeDate(startedDate);
}
return startedDate;
}
function getFinishDate(state) {
let finishDate = state?.terminated?.finishedAt || '';
if (!!finishDate) {
finishDate = localeDate(finishDate);
}
return finishDate;
}
return [
getStartedDate(status?.state),
getFinishDate(status?.state),
getStartedDate(status?.lastState),
getFinishDate(status?.lastState),
];
}, [status]);
function ContainerStatusLabel(props) {
const { state, container } = props;
const [stateDetails, label, statusType] = React.useMemo(() => {
let stateDetails = null;
let label = t('translation|Ready');
let statusType = '';
if (!state) {
return [stateDetails, label, statusType];
}
if (!!state.waiting) {
stateDetails = state.waiting;
statusType = 'warning';
label = t('translation|Waiting');
}
else if (!!state.running) {
statusType = 'success';
label = t('translation|Running');
}
else if (!!state.terminated) {
stateDetails = state.terminated;
if (state.terminated.exitCode === 0) {
statusType = '';
label = state.terminated.reason;
}
else {
statusType = 'error';
label = t('translation|Error');
}
}
return [stateDetails, label, statusType];
}, [state]);
if (!state || !container) {
return null;
}
const tooltipID = 'container-state-message-' + (container?.name ?? '');
return (_jsx(Box, { children: _jsxs(Box, { children: [_jsx(StatusLabel, { status: statusType, "aria-describedby": !!stateDetails?.message ? tooltipID : undefined, children: label + (stateDetails?.reason ? ` (${stateDetails.reason})` : '') }), !!stateDetails && stateDetails.message && (_jsx(LightTooltip, { role: "tooltip", title: stateDetails.message, interactive: true, id: tooltipID, children: _jsx(Box, { "aria-label": "hidden", display: "inline", px: 1, style: { verticalAlign: 'bottom' }, children: _jsx(Icon, { icon: "mdi:alert-outline", width: "1.3rem", height: "1.3rem", "aria-label": "hidden" }) }) }))] }) }));
}
function StatusValue(props) {
const { rows } = props;
const id = useId('status-value-');
const rowsToDisplay = React.useMemo(() => {
return rows.filter(({ hide }) => !hide);
}, [rows]);
if (rowsToDisplay.length === 0) {
return null;
}
return (_jsx(Box, { children: rowsToDisplay.map(({ name, value }, idx) => {
const rowId = `${id}-${idx}`;
return (_jsxs(Grid, { container: true, spacing: 2, direction: "row", children: [_jsx(Grid, { item: true, children: _jsx(Typography, { id: rowId, color: "textSecondary", children: name }) }), _jsx(Grid, { item: true, children: _jsx(Typography, { "aria-labelledby": rowId, children: value }) })] }, rowId));
}) }));
}
function containerRows() {
return [
{
name: container.name,
withHighlightStyle: true,
},
{
name: t('translation|Status'),
value: _jsx(ContainerStatusLabel, { state: status?.state, container: container }),
hide: !status,
},
{
name: t('translation|Exit Code'),
value: status?.state?.terminated?.exitCode,
hide: !status?.state?.terminated,
},
{
name: t('translation|Started'),
value: startedDate,
hide: !startedDate,
},
{
name: t('translation|Finished'),
value: finishDate,
hide: !finishDate,
},
{
name: t('translation|Restart Count'),
value: status?.restartCount,
hide: !status,
},
{
name: t('translation|Last State'),
value: (_jsxs(Grid, { container: true, direction: "column", spacing: 1, children: [_jsx(Grid, { item: true, children: _jsx(ContainerStatusLabel, { state: status?.lastState, container: container }) }), _jsx(Grid, { item: true, children: _jsx(StatusValue, { rows: [
{
name: t('translation|Exit Code'),
value: status?.lastState?.terminated?.exitCode,
hide: !status?.lastState?.terminated,
},
{
name: t('translation|Started'),
value: lastStateStartedDate,
hide: !lastStateStartedDate,
},
{
name: t('translation|Finished'),
value: lastStateFinishDate,
hide: !lastStateFinishDate,
},
] }) })] })),
hide: Object.keys(status?.lastState ?? {}).length === 0,
},
{
name: t('Container ID'),
value: status?.containerID,
hide: !status,
},
{
name: t('Image Pull Policy'),
value: container.imagePullPolicy,
},
{
name: t('Image'),
value: (_jsxs(_Fragment, { children: [_jsx(Typography, { children: container.image }), status?.imageID && (_jsxs(Typography, { sx: theme => ({
paddingTop: theme.spacing(1),
fontSize: '.95rem',
}), children: [_jsx(Typography, { component: "span", style: { fontWeight: 'bold' }, children: "ID:" }), ' ', status?.imageID] }))] })),
},
{
name: t('Args'),
value: container.args && (_jsx(MetadataDictGrid, { dict: container.args, showKeys: false })),
hide: !container.args,
},
{
name: t('Command'),
value: (container.command || []).join(' '),
hide: !container.command,
},
{
name: t('glossary|Environment'),
value: _jsx(ContainerEnvironmentVariables, { pod: resource, container: container }),
hide: _.isEmpty(container?.env) && _.isEmpty(container?.envFrom),
},
{
name: t('Liveness Probes'),
value: _jsx(LivenessProbes, { liveness: container.livenessProbe }),
hide: _.isEmpty(container.livenessProbe),
},
{
name: t('Ports'),
value: (_jsx(Grid, { container: true, children: container.ports?.map(({ containerPort, protocol, name }, index) => (_jsxs(_Fragment, { children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Box, { display: "flex", alignItems: 'center', children: [_jsxs(Box, { px: 0.5, minWidth: 120, children: [name && _jsx(ValueLabel, { children: `${name} ` }), _jsx(ValueLabel, { children: `${protocol}:` }), _jsx(ValueLabel, { children: containerPort })] }), !!resource && ['Service', 'Pod'].includes(resource.kind) && (_jsx(PortForward, { containerPort: containerPort, resource: resource }))] }) }, `port_line_${index}`), index < container.ports.length - 1 && (_jsx(Grid, { item: true, xs: 12, children: _jsx(Box, { mt: 2, mb: 2, children: _jsx(Divider, { role: "separator" }) }) }))] }))) })),
hide: _.isEmpty(container.ports),
},
{
name: t('Volume Mounts'),
value: _jsx(VolumeMounts, { mounts: container?.volumeMounts || undefined }),
valueFullRow: true,
hide: _.isEmpty(container?.volumeMounts),
},
];
}
return (_jsx(Box, { pb: 1, children: _jsx(NameValueTable, { rows: containerRows() }) }));
}
export function OwnedPodsSection(props) {
const { resource, hideColumns, noSearch } = props;
let namespace;
if (resource.kind === 'Namespace') {
namespace = resource.metadata.name;
}
else {
namespace = resource.metadata.namespace;
}
let labelSelector = '';
if (resource?.jsonData?.spec?.selector) {
labelSelector = labelSelectorToQuery(resource?.jsonData?.spec?.selector);
}
else if (resource.kind === 'JobSet') {
labelSelector = `jobset.sigs.k8s.io/jobset-name=${resource.metadata.name}`;
}
const queryData = {
namespace,
labelSelector,
fieldSelector: resource.kind === 'Node' ? `spec.nodeName=${resource.metadata.name}` : undefined,
cluster: resource.cluster,
};
const podMetricsQueryData = {
...queryData,
// The metrics.k8s.io pod metrics list endpoint does not support spec.nodeName field selectors.
fieldSelector: undefined,
};
const { items: pods, errors } = Pod.useList(queryData);
const { items: podMetrics } = PodMetrics.useList({
...podMetricsQueryData,
refetchInterval: METRIC_REFETCH_INTERVAL_MS,
});
const onlyOneNamespace = !!resource.metadata.namespace || resource.kind === 'Namespace';
const hideNamespaceFilter = on