forked from
pds.ls/pdsls
atmosphere explorer
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};