branch:
LogPanel.tsx
3306 bytesRaw
import { useEffect, useRef } from "react";
import { TrashIcon } from "@phosphor-icons/react";
import { Button, Surface } from "@cloudflare/kumo";

export interface LogEntry {
  id: string;
  timestamp: Date;
  direction: "in" | "out" | "error" | "info";
  type: string;
  data: unknown;
}

interface LogPanelProps {
  logs: LogEntry[];
  onClear: () => void;
  maxHeight?: string;
}

export function LogPanel({
  logs,
  onClear,
  maxHeight = "300px"
}: LogPanelProps) {
  const scrollRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [logs]);

  const getLogClass = (direction: LogEntry["direction"]) => {
    switch (direction) {
      case "in":
        return "log-entry log-entry-in";
      case "out":
        return "log-entry log-entry-out";
      case "error":
        return "log-entry log-entry-error";
      case "info":
        return "log-entry log-entry-info";
      default:
        return "log-entry";
    }
  };

  const getDirectionLabel = (direction: LogEntry["direction"]) => {
    switch (direction) {
      case "in":
        return "←";
      case "out":
        return "→";
      case "error":
        return "✕";
      default:
        return "•";
    }
  };

  const getDirectionColor = (direction: LogEntry["direction"]) => {
    switch (direction) {
      case "in":
        return "text-kumo-success";
      case "out":
        return "text-kumo-info";
      case "error":
        return "text-kumo-danger";
      default:
        return "text-kumo-subtle";
    }
  };

  return (
    <Surface className="overflow-hidden rounded-lg ring ring-kumo-line">
      <div className="flex items-center justify-between px-3 py-2 border-b border-kumo-line bg-kumo-elevated">
        <span className="text-xs font-semibold uppercase tracking-wider text-kumo-subtle">
          Event Log
        </span>
        <Button
          variant="ghost"
          shape="square"
          size="xs"
          aria-label="Clear logs"
          icon={<TrashIcon size={14} />}
          onClick={onClear}
          title="Clear logs"
        />
      </div>

      <div ref={scrollRef} className="overflow-y-auto" style={{ maxHeight }}>
        {logs.length === 0 ? (
          <div className="px-3 py-3 text-xs text-kumo-inactive">
            Waiting for events…
          </div>
        ) : (
          logs.map((log) => (
            <div key={log.id} className={getLogClass(log.direction)}>
              <span className="text-kumo-inactive">
                {log.timestamp.toLocaleTimeString()}
              </span>
              <span
                className={`mx-2 font-bold ${getDirectionColor(log.direction)}`}
              >
                {getDirectionLabel(log.direction)}
              </span>
              <span className="font-semibold text-kumo-default">
                {log.type}
              </span>
              {log.data !== undefined && (
                <span className="ml-2 text-kumo-subtle">
                  {typeof log.data === "string"
                    ? log.data
                    : JSON.stringify(log.data)}
                </span>
              )}
            </div>
          ))
        )}
      </div>
    </Surface>
  );
}