Skip to content
Open
Show file tree
Hide file tree
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
70 changes: 59 additions & 11 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"msw:init": "msw init public/"
},
"dependencies": {
"@tanstack/react-hotkeys": "^0.10.0",
"clsx": "^2.1.1",
"json-bigint": "^1.0.0",
"react": "^19.2.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import type { ReactNode } from "react";
import CopyIconButton from "./CopyIconButton";
import {
UI_BODY_MUTED_ITALIC_CLASS,
Expand Down Expand Up @@ -47,17 +48,21 @@ export const PANEL_DIFF_AFTER_VALUE_CLASS = `${PANEL_DIFF_VALUE_BASE_CLASS} bg-g

export const DEFAULT_COLLAPSE_LINES = 15;

const colorParseCtx = document.createElement("canvas").getContext("2d");
const colorParseContext = document.createElement("canvas").getContext("2d");

function stripAlpha(color) {
if (typeof color !== "string") return color;
colorParseCtx.fillStyle = color;
const m = colorParseCtx.fillStyle.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/);
const stripAlpha = (color: string | null): string | null => {
if (color === null || colorParseContext === null) return color;
colorParseContext.fillStyle = color;
const normalizedColor = colorParseContext.fillStyle;
if (typeof normalizedColor !== "string") return color;
const match = normalizedColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/);

return m ? `rgb(${m[1]}, ${m[2]}, ${m[3]})` : colorParseCtx.fillStyle;
}
return match
? `rgb(${String(match[1])}, ${String(match[2])}, ${String(match[3])})`
: normalizedColor;
};

function renderBreakablePath(path) {
const renderBreakablePath = (path: string) => {
const segments = path.split("/");
return segments.map((segment, i) => (
<span key={i}>
Expand All @@ -69,9 +74,23 @@ function renderBreakablePath(path) {
) : null}
</span>
));
};

interface PanelHeaderProps {
meta?: ReactNode;
preserveSubtitleEnd?: boolean;
subtitle?: string | null;
title: ReactNode;
titleColor?: string | null;
}

export function PanelHeader({ title, titleColor, subtitle, meta }) {
export const PanelHeader = ({
title,
titleColor = null,
subtitle = null,
meta = null,
preserveSubtitleEnd = false,
}: PanelHeaderProps) => {
const opaqueColor = stripAlpha(titleColor);
return (
<div className="min-w-0 pr-4">
Expand All @@ -82,43 +101,71 @@ export function PanelHeader({ title, titleColor, subtitle, meta }) {
{title}
</div>
{subtitle ? (
<div className={PANEL_SUBTITLE_CLASS}>
{renderBreakablePath(subtitle)}
</div>
preserveSubtitleEnd ? (
<div
className={`${PANEL_SUBTITLE_CLASS} overflow-hidden text-ellipsis whitespace-nowrap text-left`}
style={{
direction: "rtl",
textAlign: "left",
textOverflow: "ellipsis",
}}
title={subtitle}
>
{"\u202A" + subtitle + "\u202C"}
</div>
) : (
<div className={PANEL_SUBTITLE_CLASS}>
{renderBreakablePath(subtitle)}
</div>
)
) : null}
{meta ? <div className={PANEL_META_CLASS}>{meta}</div> : null}
</div>
);
};

interface PanelSectionTitleProps {
children: ReactNode;
className?: string;
}

export function PanelSectionTitle({ children, className = "" }) {
export const PanelSectionTitle = ({
children,
className = "",
}: PanelSectionTitleProps) => {
return (
<div className={`${PANEL_SECTION_TITLE_CLASS} ${className}`}>
{children}
</div>
);
}
};

export function isEmptyValue(value) {
if (value == null || value === "") return true;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === "object") return Object.keys(value).length === 0;
return false;
interface PanelDetailRowProps {
collapseLineCount?: number;
label: ReactNode;
relaxedCollapse?: boolean;
value: unknown;
}

export function PanelDetailRow({
export const PanelDetailRow = ({
label,
value,
relaxedCollapse = false,
collapseLineCount = DEFAULT_COLLAPSE_LINES,
}) {
const displayValue =
typeof value === "object" && value !== null
? JSON.stringify(value, null, 2)
: value;

const textToCopy =
displayValue != null && displayValue !== "" ? String(displayValue) : "";
}: PanelDetailRowProps) => {
const textToCopy = (() => {
if (value === null || value === undefined || value === "") return "";
if (typeof value === "object") return JSON.stringify(value, null, 2);
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "bigint" ||
typeof value === "boolean"
) {
return String(value);
}
return "";
})();
const hasValue = textToCopy !== "";
const lineCount = hasValue ? textToCopy.split("\n").length : 0;
const isCollapsible = lineCount > collapseLineCount;
Expand All @@ -131,10 +178,12 @@ export function PanelDetailRow({
{isCollapsible && (
<button
type="button"
onClick={() => setIsCollapsed((p) => !p)}
onClick={() => {
setIsCollapsed((previous) => !previous);
}}
className={PANEL_COLLAPSE_TOGGLE_CLASS}
>
{isCollapsed ? `(${lineCount} lines) ▼` : "▲"}
{isCollapsed ? `(${String(lineCount)} lines) ▼` : "▲"}
</button>
)}
</div>
Expand Down Expand Up @@ -165,4 +214,4 @@ export function PanelDetailRow({
</div>
</div>
);
}
};
Loading
Loading