Skip to content

Commit

Permalink
feat: on log click
Browse files Browse the repository at this point in the history
  • Loading branch information
topliceanurazvan committed Jan 8, 2024
1 parent e202f8f commit b7a49d4
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 170 deletions.
334 changes: 196 additions & 138 deletions packages/web/src/components/molecules/Console/Console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ import {
FC,
PropsWithChildren,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {usePrevious, useUpdate} from 'react-use';
import {useCopyToClipboard, usePrevious, useUpdate} from 'react-use';

import {escapeCarriageReturn} from 'escape-carriage';
import {debounce} from 'lodash';

import {useEventCallback} from '@hooks/useEventCallback';
import {useLastCallback} from '@hooks/useLastCallback';

import {notificationCall} from '@molecules';

import * as S from './Console.styled';
import {ConsoleLine} from './ConsoleLine';
import {ConsoleLineDimensions, ConsoleLineMonitor} from './ConsoleLineMonitor';
Expand All @@ -28,7 +31,10 @@ export interface ConsoleProps {
wrap?: boolean;
content: string;
start?: number;
LineComponent?: FC<PropsWithChildren<{number: number; maxDigits: number}>>;
LineComponent?: FC<
PropsWithChildren<{number: number; maxDigits: number; selectedLine?: number; onLineClick?: (line: number) => void}>
>;
isRunning?: boolean;
}

export interface ConsoleRef {
Expand All @@ -43,141 +49,193 @@ export interface ConsoleRef {
}

// TODO: Optimize to process only newly added content
export const Console = forwardRef<ConsoleRef, ConsoleProps>(({content, wrap, LineComponent = ConsoleLine}, ref) => {
const processor = useMemo(() => LogProcessor.from(escapeCarriageReturn(content)), [content]);
const containerRef = useRef<HTMLDivElement | null>(null);
const maxDigits = processor.maxDigits;

const rerender = useUpdate();

const [{baseWidth, characterWidth, maxCharacters, lineHeight}, setDimensions] = useState<ConsoleLineDimensions>({
baseWidth: 0,
characterWidth: 1000,
maxCharacters: Infinity,
lineHeight: 1000,
lines: 0,
});

const {getTop, getSize, getVisualLine, total} = useLogLinesPosition(processor, maxCharacters);
const getTopPx = (line: number) => lineHeight * getTop(line);
const getCenterPx = (line: number) => getTopPx(line) + (getSize(line) * lineHeight) / 2;

const getViewportTop = useLastCallback(() => Math.floor((containerRef.current?.scrollTop || 0) / lineHeight));
const getViewportHeight = useLastCallback(() => Math.ceil((containerRef.current?.clientHeight || 0) / lineHeight));
const getViewport = () => {
const prerender = Math.max(Math.round(getViewportHeight() / 2), 30);
const viewportStart = Math.max(getViewportTop() - prerender, 0);
const viewportEnd = Math.min(viewportStart + getViewportHeight() + 2 * prerender, total - 1);
return {start: viewportStart, end: viewportEnd};
};
const getViewportLast = () => Math.ceil(Math.min(1 + getViewportTop() + getViewportHeight(), total));

// Keep information about line width
const maxCharactersCount = useMemo(() => processor.getMaxLineLength(), [processor]);
const minWidth = wrap ? 0 : baseWidth + characterWidth * maxCharactersCount;

// Compute current position
const {start: viewportStart, end: viewportEnd} = getViewport();
const {index: start, start: visualStart} = useMemo(
() => getVisualLine(viewportStart + 1),
[processor, maxCharacters, viewportStart]
);
const {index: end, end: visualEnd} = useMemo(
() => getVisualLine(viewportEnd + 1),
[processor, maxCharacters, viewportEnd]
);

const displayed = useMemo(() => processor.getProcessedLines(start, end + 1), [processor, start, end]);

const beforeCount = visualStart;
const afterCount = total - visualEnd;
const beforePx = Math.max(0, Math.floor(beforeCount * lineHeight));
const afterPx = Math.max(0, Math.floor(afterCount * lineHeight));
const styleTop = useMemo(() => ({height: `${beforePx}px`, width: `${minWidth}px`}), [beforePx, minWidth]);
const styleBottom = useMemo(() => ({height: `${afterPx}px`}), [afterPx]);

const scrollToLine = useLastCallback((line: number) => {
containerRef.current?.scrollTo(0, getCenterPx(line) - containerRef.current?.clientHeight / 2);
});
const getLineRect = useLastCallback((line: number) => ({
top: getTop(line),
height: getSize(line),
}));
const isScrolledToEnd = useLastCallback(() => {
const last = getViewportLast();
return last === 1 || last === total;
});
const scrollToEnd = () => {
if (containerRef.current) {
containerRef.current.scrollTo(0, containerRef.current?.scrollHeight);
}
};
const getVisualLinesCount = useLastCallback(() => total);

useImperativeHandle(
ref,
() => ({
get container() {
return containerRef.current;
},
isScrolledToStart() {
return containerRef.current?.scrollTop === 0;
},
scrollToStart: () => {
if (containerRef.current) {
containerRef.current.scrollTo(0, 0);
export const Console = forwardRef<ConsoleRef, ConsoleProps>(
({content, wrap, isRunning, LineComponent = ConsoleLine}, ref) => {
const processor = useMemo(() => LogProcessor.from(escapeCarriageReturn(content)), [content]);
const containerRef = useRef<HTMLDivElement | null>(null);
const maxDigits = processor.maxDigits;

const rerender = useUpdate();

const [{baseWidth, characterWidth, maxCharacters, lineHeight}, setDimensions] = useState<ConsoleLineDimensions>({
baseWidth: 0,
characterWidth: 1000,
maxCharacters: Infinity,
lineHeight: 1000,
lines: 0,
});

const [selectedLine, setSelectedLine] = useState<number>();

const {getTop, getSize, getVisualLine, total} = useLogLinesPosition(processor, maxCharacters);
const getTopPx = (line: number) => lineHeight * getTop(line);
const getCenterPx = (line: number) => getTopPx(line) + (getSize(line) * lineHeight) / 2;

const getViewportTop = useLastCallback(() => Math.floor((containerRef.current?.scrollTop || 0) / lineHeight));
const getViewportHeight = useLastCallback(() => Math.ceil((containerRef.current?.clientHeight || 0) / lineHeight));
const getViewport = () => {
const prerender = Math.max(Math.round(getViewportHeight() / 2), 30);
const viewportStart = Math.max(getViewportTop() - prerender, 0);
const viewportEnd = Math.min(viewportStart + getViewportHeight() + 2 * prerender, total - 1);
return {start: viewportStart, end: viewportEnd};
};
const getViewportLast = () => Math.ceil(Math.min(1 + getViewportTop() + getViewportHeight(), total));

// Keep information about line width
const maxCharactersCount = useMemo(() => processor.getMaxLineLength(), [processor]);
const minWidth = wrap ? 0 : baseWidth + characterWidth * maxCharactersCount;

// Compute current position
const {start: viewportStart, end: viewportEnd} = getViewport();
const {index: start, start: visualStart} = useMemo(
() => getVisualLine(viewportStart + 1),
[processor, maxCharacters, viewportStart]
);
const {index: end, end: visualEnd} = useMemo(
() => getVisualLine(viewportEnd + 1),
[processor, maxCharacters, viewportEnd]
);

const displayed = useMemo(() => processor.getProcessedLines(start, end + 1), [processor, start, end]);

const beforeCount = visualStart;
const afterCount = total - visualEnd;
const beforePx = Math.max(0, Math.floor(beforeCount * lineHeight));
const afterPx = Math.max(0, Math.floor(afterCount * lineHeight));
const styleTop = useMemo(() => ({height: `${beforePx}px`, width: `${minWidth}px`}), [beforePx, minWidth]);
const styleBottom = useMemo(() => ({height: `${afterPx}px`}), [afterPx]);

const [, copyToClipboard] = useCopyToClipboard();

const onLineClick = useCallback(
(line: number) => {
if (isRunning) {
return;
}
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('logLine', line.toString());

copyToClipboard(
`${window.location.origin}${window.location.pathname}?${urlParams.toString()}${window.location.hash}`
);
notificationCall('success', `Log line copied to clipboard!`);
},
scrollToEnd,
isScrolledToEnd,
scrollToLine,
getLineRect,
getVisualLinesCount,
}),
[]
);

// Keep the scroll position
const clientHeight = containerRef.current?.clientHeight || 0;
const domScrollTop = containerRef.current?.scrollTop || 0;
const scrollTop = Math.min(domScrollTop, total * lineHeight - clientHeight);
useLayoutEffect(() => {
containerRef.current!.scrollTop = scrollTop;
}, [scrollTop]);

// Scroll to bottom after logs change
const scrolledToEnd = getViewportLast() >= (usePrevious(total) || 0);
useEffect(() => {
if (scrolledToEnd) {
scrollToEnd();
}
}, [content]);

// Inform about position change
// FIXME
useEffect(() => {
const t = setTimeout(() => containerRef.current?.dispatchEvent(new Event('reposition')), 1);
return () => clearTimeout(t);
}, [viewportStart, viewportEnd, scrollTop, clientHeight, total]);

// Re-render on scroll
const rerenderDebounce = useMemo(() => debounce(rerender, 5), []);
const onScroll = () => {
const viewport = getViewport();
if (viewport.start !== viewportStart || viewport.end !== viewportEnd) {
rerenderDebounce();
}
};
useEventCallback('scroll', onScroll, containerRef?.current);

return (
<S.Container $wrap={wrap} ref={containerRef}>
<S.Content>
<ConsoleLineMonitor Component={LineComponent} maxDigits={maxDigits} wrap={wrap} onChange={setDimensions} />
<S.Space style={styleTop} />
<ConsoleLines lines={displayed} start={start} maxDigits={maxDigits} LineComponent={LineComponent} />
<S.Space style={styleBottom} />
</S.Content>
</S.Container>
);
});
[copyToClipboard, isRunning]
);

const scrollToLine = useLastCallback((line: number) => {
containerRef.current?.scrollTo(0, getCenterPx(line) - containerRef.current?.clientHeight / 2);
});
const getLineRect = useLastCallback((line: number) => ({
top: getTop(line),
height: getSize(line),
}));
const isScrolledToEnd = useLastCallback(() => {
const last = getViewportLast();
return last === 1 || last === total;
});
const scrollToEnd = () => {
if (containerRef.current) {
containerRef.current.scrollTo(0, containerRef.current?.scrollHeight);
}
};
const getVisualLinesCount = useLastCallback(() => total);

useImperativeHandle(
ref,
() => ({
get container() {
return containerRef.current;
},
isScrolledToStart() {
return containerRef.current?.scrollTop === 0;
},
scrollToStart: () => {
if (containerRef.current) {
containerRef.current.scrollTo(0, 0);
}
},
scrollToEnd,
isScrolledToEnd,
scrollToLine,
getLineRect,
getVisualLinesCount,
}),
[]
);

// Keep the scroll position
const clientHeight = containerRef.current?.clientHeight || 0;
const domScrollTop = containerRef.current?.scrollTop || 0;
const scrollTop = Math.min(domScrollTop, total * lineHeight - clientHeight);
useLayoutEffect(() => {
containerRef.current!.scrollTop = scrollTop;
}, [scrollTop]);

// Scroll to bottom after logs change
const scrolledToEnd = getViewportLast() >= (usePrevious(total) || 0);
useEffect(() => {
if (scrolledToEnd) {
scrollToEnd();
}
}, [content]);

useLayoutEffect(() => {
const checkForQueryParam = () => {
const queryParams = new URLSearchParams(window.location.search);
const logLineParam = queryParams.get('logLine');

if (logLineParam) {
const logLine = parseInt(logLineParam, 10);
if (logLine) {
setTimeout(() => {
scrollToLine(logLine);
setSelectedLine(logLine);
}, 0);
}
}
};

if (isRunning) {
return;
}

checkForQueryParam();
}, [isRunning, scrollToLine]);

// Inform about position change
// FIXME
useEffect(() => {
const t = setTimeout(() => containerRef.current?.dispatchEvent(new Event('reposition')), 1);
return () => clearTimeout(t);
}, [viewportStart, viewportEnd, scrollTop, clientHeight, total]);

// Re-render on scroll
const rerenderDebounce = useMemo(() => debounce(rerender, 5), []);
const onScroll = () => {
const viewport = getViewport();
if (viewport.start !== viewportStart || viewport.end !== viewportEnd) {
rerenderDebounce();
}
};
useEventCallback('scroll', onScroll, containerRef?.current);

return (
<S.Container $wrap={wrap} ref={containerRef}>
<S.Content>
<ConsoleLineMonitor Component={LineComponent} maxDigits={maxDigits} wrap={wrap} onChange={setDimensions} />
<S.Space style={styleTop} />
<ConsoleLines
lines={displayed}
start={start}
maxDigits={maxDigits}
LineComponent={LineComponent}
onLineClick={onLineClick}
selectedLine={selectedLine}
/>
<S.Space style={styleBottom} />
</S.Content>
</S.Container>
);
}
);
18 changes: 15 additions & 3 deletions packages/web/src/components/molecules/Console/ConsoleLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ export const ConsoleLines: FC<{
start: number;
maxDigits: number;
LineComponent: ConsoleProps['LineComponent'];
}> = memo(({lines, start, maxDigits, LineComponent = ConsoleLine}) => (
onLineClick?: (line: number) => void;
selectedLine?: number;
}> = memo(({lines, start, maxDigits, LineComponent = ConsoleLine, onLineClick, selectedLine}) => (
<>
{lines.map((line, lineIndex) => (
// eslint-disable-next-line react/no-array-index-key
<LineComponent key={start + lineIndex} number={start + lineIndex + 1} maxDigits={maxDigits}>
<LineComponent
// eslint-disable-next-line react/no-array-index-key
key={start + lineIndex}
number={start + lineIndex + 1}
maxDigits={maxDigits}
onLineClick={() => {
if (onLineClick) {
onLineClick(start + lineIndex + 1);
}
}}
selectedLine={selectedLine}
>
{line.nodes}
</LineComponent>
))}
Expand Down
Loading

0 comments on commit b7a49d4

Please sign in to comment.