UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

347 lines (346 loc) 17 kB
import { jsx as _jsx, jsxs as _jsxs } 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 FormControl from '@mui/material/FormControl'; import FormControlLabel from '@mui/material/FormControlLabel'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import Switch from '@mui/material/Switch'; import { styled } from '@mui/system'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import Pod from '../../lib/k8s/pod'; import { DefaultHeaderAction } from '../../redux/actionButtonsSlice'; import { EventStatus, HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { ActionButton, LightTooltip, SectionBox, SimpleTable } from '../common'; import Link from '../common/Link'; import { LogViewer } from '../common/LogViewer'; import { ConditionsSection, ContainersSection, DetailsGrid, VolumeSection, } from '../common/Resource'; import AuthVisible from '../common/Resource/AuthVisible'; import Terminal from '../common/Terminal'; import { makePodStatusLabel } from './List'; const PaddedFormControlLabel = styled(FormControlLabel)(({ theme }) => ({ margin: 0, paddingTop: theme.spacing(2), paddingRight: theme.spacing(2), })); export function PodLogViewer(props) { const { item, onClose, open, ...other } = props; const [container, setContainer] = React.useState(getDefaultContainer()); const [showPrevious, setShowPrevious] = React.useState(false); const [showTimestamps, setShowTimestamps] = React.useState(true); const [follow, setFollow] = React.useState(true); const [lines, setLines] = React.useState(100); const [logs, setLogs] = React.useState({ logs: [], lastLineShown: -1, }); const [showReconnectButton, setShowReconnectButton] = React.useState(false); const [cancelLogsStream, setCancelLogsStream] = React.useState(null); const xtermRef = React.useRef(null); const { t } = useTranslation(); function getDefaultContainer() { return item.spec.containers.length > 0 ? item.spec.containers[0].name : ''; } const options = { leading: true, trailing: true, maxWait: 1000 }; function setLogsDebounced(logLines) { setLogs(current => { if (current.lastLineShown >= logLines.length) { xtermRef.current?.clear(); xtermRef.current?.write(logLines.join('').replaceAll('\n', '\r\n')); } else { xtermRef.current?.write(logLines .slice(current.lastLineShown + 1) .join('') .replaceAll('\n', '\r\n')); } return { logs: logLines, lastLineShown: logLines.length - 1, }; }); // If we stopped following the logs and we have logs already, // then we don't need to fetch them again. if (!follow && logs.logs.length > 0) { xtermRef.current?.write('\n\n' + t('translation|Logs are paused. Click the follow button to resume following them.') + '\r\n'); return; } } const debouncedSetState = _.debounce(setLogsDebounced, 500, options); React.useEffect(() => { let callback = null; if (props.open) { xtermRef.current?.clear(); setLogs({ logs: [], lastLineShown: -1 }); callback = item.getLogs(container, debouncedSetState, { tailLines: lines, showPrevious, showTimestamps, follow, /** * When the connection is lost, show the reconnect button. * This will stop the current log stream. */ onReconnectStop: () => { setShowReconnectButton(true); }, }); } return function cleanup() { if (callback) { callback(); } }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [container, lines, open, showPrevious, showTimestamps, follow]); function handleContainerChange(event) { setContainer(event.target.value); } function handleLinesChange(event) { setLines(event.target.value); } function handlePreviousChange() { setShowPrevious(previous => !previous); } function hasContainerRestarted() { const cont = item?.status?.containerStatuses?.find((c) => c.name === container); if (!cont) { return false; } return cont.restartCount > 0; } function handleTimestampsChange() { setShowTimestamps(timestamps => !timestamps); } function handleFollowChange() { setFollow(follow => !follow); } /** * Handle the reconnect button being clicked. * This will start a new log stream and hide the reconnect button. */ function handleReconnect() { // If there's an existing log stream, cancel it if (cancelLogsStream) { cancelLogsStream(); } // Start a new log stream const newCancelLogsStream = item.getLogs(container, debouncedSetState, { tailLines: lines, showPrevious, showTimestamps, follow, /** * When the connection is lost, show the reconnect button. * This will stop the current log stream. */ onReconnectStop: () => { setShowReconnectButton(true); }, }); // Set the cancelLogsStream function to the new one setCancelLogsStream(() => newCancelLogsStream); // Hide the reconnect button setShowReconnectButton(false); } return (_jsx(LogViewer, { title: t('glossary|Logs: {{ itemName }}', { itemName: item.getName() }), downloadName: `${item.getName()}_${container}`, open: open, onClose: onClose, logs: logs.logs, xtermRef: xtermRef, handleReconnect: handleReconnect, showReconnectButton: showReconnectButton, topActions: [ _jsxs(FormControl, { sx: { minWidth: '11rem' }, children: [_jsx(InputLabel, { shrink: true, id: "container-name-chooser-label", children: t('glossary|Container') }), _jsxs(Select, { labelId: "container-name-chooser-label", id: "container-name-chooser", value: container, onChange: handleContainerChange, children: [item?.spec?.containers && (_jsx(MenuItem, { disabled: true, value: "", children: t('glossary|Containers') })), item?.spec?.containers.map(({ name }) => (_jsx(MenuItem, { value: name, children: name }, name))), item?.spec?.initContainers && (_jsx(MenuItem, { disabled: true, value: "", children: t('translation|Init Containers') })), item.spec.initContainers?.map(({ name }) => (_jsx(MenuItem, { value: name, children: name }, `init_container_${name}`))), item?.spec?.ephemeralContainers && (_jsx(MenuItem, { disabled: true, value: "", children: t('glossary|Ephemeral Containers') })), item.spec.ephemeralContainers?.map(({ name }) => (_jsx(MenuItem, { value: name, children: name }, `eph_container_${name}`)))] })] }), _jsxs(FormControl, { sx: { minWidth: '6rem' }, children: [_jsx(InputLabel, { shrink: true, id: "container-lines-chooser-label", children: t('translation|Lines') }), _jsxs(Select, { labelId: "container-lines-chooser-label", id: "container-lines-chooser", value: lines, onChange: handleLinesChange, children: [[100, 1000, 2500].map(i => (_jsx(MenuItem, { value: i, children: i }, i))), _jsx(MenuItem, { value: -1, children: "All" })] })] }), _jsx(LightTooltip, { title: hasContainerRestarted() ? t('translation|Show logs for previous instances of this container.') : t('translation|You can only select this option for containers that have been restarted.'), children: _jsx(PaddedFormControlLabel, { label: t('translation|Show previous'), disabled: !hasContainerRestarted(), control: _jsx(Switch, { checked: showPrevious, onChange: handlePreviousChange, name: "checkPrevious", color: "primary", size: "small" }) }) }), _jsx(PaddedFormControlLabel, { label: t('translation|Timestamps'), control: _jsx(Switch, { checked: showTimestamps, onChange: handleTimestampsChange, name: "checkTimestamps", color: "primary", size: "small" }) }), _jsx(PaddedFormControlLabel, { label: t('translation|Follow'), control: _jsx(Switch, { checked: follow, onChange: handleFollowChange, name: "follow", color: "primary", size: "small" }) }), ], ...other })); } export function VolumeDetails(props) { const { volumes } = props; if (!volumes) { return null; } const { t } = useTranslation(); return (_jsx(SectionBox, { title: t('translation|Volumes'), children: _jsx(SimpleTable, { columns: [ { label: t('translation|Name'), getter: data => data.name, }, { label: t('translation|Type'), getter: data => Object.keys(data)[1], }, ], data: volumes, reflectInURL: "volumes" }) })); } function TolerationsSection(props) { const { tolerations } = props; const { t } = useTranslation(['glossary', 'translation']); return (_jsx(SectionBox, { title: t('Tolerations'), children: _jsx(SimpleTable, { data: tolerations, columns: [ { label: t('translation|Key'), getter: data => data.key, }, { label: t('translation|Value'), getter: data => data.value, }, { label: t('translation|Operator'), getter: data => data.operator, gridTemplate: '0.5fr', }, { label: t('translation|Effect'), getter: data => data.effect, }, { label: t('Seconds'), getter: data => data.tolerationSeconds, gridTemplate: '0.5fr', }, ] }) })); } export default function PodDetails(props) { const { showLogsDefault } = props; const params = useParams(); const { name = params.name, namespace = params.namespace, cluster } = props; const [showLogs, setShowLogs] = React.useState(!!showLogsDefault); const [showTerminal, setShowTerminal] = React.useState(false); const { t } = useTranslation('glossary'); const [isAttached, setIsAttached] = React.useState(false); const dispatchHeadlampEvent = useEventCallback(); return (_jsx(DetailsGrid, { resourceType: Pod, name: name, namespace: namespace, cluster: cluster, withEvents: true, actions: item => item && [ { id: DefaultHeaderAction.POD_LOGS, action: (_jsx(AuthVisible, { item: item, authVerb: "get", subresource: "log", children: _jsx(ActionButton, { description: t('Show Logs'), "aria-label": t('logs'), icon: "mdi:file-document-box-outline", onClick: () => { setShowLogs(true); dispatchHeadlampEvent({ type: HeadlampEventType.LOGS, data: { status: EventStatus.OPENED, }, }); } }) })), }, { id: DefaultHeaderAction.POD_TERMINAL, action: (_jsx(AuthVisible, { item: item, authVerb: "get", subresource: "exec", children: _jsx(ActionButton, { description: t('Terminal / Exec'), "aria-label": t('terminal'), icon: "mdi:console", onClick: () => { setShowTerminal(true); dispatchHeadlampEvent({ type: HeadlampEventType.TERMINAL, data: { resource: item, status: EventStatus.CLOSED, }, }); } }) })), }, { id: DefaultHeaderAction.POD_ATTACH, action: (_jsx(AuthVisible, { item: item, authVerb: "get", subresource: "attach", children: _jsx(ActionButton, { description: t('Attach'), "aria-label": t('attach'), icon: "mdi:connection", onClick: () => { setIsAttached(true); dispatchHeadlampEvent({ type: HeadlampEventType.POD_ATTACH, data: { resource: item, status: EventStatus.OPENED, }, }); } }) })), }, ], extraInfo: item => item && [ { name: t('State'), value: makePodStatusLabel(item, false), }, { name: t('Node'), value: item.spec.nodeName ? (_jsx(Link, { routeName: "node", params: { name: item.spec.nodeName }, activeCluster: item.cluster, children: item.spec.nodeName })) : (''), }, { name: t('Service Account'), value: !!item.spec.serviceAccountName || !!item.spec.serviceAccount ? (_jsx(Link, { routeName: "serviceAccount", params: { namespace: item.metadata.namespace, name: item.spec.serviceAccountName || item.spec.serviceAccount, }, activeCluster: item.cluster, children: item.spec.serviceAccountName || item.spec.serviceAccount })) : (''), }, { name: t('Host IP'), value: item.status.hostIP ?? '', }, { name: t('Pod IP'), value: item.status.podIP ?? '', }, { name: t('QoS Class'), value: item.status.qosClass, }, { name: t('Priority'), value: item.spec.priority, }, ], extraSections: item => item && [ { id: 'headlamp.pod-tolerations', section: _jsx(TolerationsSection, { tolerations: item?.spec?.tolerations || [] }), }, { id: 'headlamp.pod-conditions', section: _jsx(ConditionsSection, { resource: item?.jsonData }), }, { id: 'headlamp.pod-containers', section: _jsx(ContainersSection, { resource: item?.jsonData }), }, { id: 'headlamp.pod-volumes', section: _jsx(VolumeSection, { resource: item?.jsonData }), }, { id: 'headlamp.pod-logs', section: (_jsx(PodLogViewer, { open: showLogs, item: item, onClose: () => { dispatchHeadlampEvent({ type: HeadlampEventType.LOGS, data: { resource: item, status: EventStatus.CLOSED, }, }); setShowLogs(false); } }, "logs")), }, { id: 'headlamp.pod-terminal', section: (_jsx(Terminal, { open: showTerminal || isAttached, item: item, onClose: () => { setShowTerminal(false); dispatchHeadlampEvent({ type: HeadlampEventType.TERMINAL, data: { resource: item, status: EventStatus.CLOSED, }, }); setIsAttached(false); }, isAttach: isAttached }, "terminal")), }, ] })); }