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};