in src/components/LogViewer/index.tsx [57:201]
export function LogViewer(props: LogViewerProps) {
const { logs, downloadName = 'log', xtermRef: outXtermRef, topActions = [] } = props;
const [isFullScreen] = React.useState(false);
const classes = useStyle({ isFullScreen });
const xtermRef = React.useRef<XTerminal | null>(null);
const fitAddonRef = React.useRef<any>(null);
const searchAddonRef = React.useRef<any>(null);
const [terminalContainerRef, setTerminalContainerRef] = React.useState<HTMLElement | null>(null);
const [showSearch, setShowSearch] = React.useState(false);
useHotkeys('ctrl+shift+f', () => {
setShowSearch(true);
});
const XterminalReadonlyConfig: ITerminalOptions = {
cursorStyle: 'bar',
scrollback: 10000,
rows: 30, // initial rows before fit
fontFamily: 'IBM Plex Mono,monospace',
fontSize: 12,
lineHeight: 1.26,
};
function downloadLog() {
// Cuts off the last 5 digits of the timestamp to remove the milliseconds
const time = new Date().toISOString().replace(/:/g, '-').slice(0, -5);
const element = document.createElement('a');
const file = new Blob(logs, { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = `${downloadName}_${time}.txt`;
// Required for FireFox
document.body.appendChild(element);
element.click();
}
React.useEffect(() => {
if (!terminalContainerRef || !!xtermRef.current) {
return;
}
fitAddonRef.current = new FitAddon();
searchAddonRef.current = new SearchAddon();
xtermRef.current = new XTerminal(XterminalReadonlyConfig);
if (!!outXtermRef) {
outXtermRef.current = xtermRef.current;
}
xtermRef.current.loadAddon(fitAddonRef.current);
xtermRef.current.loadAddon(searchAddonRef.current);
enableCopyPasteInXterm(xtermRef.current);
xtermRef.current.open(terminalContainerRef!);
fitAddonRef.current!.fit();
xtermRef.current?.write(getJointLogs());
const pageResizeHandler = () => {
fitAddonRef.current!.fit();
console.debug('resize');
};
window.addEventListener('resize', pageResizeHandler);
return function cleanup() {
window.removeEventListener('resize', pageResizeHandler);
xtermRef.current?.dispose();
searchAddonRef.current?.dispose();
xtermRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminalContainerRef, xtermRef.current]);
React.useEffect(() => {
if (!xtermRef.current) {
return;
}
// We're delegating to external xterm ref.
if (!!outXtermRef) {
return;
}
xtermRef.current?.clear();
xtermRef.current?.write(getJointLogs());
return function cleanup() {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [logs, xtermRef]);
function getJointLogs() {
return logs?.join('').replaceAll('\n', '\r\n');
}
return (
<>
<Grid container justifyContent="space-between" alignItems="center" wrap="nowrap">
<Grid item container spacing={1}>
{topActions.map((component, i) => (
<Grid item key={i}>
{component}
</Grid>
))}
</Grid>
<Grid item xs>
<ActionButton
description={'Find'}
onClick={() => setShowSearch((show) => !show)}
icon="mdi:magnify"
/>
</Grid>
<Grid item xs>
<ActionButton
description={'Clear'}
onClick={() => clearPodLogs(xtermRef)}
icon="mdi:broom"
/>
</Grid>
<Grid item xs>
<ActionButton
description={'Download'}
onClick={downloadLog}
icon="mdi:file-download-outline"
/>
</Grid>
</Grid>
<Box className={classes.logBox}>
<div
id="xterm-container"
className={classes.terminal}
ref={(ref) => setTerminalContainerRef(ref)}
style={{ flex: 1, display: 'flex', flexDirection: 'column-reverse' }}
/>
<SearchPopover
open={showSearch}
onClose={() => setShowSearch(false)}
searchAddonRef={searchAddonRef}
/>
</Box>
</>
);
}