atmosphere explorer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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