Skip to content

Commit

Permalink
fix: Logs rerendering and getLogs fn (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
callmevladik authored and MykolaMarusenko committed Nov 12, 2024
1 parent 14a0dd1 commit c64a624
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 31 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM epamedp/headlamp:0.22.34
FROM epamedp/headlamp:0.22.35

COPY --chown=100:101 assets/ /headlamp/frontend
COPY --chown=100:101 dist/main.js /headlamp/plugins/edp/main.js
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { ViewYAML } from '../../../../../../../components/Editor';
import { LogsViewer } from '../../../../../../../widgets/LogViewer';
import { PodsLogViewer } from '../../../../../../../widgets/PodsLogViewer';
import { TabContent } from '../../TabContent';

export const useTabs = ({ taskRun, task, stepName, pods }) => {
Expand All @@ -21,7 +21,7 @@ export const useTabs = ({ taskRun, task, stepName, pods }) => {
label: 'Logs',
component: (
<TabContent>
<LogsViewer pods={pods} getDefaultContainer={getDefaultContainer} />
<PodsLogViewer pods={pods} getDefaultContainer={getDefaultContainer} />
</TabContent>
),
disabled: !pods?.length,
Expand Down
295 changes: 295 additions & 0 deletions src/widgets/PodsLogViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import { LightTooltip } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { KubeContainerStatus } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster';
import {
FormControl,
FormControlLabel,
InputLabel,
ListSubheader,
MenuItem,
Select,
Switch,
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Terminal as XTerminal } from 'xterm';
import { LogViewer } from '../../components/LogViewer';
import { PodKubeObjectInterface } from '../../k8s/groups/default/Pod/types';
import { PodsLogViewerInnerProps, PodsLogViewerProps } from './types';

const useStyle = makeStyles((theme) => ({
containerFormControl: {
minWidth: '11rem',
},
linesFormControl: {
minWidth: '6rem',
},
switchControl: {
margin: 0,
paddingTop: theme.spacing(2),
paddingRight: theme.spacing(2),
},
}));

const PodsLogViewerInner: React.FC<PodsLogViewerInnerProps> = ({
pods,
activePod,
container,
setContainer,
handlePodChange,
}) => {
const classes = useStyle();
const [showPrevious, setShowPrevious] = React.useState<boolean>(false);
const [showTimestamps, setShowTimestamps] = React.useState<boolean>(false);
const [follow, setFollow] = React.useState<boolean>(true);
const [lines, setLines] = React.useState<number>(100);
const [logs, setLogs] = React.useState<{ logs: string[]; lastLineShown: number }>({
logs: [],
lastLineShown: -1,
});
const xtermRef = React.useRef<XTerminal | null>(null);
const { t } = useTranslation('frequent');

const options = { leading: true, trailing: true, maxWait: 1000 };
const setLogsDebounced = React.useCallback(
(logLines: string[]) => {
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;
}
},
[follow, logs.logs.length, t]
);
const debouncedSetState = _.debounce(setLogsDebounced, 500, options);

React.useEffect(
() => {
let callback: any = null;

if (!!activePod) {
xtermRef.current?.clear();
setLogs({ logs: [], lastLineShown: -1 });

callback = activePod.getLogs(container, debouncedSetState, {
tailLines: lines,
showPrevious,
showTimestamps,
follow,
});
}

return function cleanup() {
if (callback) {
callback();
}
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[container, lines, open, showPrevious, showTimestamps, follow]
);

const handleContainerChange = (event: any) => {
setContainer(event.target.value);
};

const handleLinesChange = (event: any) => {
setLines(event.target.value);
};

const handlePreviousChange = () => {
setShowPrevious((previous) => !previous);
};

const hasContainerRestarted = () => {
const cont = activePod?.status?.containerStatuses?.find(
(c: KubeContainerStatus) => c.name === container
);
if (!cont) {
return false;
}

return cont.restartCount > 0;
};

const handleTimestampsChange = () => {
setShowTimestamps((timestamps) => !timestamps);
};

const handleFollowChange = () => {
setFollow((follow) => !follow);
};

return (
<LogViewer
downloadName={`${activePod?.getName()}_${container}`}
logs={logs.logs}
xtermRef={xtermRef}
topActions={[
<FormControl>
<InputLabel shrink id={'pod-name-chooser-logs-label'}>
{'Pod'}
</InputLabel>
<Select
labelId="pod-name-chooser-logs-label"
id="pod-name-chooser-logs"
value={activePod ? activePod.metadata.name : ''}
onChange={handlePodChange}
>
{pods &&
!!pods.length &&
pods.map(({ metadata: { name } }) => (
<MenuItem value={name} key={name}>
{name}
</MenuItem>
))}
</Select>
</FormControl>,
<FormControl className={classes.containerFormControl}>
<InputLabel shrink id="container-name-chooser-logs-label">
{'Container'}
</InputLabel>
<Select
labelId="container-name-chooser-logs-label"
id="container-name-chooser-logs"
value={container}
onChange={handleContainerChange}
>
<ListSubheader color="inherit">Containers</ListSubheader>
{(activePod?.spec.containers || []).map(({ name }) => (
<MenuItem value={name} key={name}>
{name}
</MenuItem>
))}
{activePod?.spec?.initContainers?.length && (
<ListSubheader color="inherit">Init Containers</ListSubheader>
)}
{(activePod?.spec.initContainers || []).map(({ name }) => (
<MenuItem value={name} key={name}>
{name}
</MenuItem>
))}
</Select>
</FormControl>,
<FormControl className={classes.linesFormControl}>
<InputLabel shrink id="container-lines-chooser-label">
{'Lines'}
</InputLabel>
<Select
labelId="container-lines-chooser-label"
id="container-lines-chooser"
value={lines}
onChange={handleLinesChange}
>
{[100, 1000, 2500].map((i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
</FormControl>,
<LightTooltip
title={
hasContainerRestarted()
? 'Show logs for previous instances of this container.'
: 'You can only select this option for containers that have been restarted.'
}
>
<FormControlLabel
className={classes.switchControl}
label={'Show previous'}
disabled={!hasContainerRestarted()}
control={
<Switch
checked={showPrevious}
onChange={handlePreviousChange}
name="checkPrevious"
color="primary"
size="small"
/>
}
/>
</LightTooltip>,
<FormControlLabel
className={classes.switchControl}
label={'Timestamps'}
control={
<Switch
checked={showTimestamps}
onChange={handleTimestampsChange}
name="checkTimestamps"
color="primary"
size="small"
/>
}
/>,
<FormControlLabel
className={classes.switchControl}
label={'Follow'}
control={
<Switch
checked={follow}
onChange={handleFollowChange}
name="follow"
color="primary"
size="small"
/>
}
/>,
]}
/>
);
};

export const PodsLogViewer: React.FC<PodsLogViewerProps> = ({ pods, getDefaultContainer }) => {
const firstPod = pods?.[0];

const [activePod, setActivePod] = React.useState<PodKubeObjectInterface>(firstPod);
const [container, setContainer] = React.useState<string>(getDefaultContainer(firstPod));

const handlePodChange = React.useCallback(
(event: any) => {
const newPodName = event.target.value;
const newPod = pods.find(({ metadata: { name } }) => name === newPodName);
if (newPod) {
setActivePod(newPod);
setContainer(getDefaultContainer(newPod));
}
},
[getDefaultContainer, pods]
);

return (
<PodsLogViewerInner
pods={pods}
activePod={activePod}
handlePodChange={handlePodChange}
container={container}
setContainer={setContainer}
/>
);
};
14 changes: 14 additions & 0 deletions src/widgets/PodsLogViewer/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PodKubeObjectInterface } from "../../k8s/groups/default/Pod/types";

export interface PodsLogViewerProps {
pods: PodKubeObjectInterface[];
getDefaultContainer: (pod: PodKubeObjectInterface) => string;
}

export interface PodsLogViewerInnerProps {
pods: PodKubeObjectInterface[];
activePod: PodKubeObjectInterface;
container: string;
setContainer: React.Dispatch<React.SetStateAction<string>>;
handlePodChange: (event: any) => void;
}
Loading

0 comments on commit c64a624

Please sign in to comment.