Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ const ScrollToButton = ({

export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: Props) => {
const hash = location.hash.replace("#", "");
const parentRef = useRef(null);
const parentRef = useRef<HTMLDivElement | null>(null);

const rowVirtualizer = useVirtualizer({
count: parsedLogs.length,
estimateSize: () => 20,
Expand All @@ -95,15 +96,103 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
}, [rowVirtualizer, parsedLogs]);

useLayoutEffect(() => {
if (location.hash && !isLoading) {
rowVirtualizer.scrollToIndex(Math.min(Number(hash) + 5, parsedLogs.length - 1));
if (!location.hash || isLoading || parsedLogs.length === 0) {
return;
}

const targetIndex = Math.max(0, Math.min(parsedLogs.length - 1, Number(hash) || 0));
const el = parentRef.current;

const total = rowVirtualizer.getTotalSize();
const clientH = el?.clientHeight ?? 0;
Comment on lines +100 to +107
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the direct link to lines. We used to add 5 more lines so it wouldn't be on the edge of the container, f screenshot.

Screenshot 2025-09-02 at 14 06 18

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both clicking on a log line, or refreshing the page with a URL with a line number specified will not land where we want. http://localhost:28080/dags/big_logs/runs/manual__2025-09-02T12:02:44.060539+00:00/tasks/my_task?try_number=1#4806

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const vItem = rowVirtualizer.getVirtualItems().find((virtualRow) => virtualRow.index === targetIndex);
const approxPerItem = 20;
const anchor = vItem?.start ?? targetIndex * approxPerItem;

const offset = Math.max(0, Math.min(total - clientH, anchor));

if (el) {
if (typeof rowVirtualizer.scrollToOffset === "function") {
try {
rowVirtualizer.scrollToOffset(offset);
} catch {
rowVirtualizer.scrollToIndex(targetIndex, { align: "start" });
}
} else {
rowVirtualizer.scrollToIndex(targetIndex, { align: "start" });
}

el.scrollTop = offset;

requestAnimationFrame(() => {
el.scrollTop = offset;
});
} else {
rowVirtualizer.scrollToIndex(targetIndex, { align: "start" });
}
}, [isLoading, rowVirtualizer, hash, parsedLogs]);

const handleScrollTo = (to: "bottom" | "top") => {
if (parsedLogs.length > 0) {
rowVirtualizer.scrollToIndex(to === "bottom" ? parsedLogs.length - 1 : 0);
if (parsedLogs.length === 0) {
return;
}

const el = rowVirtualizer.scrollElement ?? parentRef.current;

if (!el) {
return;
}

if (to === "top") {
if (typeof rowVirtualizer.scrollToOffset === "function") {
try {
rowVirtualizer.scrollToOffset(0);
} catch {
rowVirtualizer.scrollToIndex(0, { align: "start" });
}
} else {
rowVirtualizer.scrollToIndex(0, { align: "start" });
}
el.scrollTop = 0;
requestAnimationFrame(() => {
el.scrollTop = 0;
});

return;
}

// === bottom === (instant jump even for huge lists)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a comment for bottom, but not for top. Remove it

Suggested change
// === bottom === (instant jump even for huge lists)

const total = rowVirtualizer.getTotalSize();
const clientH = el.clientHeight || 0;
const offset = Math.max(0, Math.floor(total - clientH));

// First tell the virtualizer where we want to be
if (typeof rowVirtualizer.scrollToOffset === "function") {
try {
rowVirtualizer.scrollToOffset(offset);
} catch {
rowVirtualizer.scrollToIndex(parsedLogs.length - 1, { align: "end" });
}
Comment on lines +172 to +176
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be enough. We can remove the typeof check. (undefined is not callable) (same above)

} else {
rowVirtualizer.scrollToIndex(parsedLogs.length - 1, { align: "end" });
}

el.scrollTop = offset;

requestAnimationFrame(() => {
el.scrollTop = offset;
requestAnimationFrame(() => {
el.scrollTop = offset;
const lastItem = el.querySelector<HTMLElement>(
`[data-testid="virtualized-item-${parsedLogs.length - 1}"]`,
);

if (lastItem) {
lastItem.scrollIntoView({ behavior: "auto", block: "end" });
}
});
});
Comment on lines +183 to +195
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are those requestAnimationFrame achieving? I Tried without them and it still look fine.

};

useHotkeys("mod+ArrowDown", () => handleScrollTo("bottom"), { enabled: !isLoading });
Expand All @@ -113,50 +202,54 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
<Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0} position="relative">
<ErrorAlert error={error ?? logError} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<Code
css={{
"& *::selection": {
bg: "blue.subtle",
},
}}
data-testid="virtualized-list"

<Box
data-testid="virtual-scroll-container"
flexGrow={1}
h="auto"
minHeight={0}
overflow="auto"
position="relative"
py={3}
ref={parentRef}
textWrap={wrap ? "pre" : "nowrap"}
width="100%"
>
<VStack alignItems="flex-start" gap={0} h={`${rowVirtualizer.getTotalSize()}px`}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<Box
_ltr={{
left: 0,
right: "auto",
}}
_rtl={{
left: "auto",
right: 0,
}}
bgColor={
Boolean(hash) && virtualRow.index === Number(hash) - 1 ? "blue.emphasized" : "transparent"
}
data-index={virtualRow.index}
data-testid={`virtualized-item-${virtualRow.index}`}
key={virtualRow.key}
position="absolute"
ref={rowVirtualizer.measureElement}
top={0}
transform={`translateY(${virtualRow.start}px)`}
width={wrap ? "100%" : "max-content"}
>
{parsedLogs[virtualRow.index] ?? undefined}
</Box>
))}
</VStack>
</Code>
<Code
css={{
"& *::selection": { bg: "blue.subtle" },
}}
data-testid="virtualized-list"
display="block"
textWrap={wrap ? "pre" : "nowrap"}
width="100%"
>
<VStack
alignItems="flex-start"
gap={0}
h={`${rowVirtualizer.getTotalSize()}px`}
position="relative"
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<Box
_ltr={{ left: 0, right: "auto" }}
_rtl={{ left: "auto", right: 0 }}
bgColor={
Boolean(hash) && virtualRow.index === Number(hash) - 1 ? "blue.emphasized" : "transparent"
}
data-index={virtualRow.index}
data-testid={`virtualized-item-${virtualRow.index}`}
key={virtualRow.key}
position="absolute"
ref={rowVirtualizer.measureElement}
top={0}
transform={`translateY(${virtualRow.start}px)`}
width={wrap ? "100%" : "max-content"}
>
{parsedLogs[virtualRow.index] ?? undefined}
</Box>
))}
</VStack>
</Code>
</Box>

{showScrollButtons ? (
<>
Expand Down