UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

757 lines (756 loc) 35.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 _, { has } from 'lodash'; 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 Pod from '../../../lib/k8s/pod'; import { METRIC_REFETCH_INTERVAL_MS, PodMetrics } from '../../../lib/k8s/PodMetrics'; import { createRouteURL } from '../../../lib/router'; import { getThemeName } from '../../../lib/themes'; import { localeDate, useId } from '../../../lib/util'; import { HeadlampEventType, useEventCallback } from '../../../redux/headlampEventSlice'; import { useTypedSelector } from '../../../redux/reducers/reducers'; import { useHasPreviousRoute } from '../../App/RouteSwitcher'; import { SectionBox } from '../../common/SectionBox'; import SimpleTable, { NameValueTable } from '../../common/SimpleTable'; import { DefaultDetailsViewSection, } from '../../DetailsViewSection/detailsViewSectionSlice'; 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 { 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, }, }); } }, [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); }, [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); }, [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 })] })), }); } // 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, ...other } = props; const [showPassword, setShowPassword] = React.useState(false); const { t } = useTranslation(); function handleClickShowPassword() { setShowPassword(!showPassword); } return (_jsxs(Grid, { container: true, alignItems: "stretch", spacing: 2, children: [_jsx(Grid, { item: true, children: _jsx(IconButton, { edge: "end", "aria-label": t('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, { readOnly: !showPassword, type: "password", fullWidth: true, multiline: showPassword, maxRows: "20", value: showPassword ? 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() })); } 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() { const env = {}; (container.env || []).forEach(envVar => { let value = ''; if (envVar.value) { value = envVar.value; } else if (envVar.valueFrom) { if (envVar.valueFrom.fieldRef) { value = envVar.valueFrom.fieldRef.fieldPath; } else if (envVar.valueFrom.secretKeyRef) { value = envVar.valueFrom.secretKeyRef.key; } } env[envVar.name] = value; }); 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('Environment'), value: _jsx(MetadataDictGrid, { dict: env }), hide: _.isEmpty(env), }, { 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 }, 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: [_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, {}) }) }))] }))) })), hide: _.isEmpty(container.ports), }, { name: t('Volume Mounts'), value: _jsx(VolumeMounts, { mounts: container?.volumeMounts || undefined }), valueCellProps: { sm: 12 }, 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; } const queryData = { namespace, labelSelector: resource?.jsonData?.spec?.selector ? labelSelectorToQuery(resource?.jsonData?.spec?.selector) : '', fieldSelector: resource.kind === 'Node' ? `spec.nodeName=${resource.metadata.name}` : undefined, cluster: resource.cluster, }; const { items: pods, errors } = Pod.useList(queryData); const { items: podMetrics } = PodMetrics.useList({ ...queryData, refetchInterval: METRIC_REFETCH_INTERVAL_MS, }); const onlyOneNamespace = !!resource.metadata.namespace || resource.kind === 'Namespace'; const hideNamespaceFilter = onlyOneNamespace || noSearch; return (_jsx(PodListRenderer, { hideColumns: hideColumns || onlyOneNamespace ? ['namespace'] : undefined, pods: pods, errors: errors, metrics: podMetrics, noNamespaceFilter: hideNamespaceFilter })); } export function ContainersSection(props) { const { resource } = props; const { t } = useTranslation('glossary'); let title = '…'; function getContainers() { if (!resource) { return []; } let containers = []; if (resource.spec) { if (resource.spec.containers) { title = t('Containers'); containers = resource.spec.containers; } else if (resource.spec.template && resource.spec.template.spec) { title = t('Container Spec'); containers = resource.spec.template.spec.containers; } } return containers; } function getInitContainers() { return resource?.spec?.initContainers || []; } function getEphemeralContainers() { return resource?.spec?.ephemeralContainers || []; } function getStatuses(statusKind) { if (!resource || resource.kind !== 'Pod') { return {}; } const statuses = {}; (resource.status[statusKind] || []).forEach(containerStatus => { const { name, ...status } = containerStatus; statuses[name] = { ...status }; }); return statuses; } const containers = getContainers(); const initContainers = getInitContainers(); const ephemContainers = getEphemeralContainers(); const statuses = getStatuses('containerStatuses'); const initStatuses = getStatuses('initContainerStatuses'); const ephemStatuses = getStatuses('ephemeralContainerStatuses'); const numContainers = containers.length; return (_jsxs(_Fragment, { children: [_jsx(SectionBox, { title: title, children: numContainers === 0 ? (_jsx(Empty, { children: t('translation|No data to be shown.') })) : (containers.map((container) => (_jsx(ContainerInfo, { resource: resource, container: container, status: statuses[container.name] }, `container_${container.name}`)))) }), ephemContainers.length > 0 && (_jsx(SectionBox, { title: t('glossary|Ephemeral Containers'), children: ephemContainers.map((ephemContainer) => (_jsx(ContainerInfo, { resource: resource, container: ephemContainer, status: ephemStatuses[ephemContainer.name] }, `ephem_container_${ephemContainer.name}`))) })), initContainers.length > 0 && (_jsx(SectionBox, { title: t('translation|Init Containers'), children: initContainers.map((initContainer, i) => (_jsx(ContainerInfo, { resource: resource, container: initContainer, status: initStatuses[initContainer.name] }, `init_container_${i}`))) }))] })); } export function ConditionsSection(props) { const { resource } = props; const { t } = useTranslation(['glossary']); if (!resource) { return null; } return (_jsx(SectionBox, { title: t('translation|Conditions'), children: _jsx(ConditionsTable, { resource: resource }) })); } export function VolumeSection(props) { const { t } = useTranslation('glossary'); const { resource } = props; const volumes = resource?.spec?.volumes; if (!volumes) { return null; } const namespace = resource?.metadata?.namespace; /* * printVolumeLink will print a working link that is set within the router using fields from the resource as params */ function PrintVolumeLink(props) { const { volumeName, volumeKind, volume } = props; const resourceClasses = ResourceClasses; const classList = Object.keys(resourceClasses); for (const kind of classList) { if (kind.toLowerCase() === volumeKind.toLowerCase()) { const volumeClass = resourceClasses[kind]; const volumeRoute = volumeClass.detailsRoute; const volumeNamespace = volumeClass.isNamespaced ? namespace : null; const volumeKindNames = { configMap: 'name', secret: 'secretName', persistentVolumeClaim: 'claimName', }; const volumeNameKey = volumeKindNames[volumeKind]; if (!!volumeNameKey) { const detailName = volume[volumeKind][volumeNameKey]; if (!!detailName) { return (_jsx(Link, { routeName: volumeRoute, params: { namespace: volumeNamespace, name: detailName }, children: volumeName })); } } } } return _jsx(Typography, { children: volumeName }); } function volumeRows(volume) { const { name, ...objWithVolumeKind } = volume; const volumeKind = Object.keys(objWithVolumeKind)[0] || ''; if (!volume) { return []; } function printVolumeDetails(volume) { const { ...vol } = volume[volumeKind]; // array for items that are printable const directPrint = []; // array for items that are not printable and need to be printed to yaml const yamlPrint = []; // loop over volumeKeys and check if the value is a string, number, or bool for (const key in vol) { if (!(vol[key] === '')) { if (typeof vol[key] === 'string' || typeof vol[key] === 'number' || typeof vol[key] === 'boolean') { directPrint.push({ volKey: key, volValue: typeof vol[key] === 'boolean' ? vol[key].toString() : vol[key], }); } else { yamlPrint.push({ volKey: key, volValue: vol[key], }); } } } const volumePrints = { directPrint, yamlPrint, }; return volumePrints; } const volumeDetails = printVolumeDetails(volume); return [ { name: name, withHighlightStyle: true, }, ...(volumeKind ? [ { name: 'Kind', value: volumeKind, }, ] : []), { name: 'Source', value: _jsx(PrintVolumeLink, { volumeName: name, volumeKind: volumeKind, volume: volume }), }, ...(volumeDetails.directPrint ? volumeDetails.directPrint.map(({ volKey, volValue }) => { return { name: volKey, value: volValue, }; }) : []), ...(volumeDetails.yamlPrint ? volumeDetails.yamlPrint.map(({ volKey, volValue }) => { return { name: volKey, value: (_jsx(Typography, { component: "pre", variant: "body2", children: YAML.stringify(volValue) })), }; }) : []), ]; } return (_jsx(SectionBox, { title: t('translation|Volumes'), children: volumes.map((volume) => (_jsx(NameValueTable, { rows: volumeRows(volume) }, volume.name))) })); }