forked from pdsls.dev/pdsls
atproto explorer
at main 6.3 kB view raw
1import { 2 CompatibleOperationOrTombstone, 3 defs, 4 IndexedEntry, 5 processIndexedEntryLog, 6} from "@atcute/did-plc"; 7import { createResource, createSignal, For, Show } from "solid-js"; 8import Tooltip from "../components/tooltip.jsx"; 9import { localDateFromTimestamp } from "../utils/date.js"; 10import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 11 12type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 13 14export 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 "hover:bg-neutral-200 dark:hover:bg-neutral-700": activePlcEvent() !== props.event, 41 }} 42 onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)} 43 > 44 <span 45 class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`} 46 ></span> 47 </button> 48 ); 49 50 const DiffItem = (props: { diff: DiffEntry }) => { 51 const diff = props.diff; 52 let title = "Unknown log entry"; 53 let icon = "lucide--circle-help"; 54 let value = ""; 55 56 if (diff.type === "identity_created") { 57 icon = "lucide--bell"; 58 title = `Identity created`; 59 } else if (diff.type === "identity_tombstoned") { 60 icon = "lucide--skull"; 61 title = `Identity tombstoned`; 62 } else if (diff.type === "handle_added" || diff.type === "handle_removed") { 63 icon = "lucide--at-sign"; 64 title = diff.type === "handle_added" ? "Alias added" : "Alias removed"; 65 value = diff.handle; 66 } else if (diff.type === "handle_changed") { 67 icon = "lucide--at-sign"; 68 title = "Alias updated"; 69 value = `${diff.prev_handle}${diff.next_handle}`; 70 } else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") { 71 icon = "lucide--key-round"; 72 title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed"; 73 value = diff.rotation_key; 74 } else if (diff.type === "service_added" || diff.type === "service_removed") { 75 icon = "lucide--hard-drive"; 76 title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`; 77 value = `${diff.service_endpoint}`; 78 } else if (diff.type === "service_changed") { 79 icon = "lucide--hard-drive"; 80 title = `Service ${diff.service_id} updated`; 81 value = `${diff.prev_service_endpoint}${diff.next_service_endpoint}`; 82 } else if ( 83 diff.type === "verification_method_added" || 84 diff.type === "verification_method_removed" 85 ) { 86 icon = "lucide--shield-check"; 87 title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`; 88 value = `${diff.method_key}`; 89 } else if (diff.type === "verification_method_changed") { 90 icon = "lucide--shield-check"; 91 title = `Verification method ${diff.method_id} updated`; 92 value = `${diff.prev_method_key}${diff.next_method_key}`; 93 } 94 95 return ( 96 <div class="grid grid-cols-[min-content_1fr] items-center gap-x-1"> 97 <div class={icon + ` iconify shrink-0`} /> 98 <p 99 classList={{ 100 "font-semibold": true, 101 "text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified, 102 }} 103 > 104 {title} 105 </p> 106 <div></div> 107 {value} 108 </div> 109 ); 110 }; 111 112 return ( 113 <div class="flex w-full flex-col gap-2 wrap-anywhere"> 114 <div class="flex items-center justify-between"> 115 <div class="flex items-center gap-1"> 116 <div class="iconify lucide--filter" /> 117 <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"> 118 <FilterButton icon="iconify lucide--at-sign" event="handle" /> 119 <FilterButton icon="iconify lucide--key-round" event="rotation_key" /> 120 <FilterButton icon="iconify lucide--hard-drive" event="service" /> 121 <FilterButton icon="iconify lucide--shield-check" event="verification_method" /> 122 </div> 123 </div> 124 <Tooltip text="Audit log"> 125 <a 126 href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`} 127 target="_blank" 128 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" 129 > 130 <span class="iconify lucide--external-link"></span> 131 </a> 132 </Tooltip> 133 </div> 134 <div class="flex flex-col gap-1 text-sm"> 135 <For each={plcOps()}> 136 {([entry, diffs]) => ( 137 <Show 138 when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))} 139 > 140 <div class="flex flex-col"> 141 <span class="text-neutral-500 dark:text-neutral-400"> 142 {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 143 </span> 144 {diffs.map((diff) => ( 145 <Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}> 146 <DiffItem diff={diff} /> 147 </Show> 148 ))} 149 </div> 150 </Show> 151 )} 152 </For> 153 </div> 154 </div> 155 ); 156};