atmosphere explorer

new stream layout

handle.invalid 143d88d2 778c79d9

verified
+155 -56
+155 -56
src/views/stream/index.tsx
··· 3 3 import { A, useLocation, useSearchParams } from "@solidjs/router"; 4 4 import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; 5 5 import { Button } from "../../components/button"; 6 + import DidHoverCard from "../../components/hover-card/did"; 6 7 import { JSONValue } from "../../components/json"; 7 - import { StickyOverlay } from "../../components/sticky"; 8 8 import { TextInput } from "../../components/text-input"; 9 + import { addToClipboard } from "../../utils/copy"; 10 + import { localDateFromTimestamp } from "../../utils/date"; 9 11 import { StreamStats, StreamStatsPanel } from "./stats"; 10 12 11 13 const LIMIT = 20; 12 14 type Parameter = { name: string; param: string | string[] | undefined }; 13 15 16 + const StreamRecordItem = (props: { record: any; streamType: "jetstream" | "firehose" }) => { 17 + const [expanded, setExpanded] = createSignal(false); 18 + 19 + const getBasicInfo = () => { 20 + const rec = props.record; 21 + if (props.streamType === "jetstream") { 22 + const collection = rec.commit?.collection || rec.kind; 23 + const rkey = rec.commit?.rkey; 24 + const action = rec.commit?.operation; 25 + const time = rec.time_us ? localDateFromTimestamp(rec.time_us / 1000) : undefined; 26 + return { type: rec.kind, did: rec.did, collection, rkey, action, time }; 27 + } else { 28 + const type = rec.$type?.split("#").pop() || rec.$type; 29 + const did = rec.repo ?? rec.did; 30 + const pathParts = rec.op?.path?.split("/") || []; 31 + const collection = pathParts[0]; 32 + const rkey = pathParts[1]; 33 + const time = rec.time ? localDateFromTimestamp(Date.parse(rec.time)) : undefined; 34 + return { type, did, collection, rkey, action: rec.op?.action, time }; 35 + } 36 + }; 37 + 38 + const info = getBasicInfo(); 39 + 40 + const typeColors: Record<string, string> = { 41 + create: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300", 42 + update: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300", 43 + delete: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300", 44 + identity: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", 45 + account: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", 46 + sync: "bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300", 47 + }; 48 + 49 + const copyRecord = (e: MouseEvent) => { 50 + e.stopPropagation(); 51 + addToClipboard(JSON.stringify(props.record, null, 2)); 52 + }; 53 + 54 + return ( 55 + <div class="flex flex-col gap-2"> 56 + <div class="flex items-start gap-1"> 57 + <button 58 + type="button" 59 + onclick={() => setExpanded(!expanded())} 60 + class="dark:hover:bg-dark-200 flex min-w-0 flex-1 items-start gap-2 rounded p-1 text-left hover:bg-neutral-200/70" 61 + > 62 + <span class="mt-0.5 shrink-0 text-neutral-400 dark:text-neutral-500"> 63 + {expanded() ? 64 + <span class="iconify lucide--chevron-down"></span> 65 + : <span class="iconify lucide--chevron-right"></span>} 66 + </span> 67 + <div class="flex min-w-0 flex-1 flex-col gap-0.5"> 68 + <div class="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 sm:gap-x-2"> 69 + <span 70 + class={`rounded px-1.5 py-0.5 text-xs font-medium ${typeColors[info.type === "commit" ? info.action : info.type] || "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-300"}`} 71 + > 72 + {info.type === "commit" ? info.action : info.type} 73 + </span> 74 + <Show when={info.collection && info.collection !== info.type}> 75 + <span class="text-neutral-600 dark:text-neutral-300">{info.collection}</span> 76 + </Show> 77 + <Show when={info.rkey}> 78 + <span class="text-neutral-400 dark:text-neutral-500">{info.rkey}</span> 79 + </Show> 80 + </div> 81 + <div class="flex flex-col gap-x-2 gap-y-0.5 text-xs text-neutral-500 sm:flex-row sm:items-center dark:text-neutral-400"> 82 + <Show when={info.did}> 83 + <span class="w-fit" onclick={(e) => e.stopPropagation()}> 84 + <DidHoverCard newTab did={info.did} /> 85 + </span> 86 + </Show> 87 + <Show when={info.time}> 88 + <span>{info.time}</span> 89 + </Show> 90 + </div> 91 + </div> 92 + </button> 93 + <Show when={expanded()}> 94 + <button 95 + type="button" 96 + onclick={copyRecord} 97 + class="flex size-6 shrink-0 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-200 hover:text-neutral-600 active:bg-neutral-300 sm:size-7 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:active:bg-neutral-600" 98 + > 99 + <span class="iconify lucide--copy"></span> 100 + </button> 101 + </Show> 102 + </div> 103 + <Show when={expanded()}> 104 + <div class="ml-6.5"> 105 + <div class="w-full text-xs wrap-anywhere whitespace-pre-wrap md:w-2xl"> 106 + <JSONValue newTab data={props.record} repo={info.did} hideBlobs /> 107 + </div> 108 + </div> 109 + </Show> 110 + </div> 111 + ); 112 + }; 113 + 14 114 const StreamView = () => { 15 115 const [searchParams, setSearchParams] = useSearchParams(); 16 116 const [parameters, setParameters] = createSignal<Parameter[]>([]); 17 117 const streamType = useLocation().pathname === "/firehose" ? "firehose" : "jetstream"; 18 - const [records, setRecords] = createSignal<Array<any>>([]); 118 + const [records, setRecords] = createSignal<any[]>([]); 19 119 const [connected, setConnected] = createSignal(false); 20 120 const [paused, setPaused] = createSignal(false); 21 121 const [notice, setNotice] = createSignal(""); ··· 266 366 return ( 267 367 <> 268 368 <Title>{streamType === "firehose" ? "Firehose" : "Jetstream"} - PDSls</Title> 269 - <div class="flex w-full flex-col items-center"> 369 + <div class="flex w-full flex-col items-center gap-2"> 270 370 <div class="flex gap-4 font-medium"> 271 371 <A 272 372 class="flex items-center gap-1 border-b-2" ··· 284 384 </A> 285 385 </div> 286 386 <Show when={!connected()}> 287 - <form ref={formRef} class="mt-4 mb-4 flex w-full flex-col gap-1.5 px-2 text-sm"> 387 + <form ref={formRef} class="flex w-full flex-col gap-1.5 p-2 text-sm"> 288 388 <label class="flex items-center justify-end gap-x-1"> 289 389 <span class="min-w-20">Instance</span> 290 390 <TextInput ··· 350 450 </form> 351 451 </Show> 352 452 <Show when={connected()}> 353 - <StickyOverlay> 354 - <div class="flex w-full flex-col gap-2 p-1"> 355 - <div class="flex flex-col gap-1 text-sm wrap-anywhere"> 356 - <div class="font-semibold">Parameters</div> 357 - <For each={parameters()}> 358 - {(param) => ( 359 - <Show when={param.param}> 360 - <div class="text-sm"> 361 - <div class="text-xs text-neutral-500 dark:text-neutral-400"> 362 - {param.name} 363 - </div> 364 - <div class="text-neutral-700 dark:text-neutral-300">{param.param}</div> 365 - </div> 366 - </Show> 367 - )} 368 - </For> 369 - </div> 370 - <StreamStatsPanel stats={stats()} currentTime={currentTime()} /> 371 - <div class="flex justify-end gap-2"> 372 - <button 373 - type="button" 374 - ontouchstart={(e) => { 375 - e.preventDefault(); 376 - requestAnimationFrame(() => togglePause()); 377 - }} 378 - onclick={togglePause} 379 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 380 - > 381 - {paused() ? "Resume" : "Pause"} 382 - </button> 383 - <button 384 - type="button" 385 - ontouchstart={(e) => { 386 - e.preventDefault(); 387 - requestAnimationFrame(() => disconnect()); 388 - }} 389 - onclick={disconnect} 390 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 391 - > 392 - Disconnect 393 - </button> 394 - </div> 453 + <div class="flex w-full flex-col gap-2 p-2"> 454 + <div class="flex flex-col gap-1 text-sm wrap-anywhere"> 455 + <div class="font-semibold">Parameters</div> 456 + <For each={parameters()}> 457 + {(param) => ( 458 + <Show when={param.param}> 459 + <div class="text-sm"> 460 + <div class="text-xs text-neutral-500 dark:text-neutral-400">{param.name}</div> 461 + <div class="text-neutral-700 dark:text-neutral-300">{param.param}</div> 462 + </div> 463 + </Show> 464 + )} 465 + </For> 466 + </div> 467 + <StreamStatsPanel stats={stats()} currentTime={currentTime()} /> 468 + <div class="flex justify-end gap-2"> 469 + <button 470 + type="button" 471 + ontouchstart={(e) => { 472 + e.preventDefault(); 473 + requestAnimationFrame(() => togglePause()); 474 + }} 475 + onclick={togglePause} 476 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 477 + > 478 + {paused() ? "Resume" : "Pause"} 479 + </button> 480 + <button 481 + type="button" 482 + ontouchstart={(e) => { 483 + e.preventDefault(); 484 + requestAnimationFrame(() => disconnect()); 485 + }} 486 + onclick={disconnect} 487 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 488 + > 489 + Disconnect 490 + </button> 395 491 </div> 396 - </StickyOverlay> 492 + </div> 397 493 </Show> 398 494 <Show when={notice().length}> 399 495 <div class="text-red-500 dark:text-red-400">{notice()}</div> 400 496 </Show> 401 - <div class="flex w-full flex-col gap-2 divide-y-[0.5px] divide-neutral-500 font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:text-sm md:w-3xl"> 402 - <For each={records().toReversed()}> 403 - {(rec) => ( 404 - <div class="pb-2"> 405 - <JSONValue data={rec} repo={rec.did ?? rec.repo} hideBlobs /> 406 - </div> 407 - )} 408 - </For> 409 - </div> 497 + <Show when={connected() || records().length > 0}> 498 + <div class="flex min-h-280 w-full flex-col gap-2 font-mono text-xs [overflow-anchor:auto] sm:text-sm"> 499 + <For each={records().toReversed()}> 500 + {(rec) => ( 501 + <div class="[overflow-anchor:none]"> 502 + <StreamRecordItem record={rec} streamType={streamType} /> 503 + </div> 504 + )} 505 + </For> 506 + <div class="h-px [overflow-anchor:auto]" /> 507 + </div> 508 + </Show> 410 509 </div> 411 510 </> 412 511 );