Skip to content

Commit

Permalink
wip: basic virtualization for console
Browse files Browse the repository at this point in the history
  • Loading branch information
johann-crabnebula committed Feb 5, 2024
1 parent 6387c2a commit 1e253f2
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 120 deletions.
88 changes: 88 additions & 0 deletions clients/web/src/components/auto-scroll-pane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Show, Accessor, JSXElement, For, createEffect, on } from "solid-js";
import { createVirtualizer } from "@tanstack/solid-virtual";

function scrollEnd(ref?: HTMLElement, smooth?: boolean) {
ref?.scroll({
top: ref.scrollHeight,
/**
* @todo
* make it smooth when autoscroll is turned BACK on.
*/
behavior: smooth ? "smooth" : "instant",
});
}

type AutoScrollPaneProps<DataType> = {
shouldAutoScroll: Accessor<boolean>;
dataStream: DataType[];
fallback: JSXElement;
displayOptions: Record<string, unknown>;
displayComponent: (props: {
event: DataType;
[key: string]: unknown;
}) => JSXElement;
};

export function AutoScrollPane<DataType>(props: AutoScrollPaneProps<DataType>) {
let logPanel: HTMLDivElement | undefined;

const virtualizer = createVirtualizer({
count: props.dataStream.length,
getScrollElement: () => logPanel ?? null,
estimateSize: () => 30,
overscan: 10,
});

function updateVirtualizer(newCount: number) {
virtualizer.setOptions({
...virtualizer.options,
count: newCount,
});
virtualizer.measure();
}

createEffect(
on(
() => props.dataStream,
() => {
updateVirtualizer(props.dataStream.length);
if (props.shouldAutoScroll()) {
scrollEnd(logPanel);
}
}
)
);

return (
<div
ref={(e) => (logPanel = e)}
class="overflow-auto h-[calc(100%-var(--toolbar-height))] relative"
>
<Show when={props.dataStream.length === 0 || !props.dataStream}>
{props.fallback}
</Show>
<ul class="" style={{ height: `${virtualizer.getTotalSize()}px` }}>
<For each={virtualizer.getVirtualItems()}>
{(virtualRow, index) => {
return (
<li
data-id={virtualRow.index}
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${
virtualRow.start - index() * virtualRow.size
}px)`,
}}
>
<props.displayComponent
event={props.dataStream[virtualRow.index]}
{...props.displayOptions}
/>
</li>
);
}}
</For>
</ul>
</div>
);
}
42 changes: 0 additions & 42 deletions clients/web/src/components/autoscroll-pane.tsx

This file was deleted.

73 changes: 73 additions & 0 deletions clients/web/src/components/console/log-event-entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Show, untrack } from "solid-js";
import { getLogMetadata } from "~/lib/console/get-log-metadata";
import { timestampToDate, formatTimestamp } from "~/lib/formatters";
import { getLevelClasses } from "~/lib/console/get-level-classes";
import { useMonitor } from "~/context/monitor-provider";
import { processFieldValue } from "~/lib/span/process-field-value";
import type { LogEvent } from "~/lib/proto/logs";
import clsx from "clsx";
import { getFileNameFromPath } from "~/lib/console/get-file-name-from-path";

export function LogEventEntry(props: {
event: LogEvent;
showTimestamp?: boolean;
}) {
const { monitorData } = useMonitor();
const logEvent = untrack(() => props.event);
const { message, at } = logEvent;
if (!at) return null;

const metadata = getLogMetadata(monitorData, logEvent);
const timeDate = timestampToDate(at);
const levelStyle = getLevelClasses(metadata?.level);

let target = metadata?.target;
if (target === "log") {
const field = logEvent.fields.find((field) => field.name === "log.target");
if (field) {
target = processFieldValue(field.value);
}
}

return (
<div
class={clsx(
"p-1 font-mono text-sm items-center flex gap-4 odd:bg-slate-900 group",
levelStyle ? levelStyle : "border-b-gray-800 text-white"
)}
>
<Show when={props.showTimestamp}>
<time
dateTime={timeDate.toISOString()}
class={clsx(
levelStyle
? levelStyle
: "text-slate-400 group-hover:text-slate-100",
"font-mono text-xs transition-colors"
)}
>
{formatTimestamp(timeDate)}
</time>
</Show>
<span class="group-hover:text-white text-slate-300 transition-colors">
{message}
</span>
<span class="ml-auto flex gap-2 items-center text-xs">
<Show when={target}>
{(logTarget) => (
<span class="text-slate-400 group-hover:text-slate-100 transition-colors">
{logTarget()}
</span>
)}
</Show>
<Show when={metadata?.location?.file}>
{(filePath) => (
<>
{getFileNameFromPath(filePath())}:{metadata?.location?.line ?? ""}
</>
)}
</Show>
</span>
</div>
);
}
87 changes: 9 additions & 78 deletions clients/web/src/views/dashboard/console.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { For, Show, createSignal } from "solid-js";
import { AutoscrollPane } from "~/components/autoscroll-pane";
import { createSignal } from "solid-js";
import { AutoScrollPane } from "~/components/auto-scroll-pane";
import { FilterToggle } from "~/components/filter-toggle";
import { formatTimestamp, timestampToDate } from "~/lib/formatters";
import { Toolbar } from "~/components/toolbar";
import { useMonitor } from "~/context/monitor-provider";
import clsx from "clsx";
import { createStore } from "solid-js/store";
import { getLogMetadata } from "~/lib/console/get-log-metadata";
import { LogFilterObject, filterLogs } from "~/lib/console/filter-logs";
import { getLevelClasses } from "~/lib/console/get-level-classes";
import { LogLevelFilter } from "~/components/console/log-level-filter";
import { NoLogs } from "~/components/console/no-logs";
import { getFileNameFromPath } from "~/lib/console/get-file-name-from-path";
import { processFieldValue } from "~/lib/span/process-field-value.ts";
import { LogEventEntry } from "~/components/console/log-event-entry";

export default function Console() {
const { monitorData } = useMonitor();
Expand Down Expand Up @@ -54,77 +49,13 @@ export default function Console() {
<span>Autoscroll</span>
</FilterToggle>
</Toolbar>
<AutoscrollPane
dataStream={filteredLogs()[0]}
<AutoScrollPane
dataStream={filteredLogs()}
displayComponent={LogEventEntry}
displayOptions={{ showTimestamp: showTimestamp() }}
shouldAutoScroll={shouldAutoScroll}
>
<Show when={filteredLogs().length === 0}>
<NoLogs filter={filter} reset={resetFilter} />
</Show>
<For each={filteredLogs()}>
{(logEvent) => {
const { message, at } = logEvent;
if (!at) return null;

const metadata = getLogMetadata(monitorData, logEvent);
const timeDate = timestampToDate(at);
const levelStyle = getLevelClasses(metadata?.level);

let target = metadata?.target;
if (target === "log") {
const field = logEvent.fields.find(
(field) => field.name === "log.target"
);
if (field) {
target = processFieldValue(field.value);
}
}

return (
<li
class={clsx(
"p-1 font-mono text-sm items-center flex gap-4 odd:bg-slate-900 group",
levelStyle ? levelStyle : "border-b-gray-800 text-white"
)}
>
<Show when={showTimestamp()}>
<time
dateTime={timeDate.toISOString()}
class={clsx(
levelStyle
? levelStyle
: "text-slate-400 group-hover:text-slate-100",
"font-mono text-xs transition-colors"
)}
>
{formatTimestamp(timeDate)}
</time>
</Show>
<span class="group-hover:text-white text-slate-300 transition-colors">
{message}
</span>
<span class="ml-auto flex gap-2 items-center text-xs">
<Show when={target}>
{(logTarget) => (
<span class="text-slate-400 group-hover:text-slate-100 transition-colors">
{logTarget()}
</span>
)}
</Show>
<Show when={metadata?.location?.file}>
{(filePath) => (
<>
{getFileNameFromPath(filePath())}:
{metadata?.location?.line ?? ""}
</>
)}
</Show>
</span>
</li>
);
}}
</For>
</AutoscrollPane>
fallback={<NoLogs filter={filter} reset={resetFilter} />}
/>
</>
);
}

0 comments on commit 1e253f2

Please sign in to comment.