atproto explorer pdsls.dev
atproto tool
1import { 2 CompatibleOperationOrTombstone, 3 defs, 4 IndexedEntry, 5 processIndexedEntryLog, 6} from "@atcute/did-plc"; 7import { createResource, createSignal, For, Show } from "solid-js"; 8import { localDateFromTimestamp } from "../utils/date.js"; 9import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 10 11type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 12 13export const PlcLogView = (props: { did: string }) => { 14 const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 15 16 const shouldShowDiff = (diff: DiffEntry) => 17 !activePlcEvent() || diff.type.startsWith(activePlcEvent()!); 18 19 const shouldShowEntry = (diffs: DiffEntry[]) => 20 !activePlcEvent() || diffs.some((d) => d.type.startsWith(activePlcEvent()!)); 21 22 const fetchPlcLogs = async () => { 23 const res = await fetch( 24 `${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`, 25 ); 26 const json = await res.json(); 27 const logs = defs.indexedEntryLog.parse(json); 28 try { 29 await processIndexedEntryLog(props.did as any, logs); 30 } catch (e) { 31 console.error(e); 32 } 33 const opHistory = createOperationHistory(logs).reverse(); 34 return Array.from(groupBy(opHistory, (item) => item.orig)); 35 }; 36 37 const [plcOps] = 38 createResource<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(fetchPlcLogs); 39 40 const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => { 41 const isActive = () => activePlcEvent() === props.event; 42 const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 43 44 return ( 45 <button 46 classList={{ 47 "flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true, 48 "bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(), 49 "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 50 !isActive(), 51 }} 52 onclick={toggleFilter} 53 > 54 <span class={props.icon}></span> 55 <span class="hidden font-medium sm:inline">{props.label}</span> 56 </button> 57 ); 58 }; 59 60 const DiffItem = (props: { diff: DiffEntry }) => { 61 const diff = props.diff; 62 63 const getDiffConfig = () => { 64 switch (diff.type) { 65 case "identity_created": 66 return { icon: "lucide--bell", title: "Identity created" }; 67 case "identity_tombstoned": 68 return { icon: "lucide--skull", title: "Identity tombstoned" }; 69 case "handle_added": 70 return { 71 icon: "lucide--at-sign", 72 title: "Alias added", 73 value: diff.handle, 74 isAddition: true, 75 }; 76 case "handle_removed": 77 return { 78 icon: "lucide--at-sign", 79 title: "Alias removed", 80 value: diff.handle, 81 isRemoval: true, 82 }; 83 case "handle_changed": 84 return { 85 icon: "lucide--at-sign", 86 title: "Alias updated", 87 oldValue: diff.prev_handle, 88 newValue: diff.next_handle, 89 }; 90 case "rotation_key_added": 91 return { 92 icon: "lucide--key-round", 93 title: "Rotation key added", 94 value: diff.rotation_key, 95 isAddition: true, 96 }; 97 case "rotation_key_removed": 98 return { 99 icon: "lucide--key-round", 100 title: "Rotation key removed", 101 value: diff.rotation_key, 102 isRemoval: true, 103 }; 104 case "service_added": 105 return { 106 icon: "lucide--hard-drive", 107 title: "Service added", 108 badge: diff.service_id, 109 value: diff.service_endpoint, 110 isAddition: true, 111 }; 112 case "service_removed": 113 return { 114 icon: "lucide--hard-drive", 115 title: "Service removed", 116 badge: diff.service_id, 117 value: diff.service_endpoint, 118 isRemoval: true, 119 }; 120 case "service_changed": 121 return { 122 icon: "lucide--hard-drive", 123 title: "Service updated", 124 badge: diff.service_id, 125 oldValue: diff.prev_service_endpoint, 126 newValue: diff.next_service_endpoint, 127 }; 128 case "verification_method_added": 129 return { 130 icon: "lucide--shield-check", 131 title: "Verification method added", 132 badge: diff.method_id, 133 value: diff.method_key, 134 isAddition: true, 135 }; 136 case "verification_method_removed": 137 return { 138 icon: "lucide--shield-check", 139 title: "Verification method removed", 140 badge: diff.method_id, 141 value: diff.method_key, 142 isRemoval: true, 143 }; 144 case "verification_method_changed": 145 return { 146 icon: "lucide--shield-check", 147 title: "Verification method updated", 148 badge: diff.method_id, 149 oldValue: diff.prev_method_key, 150 newValue: diff.next_method_key, 151 }; 152 default: 153 return { icon: "lucide--circle-help", title: "Unknown log entry" }; 154 } 155 }; 156 157 const config = getDiffConfig(); 158 const { 159 icon, 160 title, 161 value = "", 162 oldValue = "", 163 newValue = "", 164 badge = "", 165 isAddition = false, 166 isRemoval = false, 167 } = config; 168 169 return ( 170 <div 171 classList={{ 172 "grid grid-cols-[auto_1fr] gap-y-0.5 gap-x-2": true, 173 "opacity-60": diff.orig.nullified, 174 }} 175 > 176 <div class={`${icon} iconify shrink-0 self-center`} /> 177 <div class="flex min-w-0 items-center gap-1.5"> 178 <p 179 classList={{ 180 "font-medium text-sm": true, 181 "line-through": diff.orig.nullified, 182 }} 183 > 184 {title} 185 </p> 186 <Show when={badge}> 187 <span class="shrink-0 rounded bg-neutral-200 px-1.5 py-0.5 text-xs font-medium dark:bg-neutral-700"> 188 #{badge} 189 </span> 190 </Show> 191 <Show when={diff.orig.nullified}> 192 <span class="ml-auto rounded bg-neutral-200 px-2 py-0.5 text-xs font-medium dark:bg-neutral-700"> 193 Nullified 194 </span> 195 </Show> 196 </div> 197 <Show when={value}> 198 <div></div> 199 <div 200 classList={{ 201 "text-sm break-all flex items-start gap-2 min-w-0": true, 202 "text-green-500 dark:text-green-300": isAddition, 203 "text-red-400 dark:text-red-300": isRemoval, 204 "text-neutral-600 dark:text-neutral-400": !isAddition && !isRemoval, 205 }} 206 > 207 <Show when={isAddition}> 208 <span class="shrink-0">+</span> 209 </Show> 210 <Show when={isRemoval}> 211 <span class="shrink-0"></span> 212 </Show> 213 <span class="break-all">{value}</span> 214 </div> 215 </Show> 216 <Show when={oldValue && newValue}> 217 <div></div> 218 <div class="flex min-w-0 flex-col text-sm"> 219 <div class="flex items-start gap-2 text-red-400 dark:text-red-300"> 220 <span class="shrink-0"></span> 221 <span class="break-all">{oldValue}</span> 222 </div> 223 <div class="flex items-start gap-2 text-green-500 dark:text-green-300"> 224 <span class="shrink-0">+</span> 225 <span class="break-all">{newValue}</span> 226 </div> 227 </div> 228 </Show> 229 </div> 230 ); 231 }; 232 233 return ( 234 <div class="flex w-full flex-col gap-4 wrap-anywhere"> 235 <div class="flex flex-col gap-2"> 236 <div class="flex items-center gap-1.5 text-sm"> 237 <div class="iconify lucide--filter" /> 238 <p class="font-medium">Filter by type</p> 239 </div> 240 <div class="flex flex-wrap gap-1 sm:gap-2"> 241 <FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" /> 242 <FilterButton 243 icon="iconify lucide--key-round" 244 event="rotation_key" 245 label="Rotation Key" 246 /> 247 <FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" /> 248 <FilterButton 249 icon="iconify lucide--shield-check" 250 event="verification_method" 251 label="Verification" 252 /> 253 </div> 254 </div> 255 <div class="flex flex-col gap-3"> 256 <For each={plcOps()}> 257 {([entry, diffs]) => ( 258 <Show when={shouldShowEntry(diffs)}> 259 <div class="flex flex-col gap-1"> 260 <span class="text-sm font-semibold text-neutral-700 dark:text-neutral-300"> 261 {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 262 </span> 263 <div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 text-sm dark:border-neutral-700 dark:bg-neutral-800"> 264 <For each={diffs.filter(shouldShowDiff)}> 265 {(diff) => <DiffItem diff={diff} />} 266 </For> 267 </div> 268 </div> 269 </Show> 270 )} 271 </For> 272 </div> 273 </div> 274 ); 275};