diff --git a/Dockerfile b/Dockerfile index 82c4ab2a..80c4908a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/src/pages/pipeline-details/components/Details/components/TaskRunStepWrapper/hooks/useTabs.tsx b/src/pages/pipeline-details/components/Details/components/TaskRunStepWrapper/hooks/useTabs.tsx index 1b67b92b..aa9052af 100644 --- a/src/pages/pipeline-details/components/Details/components/TaskRunStepWrapper/hooks/useTabs.tsx +++ b/src/pages/pipeline-details/components/Details/components/TaskRunStepWrapper/hooks/useTabs.tsx @@ -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 }) => { @@ -21,7 +21,7 @@ export const useTabs = ({ taskRun, task, stepName, pods }) => { label: 'Logs', component: ( - + ), disabled: !pods?.length, diff --git a/src/widgets/PodsLogViewer/index.tsx b/src/widgets/PodsLogViewer/index.tsx new file mode 100644 index 00000000..9e22268a --- /dev/null +++ b/src/widgets/PodsLogViewer/index.tsx @@ -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 = ({ + pods, + activePod, + container, + setContainer, + handlePodChange, +}) => { + const classes = useStyle(); + const [showPrevious, setShowPrevious] = React.useState(false); + const [showTimestamps, setShowTimestamps] = React.useState(false); + const [follow, setFollow] = React.useState(true); + const [lines, setLines] = React.useState(100); + const [logs, setLogs] = React.useState<{ logs: string[]; lastLineShown: number }>({ + logs: [], + lastLineShown: -1, + }); + const xtermRef = React.useRef(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 ( + + + {'Pod'} + + + , + + + {'Container'} + + + , + + + {'Lines'} + + + , + + + } + /> + , + + } + />, + + } + />, + ]} + /> + ); +}; + +export const PodsLogViewer: React.FC = ({ pods, getDefaultContainer }) => { + const firstPod = pods?.[0]; + + const [activePod, setActivePod] = React.useState(firstPod); + const [container, setContainer] = React.useState(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 ( + + ); +}; diff --git a/src/widgets/PodsLogViewer/types.ts b/src/widgets/PodsLogViewer/types.ts new file mode 100644 index 00000000..5031fdb4 --- /dev/null +++ b/src/widgets/PodsLogViewer/types.ts @@ -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>; + handlePodChange: (event: any) => void; +} diff --git a/src/widgets/dialogs/PodsLogViewer/index.tsx b/src/widgets/dialogs/PodsLogViewer/index.tsx index a2e4c255..7c3e943b 100644 --- a/src/widgets/dialogs/PodsLogViewer/index.tsx +++ b/src/widgets/dialogs/PodsLogViewer/index.tsx @@ -54,7 +54,7 @@ export const PodsLogViewerDialog: React.FC = ({ props, const classes = useStyle(); const [showPrevious, setShowPrevious] = React.useState(false); const [showTimestamps, setShowTimestamps] = React.useState(false); - const [follow, setFollow] = React.useState(false); + const [follow, setFollow] = React.useState(true); const [lines, setLines] = React.useState(100); const [logs, setLogs] = React.useState<{ logs: string[]; lastLineShown: number }>({ logs: [], @@ -65,36 +65,39 @@ export const PodsLogViewerDialog: React.FC = ({ props, const { t } = useTranslation('frequent'); const options = { leading: true, trailing: true, maxWait: 1000 }; - const setLogsDebounced = (logLines: string[]) => { - setLogs((current) => { - if (current.lastLineShown >= logLines.length) { - xtermRef.current?.clear(); - xtermRef.current?.write(logLines.join('').replaceAll('\n', '\r\n')); - } else { + 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( - logLines - .slice(current.lastLineShown + 1) - .join('') - .replaceAll('\n', '\r\n') + '\n\n' + + t('translation|Logs are paused. Click the follow button to resume following them.') + + '\r\n' ); + return; } - - 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('logs|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(