atproto explorer

new repo tabs

handle.invalid 785a22bf 143a70c2

verified
+327 -335
+18
src/components/dropdown.tsx
··· 59 59 ); 60 60 }; 61 61 62 + export const ActionMenu = (props: { 63 + label: string; 64 + icon: string; 65 + onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 66 + }) => { 67 + return ( 68 + <button 69 + onClick={props.onClick} 70 + class="flex items-center gap-1.5 rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 71 + > 72 + <Show when={props.icon}> 73 + <span class={"iconify shrink-0 " + props.icon}></span> 74 + </Show> 75 + <span class="whitespace-nowrap">{props.label}</span> 76 + </button> 77 + ); 78 + }; 79 + 62 80 export const DropdownMenu = (props: { 63 81 icon: string; 64 82 buttonClass?: string;
+155
src/views/logs.tsx
··· 1 + import { 2 + CompatibleOperationOrTombstone, 3 + defs, 4 + IndexedEntry, 5 + processIndexedEntryLog, 6 + } from "@atcute/did-plc"; 7 + import { createResource, createSignal, For, Show } from "solid-js"; 8 + import Tooltip from "../components/tooltip.jsx"; 9 + import { localDateFromTimestamp } from "../utils/date.js"; 10 + import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 11 + 12 + type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 13 + 14 + export const PlcLogView = (props: { did: string }) => { 15 + const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 16 + 17 + const fetchPlcLogs = async () => { 18 + const res = await fetch( 19 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`, 20 + ); 21 + const json = await res.json(); 22 + const logs = defs.indexedEntryLog.parse(json); 23 + try { 24 + await processIndexedEntryLog(props.did as any, logs); 25 + } catch (e) { 26 + console.error(e); 27 + } 28 + const opHistory = createOperationHistory(logs).reverse(); 29 + return Array.from(groupBy(opHistory, (item) => item.orig)); 30 + }; 31 + 32 + const [plcOps] = 33 + createResource<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(fetchPlcLogs); 34 + 35 + const FilterButton = (props: { icon: string; event: PlcEvent }) => ( 36 + <button 37 + classList={{ 38 + "flex items-center rounded-full p-1.5": true, 39 + "bg-neutral-700 dark:bg-neutral-200": activePlcEvent() === props.event, 40 + }} 41 + onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)} 42 + > 43 + <span 44 + class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`} 45 + ></span> 46 + </button> 47 + ); 48 + 49 + const DiffItem = (props: { diff: DiffEntry }) => { 50 + const diff = props.diff; 51 + let title = "Unknown log entry"; 52 + let icon = "lucide--circle-help"; 53 + let value = ""; 54 + 55 + if (diff.type === "identity_created") { 56 + icon = "lucide--bell"; 57 + title = `Identity created`; 58 + } else if (diff.type === "identity_tombstoned") { 59 + icon = "lucide--skull"; 60 + title = `Identity tombstoned`; 61 + } else if (diff.type === "handle_added" || diff.type === "handle_removed") { 62 + icon = "lucide--at-sign"; 63 + title = diff.type === "handle_added" ? "Alias added" : "Alias removed"; 64 + value = diff.handle; 65 + } else if (diff.type === "handle_changed") { 66 + icon = "lucide--at-sign"; 67 + title = "Alias updated"; 68 + value = `${diff.prev_handle} → ${diff.next_handle}`; 69 + } else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") { 70 + icon = "lucide--key-round"; 71 + title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed"; 72 + value = diff.rotation_key; 73 + } else if (diff.type === "service_added" || diff.type === "service_removed") { 74 + icon = "lucide--hard-drive"; 75 + title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`; 76 + value = `${diff.service_endpoint}`; 77 + } else if (diff.type === "service_changed") { 78 + icon = "lucide--hard-drive"; 79 + title = `Service ${diff.service_id} updated`; 80 + value = `${diff.prev_service_endpoint} → ${diff.next_service_endpoint}`; 81 + } else if ( 82 + diff.type === "verification_method_added" || 83 + diff.type === "verification_method_removed" 84 + ) { 85 + icon = "lucide--shield-check"; 86 + title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`; 87 + value = `${diff.method_key}`; 88 + } else if (diff.type === "verification_method_changed") { 89 + icon = "lucide--shield-check"; 90 + title = `Verification method ${diff.method_id} updated`; 91 + value = `${diff.prev_method_key} → ${diff.next_method_key}`; 92 + } 93 + 94 + return ( 95 + <div class="grid grid-cols-[min-content_1fr] items-center gap-x-1"> 96 + <div class={icon + ` iconify shrink-0`} /> 97 + <p 98 + classList={{ 99 + "font-semibold": true, 100 + "text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified, 101 + }} 102 + > 103 + {title} 104 + </p> 105 + <div></div> 106 + {value} 107 + </div> 108 + ); 109 + }; 110 + 111 + return ( 112 + <div class="flex w-full flex-col gap-2 wrap-anywhere"> 113 + <div class="flex items-center justify-between"> 114 + <div class="flex items-center gap-1"> 115 + <div class="iconify lucide--filter" /> 116 + <div class="dark:shadow-dark-800 dark:bg-dark-300 flex w-fit items-center rounded-full border-[0.5px] border-neutral-300 bg-neutral-50 shadow-xs dark:border-neutral-700"> 117 + <FilterButton icon="iconify lucide--at-sign" event="handle" /> 118 + <FilterButton icon="iconify lucide--key-round" event="rotation_key" /> 119 + <FilterButton icon="iconify lucide--hard-drive" event="service" /> 120 + <FilterButton icon="iconify lucide--shield-check" event="verification_method" /> 121 + </div> 122 + </div> 123 + <Tooltip text="Audit log"> 124 + <a 125 + href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`} 126 + target="_blank" 127 + class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 128 + > 129 + <span class="iconify lucide--external-link"></span> 130 + </a> 131 + </Tooltip> 132 + </div> 133 + <div class="flex flex-col gap-1 text-sm"> 134 + <For each={plcOps()}> 135 + {([entry, diffs]) => ( 136 + <Show 137 + when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))} 138 + > 139 + <div class="flex flex-col"> 140 + <span class="text-neutral-500 dark:text-neutral-400"> 141 + {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 142 + </span> 143 + {diffs.map((diff) => ( 144 + <Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}> 145 + <DiffItem diff={diff} /> 146 + </Show> 147 + ))} 148 + </div> 149 + </Show> 150 + )} 151 + </For> 152 + </div> 153 + </div> 154 + ); 155 + };
+154 -335
src/views/repo.tsx
··· 1 1 import { Client, CredentialManager } from "@atcute/client"; 2 2 import { parsePublicMultikey } from "@atcute/crypto"; 3 - import { 4 - CompatibleOperationOrTombstone, 5 - defs, 6 - IndexedEntry, 7 - processIndexedEntryLog, 8 - } from "@atcute/did-plc"; 9 3 import { DidDocument } from "@atcute/identity"; 10 4 import { ActorIdentifier, Did, Handle } from "@atcute/lexicons"; 11 5 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; ··· 20 14 } from "solid-js"; 21 15 import { createStore } from "solid-js/store"; 22 16 import { Backlinks } from "../components/backlinks.jsx"; 23 - import { Button } from "../components/button.jsx"; 17 + import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx"; 24 18 import { TextInput } from "../components/text-input.jsx"; 25 19 import Tooltip from "../components/tooltip.jsx"; 26 20 import { didDocCache, resolveHandle, resolvePDS, validateHandle } from "../utils/api.js"; 27 - import { localDateFromTimestamp } from "../utils/date.js"; 28 - import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 29 21 import { BlobView } from "./blob.jsx"; 30 - 31 - type Tab = "collections" | "backlinks" | "identity" | "blobs"; 32 - type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 33 - 34 - const PlcLogView = (props: { 35 - did: string; 36 - plcOps: [IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]; 37 - }) => { 38 - const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 39 - 40 - const FilterButton = (props: { icon: string; event: PlcEvent }) => ( 41 - <button 42 - classList={{ 43 - "flex items-center rounded-full p-1.5": true, 44 - "bg-neutral-700 dark:bg-neutral-200": activePlcEvent() === props.event, 45 - }} 46 - onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)} 47 - > 48 - <span 49 - class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`} 50 - ></span> 51 - </button> 52 - ); 53 - 54 - const DiffItem = (props: { diff: DiffEntry }) => { 55 - const diff = props.diff; 56 - let title = "Unknown log entry"; 57 - let icon = "lucide--circle-help"; 58 - let value = ""; 22 + import { PlcLogView } from "./logs.jsx"; 59 23 60 - if (diff.type === "identity_created") { 61 - icon = "lucide--bell"; 62 - title = `Identity created`; 63 - } else if (diff.type === "identity_tombstoned") { 64 - icon = "lucide--skull"; 65 - title = `Identity tombstoned`; 66 - } else if (diff.type === "handle_added" || diff.type === "handle_removed") { 67 - icon = "lucide--at-sign"; 68 - title = diff.type === "handle_added" ? "Alias added" : "Alias removed"; 69 - value = diff.handle; 70 - } else if (diff.type === "handle_changed") { 71 - icon = "lucide--at-sign"; 72 - title = "Alias updated"; 73 - value = `${diff.prev_handle} → ${diff.next_handle}`; 74 - } else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") { 75 - icon = "lucide--key-round"; 76 - title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed"; 77 - value = diff.rotation_key; 78 - } else if (diff.type === "service_added" || diff.type === "service_removed") { 79 - icon = "lucide--hard-drive"; 80 - title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`; 81 - value = `${diff.service_endpoint}`; 82 - } else if (diff.type === "service_changed") { 83 - icon = "lucide--hard-drive"; 84 - title = `Service ${diff.service_id} updated`; 85 - value = `${diff.prev_service_endpoint} → ${diff.next_service_endpoint}`; 86 - } else if ( 87 - diff.type === "verification_method_added" || 88 - diff.type === "verification_method_removed" 89 - ) { 90 - icon = "lucide--shield-check"; 91 - title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`; 92 - value = `${diff.method_key}`; 93 - } else if (diff.type === "verification_method_changed") { 94 - icon = "lucide--shield-check"; 95 - title = `Verification method ${diff.method_id} updated`; 96 - value = `${diff.prev_method_key} → ${diff.next_method_key}`; 97 - } 98 - 99 - return ( 100 - <div class="grid grid-cols-[min-content_1fr] items-center gap-x-1"> 101 - <div class={icon + ` iconify shrink-0`} /> 102 - <p 103 - classList={{ 104 - "font-semibold": true, 105 - "text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified, 106 - }} 107 - > 108 - {title} 109 - </p> 110 - <div></div> 111 - {value} 112 - </div> 113 - ); 114 - }; 115 - 116 - return ( 117 - <> 118 - <div class="flex items-center justify-between"> 119 - <div class="flex items-center gap-1"> 120 - <div class="iconify lucide--filter" /> 121 - <div class="dark:shadow-dark-800 dark:bg-dark-300 flex w-fit items-center rounded-full border-[0.5px] border-neutral-300 bg-neutral-50 shadow-xs dark:border-neutral-700"> 122 - <FilterButton icon="iconify lucide--at-sign" event="handle" /> 123 - <FilterButton icon="iconify lucide--key-round" event="rotation_key" /> 124 - <FilterButton icon="iconify lucide--hard-drive" event="service" /> 125 - <FilterButton icon="iconify lucide--shield-check" event="verification_method" /> 126 - </div> 127 - </div> 128 - <Tooltip text="Audit log"> 129 - <a 130 - href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`} 131 - target="_blank" 132 - class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 133 - > 134 - <span class="iconify lucide--external-link"></span> 135 - </a> 136 - </Tooltip> 137 - </div> 138 - <div class="flex flex-col gap-1 text-sm"> 139 - <For each={props.plcOps}> 140 - {([entry, diffs]) => ( 141 - <Show 142 - when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))} 143 - > 144 - <div class="flex flex-col"> 145 - <span class="text-neutral-500 dark:text-neutral-400"> 146 - {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 147 - </span> 148 - {diffs.map((diff) => ( 149 - <Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}> 150 - <DiffItem diff={diff} /> 151 - </Show> 152 - ))} 153 - </div> 154 - </Show> 155 - )} 156 - </For> 157 - </div> 158 - </> 159 - ); 160 - }; 161 - 24 + type Tab = "collections" | "backlinks" | "identity" | "blobs" | "logs"; 162 25 const RepoView = () => { 163 26 const params = useParams(); 164 27 const location = useLocation(); ··· 168 31 const [didDoc, setDidDoc] = createSignal<DidDocument>(); 169 32 const [nsids, setNsids] = createSignal<Record<string, { hidden: boolean; nsids: string[] }>>(); 170 33 const [filter, setFilter] = createSignal<string>(); 171 - const [plcOps, setPlcOps] = 172 - createSignal<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(); 173 - const [showPlcLogs, setShowPlcLogs] = createSignal(false); 174 - const [loading, setLoading] = createSignal(false); 175 - const [notice, setNotice] = createSignal<string>(); 176 34 const [validHandles, setValidHandles] = createStore<Record<string, boolean>>({}); 177 35 let rpc: Client; 178 36 let pds: string; 179 37 const did = params.repo; 180 38 181 - const RepoTab = (props: { tab: Tab; label: string; icon: string }) => ( 182 - <A class="group flex flex-1 justify-center" href={`/at://${params.repo}#${props.tab}`}> 39 + const RepoTab = (props: { tab: Tab; label: string }) => ( 40 + <A class="group flex justify-center" href={`/at://${params.repo}#${props.tab}`}> 183 41 <span 184 42 classList={{ 185 - "flex gap-1 items-center border-b-2": true, 43 + "flex flex-1 border-b-2": true, 186 44 "border-transparent group-hover:border-neutral-400 dark:group-hover:border-neutral-600": 187 45 (location.hash !== `#${props.tab}` && !!location.hash) || 188 46 (!location.hash && props.tab !== "collections"), 189 47 }} 190 48 > 191 - <span class={"iconify " + props.icon}></span> 192 49 {props.label} 193 50 </span> 194 51 </A> ··· 294 151 </div> 295 152 </Show> 296 153 <div 297 - class={`dark:shadow-dark-800 dark:bg-dark-300 flex ${error() ? "justify-around" : "justify-between"} rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700`} 154 + class={`dark:shadow-dark-800 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700`} 298 155 > 299 - <Show when={!error()}> 300 - <RepoTab tab="collections" label="Collections" icon="lucide--folder-open" /> 301 - </Show> 302 - <RepoTab tab="identity" label="Identity" icon="lucide--id-card" /> 303 - <Show when={!error()}> 304 - <RepoTab tab="blobs" label="Blobs" icon="lucide--file-digit" /> 305 - </Show> 306 - <RepoTab tab="backlinks" label="Backlinks" icon="lucide--send-to-back" /> 156 + <div class="flex gap-2 sm:gap-4"> 157 + <Show when={!error()}> 158 + <RepoTab tab="collections" label="Collections" /> 159 + </Show> 160 + <RepoTab tab="identity" label="Identity" /> 161 + <Show when={did.startsWith("did:plc")}> 162 + <RepoTab tab="logs" label="Logs" /> 163 + </Show> 164 + <Show when={!error()}> 165 + <RepoTab tab="blobs" label="Blobs" /> 166 + </Show> 167 + <RepoTab tab="backlinks" label="Backlinks" /> 168 + </div> 169 + <MenuProvider> 170 + <DropdownMenu 171 + icon="lucide--ellipsis-vertical" 172 + buttonClass="rounded-sm p-1" 173 + menuClass="top-8 p-2 text-sm" 174 + > 175 + <NavMenu 176 + href={`/jetstream?dids=${params.repo}`} 177 + label="Jetstream" 178 + icon="lucide--radio-tower" 179 + /> 180 + <Show when={error()?.length === 0 || error() === undefined}> 181 + <ActionMenu 182 + label="Export Repo" 183 + icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"} 184 + onClick={() => downloadRepo()} 185 + /> 186 + </Show> 187 + </DropdownMenu> 188 + </MenuProvider> 307 189 </div> 190 + <Show when={location.hash === "#logs"}> 191 + <ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}> 192 + <Suspense 193 + fallback={ 194 + <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 195 + } 196 + > 197 + <PlcLogView did={did} /> 198 + </Suspense> 199 + </ErrorBoundary> 200 + </Show> 308 201 <Show when={location.hash === "#backlinks"}> 309 202 <ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}> 310 203 <Suspense ··· 328 221 </ErrorBoundary> 329 222 </Show> 330 223 <Show when={nsids() && (!location.hash || location.hash === "#collections")}> 331 - <div class="flex items-center gap-1"> 332 - <Tooltip text="Jetstream"> 333 - <A 334 - href={`/jetstream?dids=${params.repo}`} 335 - class="-ml-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 336 - > 337 - <span class="iconify lucide--radio-tower text-lg"></span> 338 - </A> 339 - </Tooltip> 340 - <TextInput 341 - name="filter" 342 - placeholder="Filter collections" 343 - onInput={(e) => setFilter(e.currentTarget.value)} 344 - class="grow" 345 - /> 346 - </div> 224 + <TextInput 225 + name="filter" 226 + placeholder="Filter collections" 227 + onInput={(e) => setFilter(e.currentTarget.value)} 228 + class="grow" 229 + /> 347 230 <div class="flex flex-col font-mono"> 348 231 <div class="grid grid-cols-[min-content_1fr] items-center gap-x-2 overflow-hidden text-sm"> 349 232 <For ··· 399 282 <Show when={location.hash === "#identity"}> 400 283 <Show when={didDoc()}> 401 284 {(didDocument) => ( 402 - <div class="flex flex-col gap-y-2 wrap-anywhere"> 403 - <div class="flex flex-col gap-y-1"> 404 - <div class="flex items-baseline justify-between gap-2"> 405 - <div> 406 - <div class="flex items-center gap-1"> 407 - <div class="iconify lucide--id-card" /> 408 - <p class="font-semibold">ID</p> 409 - </div> 410 - <div class="text-sm">{didDocument().id}</div> 411 - </div> 412 - <Tooltip text="DID document"> 413 - <a 414 - href={ 415 - did.startsWith("did:plc") ? 416 - `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 417 - : `https://${did.split("did:web:")[1]}/.well-known/did.json` 418 - } 419 - target="_blank" 420 - class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 421 - > 422 - <span class="iconify lucide--external-link"></span> 423 - </a> 424 - </Tooltip> 425 - </div> 285 + <div class="flex flex-col gap-y-1 wrap-anywhere"> 286 + <div class="flex items-baseline justify-between gap-2"> 426 287 <div> 427 288 <div class="flex items-center gap-1"> 428 - <div class="iconify lucide--at-sign" /> 429 - <p class="font-semibold">Aliases</p> 289 + <div class="iconify lucide--id-card" /> 290 + <p class="font-semibold">ID</p> 430 291 </div> 431 - <ul> 432 - <For each={didDocument().alsoKnownAs}> 433 - {(alias) => ( 434 - <li class="flex items-center gap-1 text-sm"> 435 - <span>{alias}</span> 436 - <Show when={alias.startsWith("at://")}> 437 - <Tooltip 438 - text={ 439 - validHandles[alias] === true ? "Valid handle" 440 - : validHandles[alias] === undefined ? 441 - "Validating" 442 - : "Invalid handle" 443 - } 444 - > 445 - <span 446 - classList={{ 447 - "iconify lucide--circle-check": validHandles[alias] === true, 448 - "iconify lucide--circle-x text-red-500 dark:text-red-400": 449 - validHandles[alias] === false, 450 - "iconify lucide--loader-circle animate-spin": 451 - validHandles[alias] === undefined, 452 - }} 453 - ></span> 454 - </Tooltip> 455 - </Show> 456 - </li> 457 - )} 458 - </For> 459 - </ul> 292 + <div class="text-sm">{didDocument().id}</div> 460 293 </div> 461 - <div> 462 - <div class="flex items-center gap-1"> 463 - <div class="iconify lucide--hard-drive" /> 464 - <p class="font-semibold">Services</p> 465 - </div> 466 - <ul> 467 - <For each={didDocument().service}> 468 - {(service) => ( 469 - <li class="flex flex-col text-sm"> 470 - <span>#{service.id.split("#")[1]}</span> 471 - <a 472 - class="w-fit text-blue-400 hover:underline active:underline" 473 - href={service.serviceEndpoint.toString()} 474 - target="_blank" 475 - > 476 - {service.serviceEndpoint.toString()} 477 - </a> 478 - </li> 479 - )} 480 - </For> 481 - </ul> 294 + <Tooltip text="DID document"> 295 + <a 296 + href={ 297 + did.startsWith("did:plc") ? 298 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 299 + : `https://${did.split("did:web:")[1]}/.well-known/did.json` 300 + } 301 + target="_blank" 302 + class="-mr-1 flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 303 + > 304 + <span class="iconify lucide--external-link"></span> 305 + </a> 306 + </Tooltip> 307 + </div> 308 + <div> 309 + <div class="flex items-center gap-1"> 310 + <div class="iconify lucide--at-sign" /> 311 + <p class="font-semibold">Aliases</p> 482 312 </div> 483 - <div> 484 - <div class="flex items-center gap-1"> 485 - <div class="iconify lucide--shield-check" /> 486 - <p class="font-semibold">Verification methods</p> 487 - </div> 488 - <ul> 489 - <For each={didDocument().verificationMethod}> 490 - {(verif) => ( 491 - <Show when={verif.publicKeyMultibase}> 492 - {(key) => ( 493 - <li class="flex flex-col text-sm"> 494 - <span>#{verif.id.split("#")[1]}</span> 495 - <span class="flex items-center gap-0.5"> 496 - <div class="iconify lucide--key-round" /> 497 - <ErrorBoundary fallback={<>unknown</>}> 498 - {parsePublicMultikey(key()).type} 499 - </ErrorBoundary> 500 - </span> 501 - <span class="truncate">{key()}</span> 502 - </li> 503 - )} 313 + <ul> 314 + <For each={didDocument().alsoKnownAs}> 315 + {(alias) => ( 316 + <li class="flex items-center gap-1 text-sm"> 317 + <span>{alias}</span> 318 + <Show when={alias.startsWith("at://")}> 319 + <Tooltip 320 + text={ 321 + validHandles[alias] === true ? "Valid handle" 322 + : validHandles[alias] === undefined ? 323 + "Validating" 324 + : "Invalid handle" 325 + } 326 + > 327 + <span 328 + classList={{ 329 + "iconify lucide--circle-check": validHandles[alias] === true, 330 + "iconify lucide--circle-x text-red-500 dark:text-red-400": 331 + validHandles[alias] === false, 332 + "iconify lucide--loader-circle animate-spin": 333 + validHandles[alias] === undefined, 334 + }} 335 + ></span> 336 + </Tooltip> 504 337 </Show> 505 - )} 506 - </For> 507 - </ul> 338 + </li> 339 + )} 340 + </For> 341 + </ul> 342 + </div> 343 + <div> 344 + <div class="flex items-center gap-1"> 345 + <div class="iconify lucide--hard-drive" /> 346 + <p class="font-semibold">Services</p> 508 347 </div> 348 + <ul> 349 + <For each={didDocument().service}> 350 + {(service) => ( 351 + <li class="flex flex-col text-sm"> 352 + <span>#{service.id.split("#")[1]}</span> 353 + <a 354 + class="w-fit text-blue-400 hover:underline active:underline" 355 + href={service.serviceEndpoint.toString()} 356 + target="_blank" 357 + > 358 + {service.serviceEndpoint.toString()} 359 + </a> 360 + </li> 361 + )} 362 + </For> 363 + </ul> 509 364 </div> 510 - <div class="flex justify-between"> 511 - <Show when={did.startsWith("did:plc")}> 512 - <div class="flex items-center gap-1"> 513 - <Button 514 - onClick={async () => { 515 - if (!plcOps()) { 516 - setLoading(true); 517 - const response = await fetch( 518 - `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`, 519 - ); 520 - const json = await response.json(); 521 - try { 522 - const logs = defs.indexedEntryLog.parse(json); 523 - try { 524 - await processIndexedEntryLog(did as any, logs); 525 - } catch (e) { 526 - console.error(e); 527 - } 528 - const opHistory = createOperationHistory(logs).reverse(); 529 - setPlcOps(Array.from(groupBy(opHistory, (item) => item.orig))); 530 - setLoading(false); 531 - } catch (e: any) { 532 - setNotice(e); 533 - console.error(e); 534 - setLoading(false); 535 - } 536 - } 537 - 538 - setShowPlcLogs(!showPlcLogs()); 539 - }} 540 - > 541 - <span class="iconify lucide--logs text-sm"></span> 542 - {showPlcLogs() ? "Hide" : "Show"} PLC Logs 543 - </Button> 544 - <Show when={loading()}> 545 - <div class="iconify lucide--loader-circle animate-spin text-xl" /> 546 - </Show> 547 - </div> 548 - </Show> 549 - <Show when={error()?.length === 0 || error() === undefined}> 550 - <div 551 - classList={{ 552 - "flex items-center gap-1": true, 553 - "flex-row-reverse": did.startsWith("did:web"), 554 - }} 555 - > 556 - <Show when={downloading()}> 557 - <div class="iconify lucide--loader-circle animate-spin text-xl" /> 558 - </Show> 559 - <Button onClick={() => downloadRepo()}> 560 - <span class="iconify lucide--download text-sm"></span> 561 - Export Repo 562 - </Button> 563 - </div> 564 - </Show> 365 + <div> 366 + <div class="flex items-center gap-1"> 367 + <div class="iconify lucide--shield-check" /> 368 + <p class="font-semibold">Verification methods</p> 369 + </div> 370 + <ul> 371 + <For each={didDocument().verificationMethod}> 372 + {(verif) => ( 373 + <Show when={verif.publicKeyMultibase}> 374 + {(key) => ( 375 + <li class="flex flex-col text-sm"> 376 + <span>#{verif.id.split("#")[1]}</span> 377 + <span class="flex items-center gap-0.5"> 378 + <div class="iconify lucide--key-round" /> 379 + <ErrorBoundary fallback={<>unknown</>}> 380 + {parsePublicMultikey(key()).type} 381 + </ErrorBoundary> 382 + </span> 383 + <span class="truncate">{key()}</span> 384 + </li> 385 + )} 386 + </Show> 387 + )} 388 + </For> 389 + </ul> 565 390 </div> 566 - <Show when={showPlcLogs()}> 567 - <Show when={notice()}> 568 - <div>{notice()}</div> 569 - </Show> 570 - <PlcLogView plcOps={plcOps() ?? []} did={did} /> 571 - </Show> 572 391 </div> 573 392 )} 574 393 </Show>