-
Notifications
You must be signed in to change notification settings - Fork 1
⚡ Bolt: Optimize LogsView performance #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||
| * Logs view component — logs viewer with filtering. | ||||||
| */ | ||||||
|
|
||||||
| import { useEffect, useMemo, useState } from "react"; | ||||||
| import { memo, useEffect, useMemo, useState } from "react"; | ||||||
| import { useApp } from "../AppContext"; | ||||||
| import type { LogEntry } from "../api-client"; | ||||||
| import { formatTime } from "./shared/format"; | ||||||
|
|
@@ -18,6 +18,63 @@ const TAG_COLORS: Record<string, { bg: string; fg: string }> = { | |||||
| websocket: { bg: "rgba(20, 184, 166, 0.15)", fg: "rgb(20, 184, 166)" }, | ||||||
| }; | ||||||
|
|
||||||
| const LogEntryItem = memo(({ entry }: { entry: LogEntry }) => { | ||||||
| return ( | ||||||
| <div | ||||||
| className="font-mono text-xs px-2 py-1 border-b border-border flex gap-2 items-baseline" | ||||||
| data-testid="log-entry" | ||||||
| > | ||||||
| {/* Timestamp */} | ||||||
| <span className="text-muted whitespace-nowrap"> | ||||||
| {formatTime(entry.timestamp, { fallback: "—" })} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Level */} | ||||||
| <span | ||||||
| className={`font-semibold w-[44px] uppercase text-[11px] ${ | ||||||
| entry.level === "error" | ||||||
| ? "text-danger" | ||||||
| : entry.level === "warn" | ||||||
| ? "text-warn" | ||||||
| : "text-muted" | ||||||
| }`} | ||||||
| > | ||||||
| {entry.level} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Source */} | ||||||
| <span className="text-muted w-16 overflow-hidden text-ellipsis whitespace-nowrap text-[11px]"> | ||||||
| [{entry.source}] | ||||||
| </span> | ||||||
|
|
||||||
| {/* Tag badges */} | ||||||
| <span className="inline-flex gap-0.5 shrink-0"> | ||||||
| {(entry.tags ?? []).map((t: string) => { | ||||||
| const c = TAG_COLORS[t]; | ||||||
| return ( | ||||||
| <span | ||||||
| key={t} | ||||||
| className="inline-block text-[10px] px-1.5 py-px rounded-lg mr-0.5" | ||||||
| style={{ | ||||||
| background: c ? c.bg : "var(--bg-muted)", | ||||||
| color: c ? c.fg : "var(--muted)", | ||||||
| fontFamily: "var(--font-body, sans-serif)", | ||||||
| }} | ||||||
| > | ||||||
| {t} | ||||||
| </span> | ||||||
| ); | ||||||
| })} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Message */} | ||||||
| <span className="flex-1 break-all">{entry.message}</span> | ||||||
| </div> | ||||||
| ); | ||||||
| }); | ||||||
|
|
||||||
| LogEntryItem.displayName = "LogEntryItem"; | ||||||
|
|
||||||
| export function LogsView() { | ||||||
| const [searchQuery, setSearchQuery] = useState(""); | ||||||
|
|
||||||
|
|
@@ -163,57 +220,10 @@ export function LogsView() { | |||||
| </div> | ||||||
| ) : ( | ||||||
| filteredLogs.map((entry: LogEntry) => ( | ||||||
| <div | ||||||
| <LogEntryItem | ||||||
| key={`${entry.timestamp}-${entry.source}-${entry.level}-${entry.message}`} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Recommendation: If possible, use a unique, stable identifier for each log entry (such as an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the full log message for the I suggest truncating the message to a reasonable length to create a more performant key while still maintaining a high degree of uniqueness.
Suggested change
|
||||||
| className="font-mono text-xs px-2 py-1 border-b border-border flex gap-2 items-baseline" | ||||||
| data-testid="log-entry" | ||||||
| > | ||||||
| {/* Timestamp */} | ||||||
| <span className="text-muted whitespace-nowrap"> | ||||||
| {formatTime(entry.timestamp, { fallback: "—" })} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Level */} | ||||||
| <span | ||||||
| className={`font-semibold w-[44px] uppercase text-[11px] ${ | ||||||
| entry.level === "error" | ||||||
| ? "text-danger" | ||||||
| : entry.level === "warn" | ||||||
| ? "text-warn" | ||||||
| : "text-muted" | ||||||
| }`} | ||||||
| > | ||||||
| {entry.level} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Source */} | ||||||
| <span className="text-muted w-16 overflow-hidden text-ellipsis whitespace-nowrap text-[11px]"> | ||||||
| [{entry.source}] | ||||||
| </span> | ||||||
|
|
||||||
| {/* Tag badges */} | ||||||
| <span className="inline-flex gap-0.5 shrink-0"> | ||||||
| {(entry.tags ?? []).map((t: string) => { | ||||||
| const c = TAG_COLORS[t]; | ||||||
| return ( | ||||||
| <span | ||||||
| key={t} | ||||||
| className="inline-block text-[10px] px-1.5 py-px rounded-lg mr-0.5" | ||||||
| style={{ | ||||||
| background: c ? c.bg : "var(--bg-muted)", | ||||||
| color: c ? c.fg : "var(--muted)", | ||||||
| fontFamily: "var(--font-body, sans-serif)", | ||||||
| }} | ||||||
| > | ||||||
| {t} | ||||||
| </span> | ||||||
| ); | ||||||
| })} | ||||||
| </span> | ||||||
|
|
||||||
| {/* Message */} | ||||||
| <span className="flex-1 break-all">{entry.message}</span> | ||||||
| </div> | ||||||
| entry={entry} | ||||||
| /> | ||||||
| )) | ||||||
| )} | ||||||
| </div> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import React from "react"; | ||
| import { act, create, type ReactTestRenderer } from "react-test-renderer"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import * as AppContext from "../../src/AppContext"; | ||
| import { LogsView } from "../../src/components/LogsView"; | ||
|
|
||
| // Mock the AppContext | ||
| vi.mock("../../src/AppContext", () => ({ | ||
| useApp: vi.fn(), | ||
| })); | ||
|
|
||
| describe("LogsView", () => { | ||
| it("renders logs correctly", async () => { | ||
| const mockUseApp = { | ||
| logs: [ | ||
| { | ||
| timestamp: 1234567890000, | ||
| source: "test-source", | ||
| level: "info", | ||
| message: "Test log message", | ||
| tags: ["test-tag"], | ||
| }, | ||
| ], | ||
| logSources: ["test-source"], | ||
| logTags: ["test-tag"], | ||
| logTagFilter: "", | ||
| logLevelFilter: "", | ||
| logSourceFilter: "", | ||
| loadLogs: vi.fn(), | ||
| setState: vi.fn(), | ||
| }; | ||
|
|
||
| // @ts-expect-error - partial mock | ||
| vi.spyOn(AppContext, "useApp").mockReturnValue(mockUseApp); | ||
|
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using A better approach would be to create a test utility function that generates a complete, type-safe mock object for For example: import type { AppContextValue } from '../../src/AppContext';
const createMockUseApp = (overrides: Partial<AppContextValue> = {}): AppContextValue => {
const defaultMock: AppContextValue = {
// ... all properties with default mock values
};
return { ...defaultMock, ...overrides };
}
// in test
vi.spyOn(AppContext, 'useApp').mockReturnValue(createMockUseApp({ logs: mockLogs }));This would remove the need for |
||
|
|
||
| let testRenderer: ReactTestRenderer | undefined; | ||
| await act(async () => { | ||
| testRenderer = create(<LogsView />); | ||
| }); | ||
|
|
||
| if (!testRenderer) throw new Error("Renderer not initialized"); | ||
|
|
||
| const root = testRenderer.root; | ||
| // Check if log entry exists | ||
| const logEntries = root.findAllByProps({ "data-testid": "log-entry" }); | ||
| expect(logEntries.length).toBe(1); | ||
|
|
||
| // Check content | ||
| // Check for text content in the rendered tree | ||
| const treeJson = JSON.stringify(testRenderer.toJSON()); | ||
| expect(treeJson).toContain("Test log message"); | ||
| expect(treeJson).toContain("info"); | ||
| expect(treeJson).toContain("test-source"); | ||
|
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asserting against a stringified JSON of the entire component tree is brittle. Minor changes to the component's structure, class names, or unrelated text could break this test unexpectedly. For more robust assertions, it's better to query for specific elements or text content. With For example, you could find the specific const messageSpan = root.find(
(node) => node.type === 'span' && node.props.className?.includes('flex-1')
);
expect(messageSpan.children).toEqual(['Test log message']); |
||
| }); | ||
|
|
||
| it("renders 'No log entries' when empty", async () => { | ||
| const mockUseApp = { | ||
| logs: [], | ||
| logSources: [], | ||
| logTags: [], | ||
| logTagFilter: "", | ||
| logLevelFilter: "", | ||
| logSourceFilter: "", | ||
| loadLogs: vi.fn(), | ||
| setState: vi.fn(), | ||
| }; | ||
|
|
||
| // @ts-expect-error - partial mock | ||
| vi.spyOn(AppContext, "useApp").mockReturnValue(mockUseApp); | ||
|
|
||
| let testRenderer: ReactTestRenderer | undefined; | ||
| await act(async () => { | ||
| testRenderer = create(<LogsView />); | ||
| }); | ||
|
|
||
| if (!testRenderer) throw new Error("Renderer not initialized"); | ||
|
|
||
| const root = testRenderer.root; | ||
| const logEntries = root.findAllByProps({ "data-testid": "log-entry" }); | ||
| expect(logEntries.length).toBe(0); | ||
|
|
||
| const treeJson = JSON.stringify(testRenderer.toJSON()); | ||
| expect(treeJson).toContain("No log entries"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{entry.message}. If log messages can contain HTML or script content from untrusted sources, this could expose the application to XSS vulnerabilities.Recommendation: Ensure that log messages are sanitized or escaped before rendering. If you are certain that log messages are always plain text, this risk is mitigated, but if not, consider using a sanitization library or rendering with
dangerouslySetInnerHTMLonly after proper sanitization.