UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

1,077 lines 58.5 kB
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