@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
347 lines (346 loc) • 17 kB
JavaScript
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")),
},
] }));
}