tangled
alpha
login
or
join now
vielle.dev
/
pdsls
forked from
pds.ls/pdsls
0
fork
atom
atproto explorer
0
fork
atom
overview
issues
pulls
pipelines
new repo tabs
handle.invalid
5 months ago
785a22bf
143a70c2
verified
This commit was signed with the committer's
known signature
.
handle.invalid
SSH Key Fingerprint:
SHA256:mBrT4x0JdzLpbVR95g1hjI1aaErfC02kmLRkPXwsYCk=
+327
-335
3 changed files
expand all
collapse all
unified
split
src
components
dropdown.tsx
views
logs.tsx
repo.tsx
+18
src/components/dropdown.tsx
···
59
);
60
};
61
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
62
export const DropdownMenu = (props: {
63
icon: string;
64
buttonClass?: string;
···
59
);
60
};
61
62
+
export const ActionMenu = (props: {
63
+
label: string;
64
+
icon: string;
65
+
onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
66
+
}) => {
67
+
return (
68
+
<button
69
+
onClick={props.onClick}
70
+
class="flex items-center gap-1.5 rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
71
+
>
72
+
<Show when={props.icon}>
73
+
<span class={"iconify shrink-0 " + props.icon}></span>
74
+
</Show>
75
+
<span class="whitespace-nowrap">{props.label}</span>
76
+
</button>
77
+
);
78
+
};
79
+
80
export const DropdownMenu = (props: {
81
icon: string;
82
buttonClass?: string;
+155
src/views/logs.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import {
2
+
CompatibleOperationOrTombstone,
3
+
defs,
4
+
IndexedEntry,
5
+
processIndexedEntryLog,
6
+
} from "@atcute/did-plc";
7
+
import { createResource, createSignal, For, Show } from "solid-js";
8
+
import Tooltip from "../components/tooltip.jsx";
9
+
import { localDateFromTimestamp } from "../utils/date.js";
10
+
import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js";
11
+
12
+
type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method";
13
+
14
+
export 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
+
}}
41
+
onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)}
42
+
>
43
+
<span
44
+
class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`}
45
+
></span>
46
+
</button>
47
+
);
48
+
49
+
const DiffItem = (props: { diff: DiffEntry }) => {
50
+
const diff = props.diff;
51
+
let title = "Unknown log entry";
52
+
let icon = "lucide--circle-help";
53
+
let value = "";
54
+
55
+
if (diff.type === "identity_created") {
56
+
icon = "lucide--bell";
57
+
title = `Identity created`;
58
+
} else if (diff.type === "identity_tombstoned") {
59
+
icon = "lucide--skull";
60
+
title = `Identity tombstoned`;
61
+
} else if (diff.type === "handle_added" || diff.type === "handle_removed") {
62
+
icon = "lucide--at-sign";
63
+
title = diff.type === "handle_added" ? "Alias added" : "Alias removed";
64
+
value = diff.handle;
65
+
} else if (diff.type === "handle_changed") {
66
+
icon = "lucide--at-sign";
67
+
title = "Alias updated";
68
+
value = `${diff.prev_handle} → ${diff.next_handle}`;
69
+
} else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") {
70
+
icon = "lucide--key-round";
71
+
title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed";
72
+
value = diff.rotation_key;
73
+
} else if (diff.type === "service_added" || diff.type === "service_removed") {
74
+
icon = "lucide--hard-drive";
75
+
title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`;
76
+
value = `${diff.service_endpoint}`;
77
+
} else if (diff.type === "service_changed") {
78
+
icon = "lucide--hard-drive";
79
+
title = `Service ${diff.service_id} updated`;
80
+
value = `${diff.prev_service_endpoint} → ${diff.next_service_endpoint}`;
81
+
} else if (
82
+
diff.type === "verification_method_added" ||
83
+
diff.type === "verification_method_removed"
84
+
) {
85
+
icon = "lucide--shield-check";
86
+
title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`;
87
+
value = `${diff.method_key}`;
88
+
} else if (diff.type === "verification_method_changed") {
89
+
icon = "lucide--shield-check";
90
+
title = `Verification method ${diff.method_id} updated`;
91
+
value = `${diff.prev_method_key} → ${diff.next_method_key}`;
92
+
}
93
+
94
+
return (
95
+
<div class="grid grid-cols-[min-content_1fr] items-center gap-x-1">
96
+
<div class={icon + ` iconify shrink-0`} />
97
+
<p
98
+
classList={{
99
+
"font-semibold": true,
100
+
"text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified,
101
+
}}
102
+
>
103
+
{title}
104
+
</p>
105
+
<div></div>
106
+
{value}
107
+
</div>
108
+
);
109
+
};
110
+
111
+
return (
112
+
<div class="flex w-full flex-col gap-2 wrap-anywhere">
113
+
<div class="flex items-center justify-between">
114
+
<div class="flex items-center gap-1">
115
+
<div class="iconify lucide--filter" />
116
+
<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">
117
+
<FilterButton icon="iconify lucide--at-sign" event="handle" />
118
+
<FilterButton icon="iconify lucide--key-round" event="rotation_key" />
119
+
<FilterButton icon="iconify lucide--hard-drive" event="service" />
120
+
<FilterButton icon="iconify lucide--shield-check" event="verification_method" />
121
+
</div>
122
+
</div>
123
+
<Tooltip text="Audit log">
124
+
<a
125
+
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`}
126
+
target="_blank"
127
+
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"
128
+
>
129
+
<span class="iconify lucide--external-link"></span>
130
+
</a>
131
+
</Tooltip>
132
+
</div>
133
+
<div class="flex flex-col gap-1 text-sm">
134
+
<For each={plcOps()}>
135
+
{([entry, diffs]) => (
136
+
<Show
137
+
when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))}
138
+
>
139
+
<div class="flex flex-col">
140
+
<span class="text-neutral-500 dark:text-neutral-400">
141
+
{localDateFromTimestamp(new Date(entry.createdAt).getTime())}
142
+
</span>
143
+
{diffs.map((diff) => (
144
+
<Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}>
145
+
<DiffItem diff={diff} />
146
+
</Show>
147
+
))}
148
+
</div>
149
+
</Show>
150
+
)}
151
+
</For>
152
+
</div>
153
+
</div>
154
+
);
155
+
};
+154
-335
src/views/repo.tsx
···
1
import { Client, CredentialManager } from "@atcute/client";
2
import { parsePublicMultikey } from "@atcute/crypto";
3
-
import {
4
-
CompatibleOperationOrTombstone,
5
-
defs,
6
-
IndexedEntry,
7
-
processIndexedEntryLog,
8
-
} from "@atcute/did-plc";
9
import { DidDocument } from "@atcute/identity";
10
import { ActorIdentifier, Did, Handle } from "@atcute/lexicons";
11
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
···
20
} from "solid-js";
21
import { createStore } from "solid-js/store";
22
import { Backlinks } from "../components/backlinks.jsx";
23
-
import { Button } from "../components/button.jsx";
24
import { TextInput } from "../components/text-input.jsx";
25
import Tooltip from "../components/tooltip.jsx";
26
import { didDocCache, resolveHandle, resolvePDS, validateHandle } from "../utils/api.js";
27
-
import { localDateFromTimestamp } from "../utils/date.js";
28
-
import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js";
29
import { BlobView } from "./blob.jsx";
30
-
31
-
type Tab = "collections" | "backlinks" | "identity" | "blobs";
32
-
type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method";
33
-
34
-
const PlcLogView = (props: {
35
-
did: string;
36
-
plcOps: [IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][];
37
-
}) => {
38
-
const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>();
39
-
40
-
const FilterButton = (props: { icon: string; event: PlcEvent }) => (
41
-
<button
42
-
classList={{
43
-
"flex items-center rounded-full p-1.5": true,
44
-
"bg-neutral-700 dark:bg-neutral-200": activePlcEvent() === props.event,
45
-
}}
46
-
onclick={() => setActivePlcEvent(activePlcEvent() === props.event ? undefined : props.event)}
47
-
>
48
-
<span
49
-
class={`${props.icon} ${activePlcEvent() === props.event ? "text-neutral-200 dark:text-neutral-900" : ""}`}
50
-
></span>
51
-
</button>
52
-
);
53
-
54
-
const DiffItem = (props: { diff: DiffEntry }) => {
55
-
const diff = props.diff;
56
-
let title = "Unknown log entry";
57
-
let icon = "lucide--circle-help";
58
-
let value = "";
59
60
-
if (diff.type === "identity_created") {
61
-
icon = "lucide--bell";
62
-
title = `Identity created`;
63
-
} else if (diff.type === "identity_tombstoned") {
64
-
icon = "lucide--skull";
65
-
title = `Identity tombstoned`;
66
-
} else if (diff.type === "handle_added" || diff.type === "handle_removed") {
67
-
icon = "lucide--at-sign";
68
-
title = diff.type === "handle_added" ? "Alias added" : "Alias removed";
69
-
value = diff.handle;
70
-
} else if (diff.type === "handle_changed") {
71
-
icon = "lucide--at-sign";
72
-
title = "Alias updated";
73
-
value = `${diff.prev_handle} → ${diff.next_handle}`;
74
-
} else if (diff.type === "rotation_key_added" || diff.type === "rotation_key_removed") {
75
-
icon = "lucide--key-round";
76
-
title = diff.type === "rotation_key_added" ? "Rotation key added" : "Rotation key removed";
77
-
value = diff.rotation_key;
78
-
} else if (diff.type === "service_added" || diff.type === "service_removed") {
79
-
icon = "lucide--hard-drive";
80
-
title = `Service ${diff.service_id} ${diff.type === "service_added" ? "added" : "removed"}`;
81
-
value = `${diff.service_endpoint}`;
82
-
} else if (diff.type === "service_changed") {
83
-
icon = "lucide--hard-drive";
84
-
title = `Service ${diff.service_id} updated`;
85
-
value = `${diff.prev_service_endpoint} → ${diff.next_service_endpoint}`;
86
-
} else if (
87
-
diff.type === "verification_method_added" ||
88
-
diff.type === "verification_method_removed"
89
-
) {
90
-
icon = "lucide--shield-check";
91
-
title = `Verification method ${diff.method_id} ${diff.type === "verification_method_added" ? "added" : "removed"}`;
92
-
value = `${diff.method_key}`;
93
-
} else if (diff.type === "verification_method_changed") {
94
-
icon = "lucide--shield-check";
95
-
title = `Verification method ${diff.method_id} updated`;
96
-
value = `${diff.prev_method_key} → ${diff.next_method_key}`;
97
-
}
98
-
99
-
return (
100
-
<div class="grid grid-cols-[min-content_1fr] items-center gap-x-1">
101
-
<div class={icon + ` iconify shrink-0`} />
102
-
<p
103
-
classList={{
104
-
"font-semibold": true,
105
-
"text-neutral-400 line-through dark:text-neutral-600": diff.orig.nullified,
106
-
}}
107
-
>
108
-
{title}
109
-
</p>
110
-
<div></div>
111
-
{value}
112
-
</div>
113
-
);
114
-
};
115
-
116
-
return (
117
-
<>
118
-
<div class="flex items-center justify-between">
119
-
<div class="flex items-center gap-1">
120
-
<div class="iconify lucide--filter" />
121
-
<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">
122
-
<FilterButton icon="iconify lucide--at-sign" event="handle" />
123
-
<FilterButton icon="iconify lucide--key-round" event="rotation_key" />
124
-
<FilterButton icon="iconify lucide--hard-drive" event="service" />
125
-
<FilterButton icon="iconify lucide--shield-check" event="verification_method" />
126
-
</div>
127
-
</div>
128
-
<Tooltip text="Audit log">
129
-
<a
130
-
href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`}
131
-
target="_blank"
132
-
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"
133
-
>
134
-
<span class="iconify lucide--external-link"></span>
135
-
</a>
136
-
</Tooltip>
137
-
</div>
138
-
<div class="flex flex-col gap-1 text-sm">
139
-
<For each={props.plcOps}>
140
-
{([entry, diffs]) => (
141
-
<Show
142
-
when={!activePlcEvent() || diffs.find((d) => d.type.startsWith(activePlcEvent()!))}
143
-
>
144
-
<div class="flex flex-col">
145
-
<span class="text-neutral-500 dark:text-neutral-400">
146
-
{localDateFromTimestamp(new Date(entry.createdAt).getTime())}
147
-
</span>
148
-
{diffs.map((diff) => (
149
-
<Show when={!activePlcEvent() || diff.type.startsWith(activePlcEvent()!)}>
150
-
<DiffItem diff={diff} />
151
-
</Show>
152
-
))}
153
-
</div>
154
-
</Show>
155
-
)}
156
-
</For>
157
-
</div>
158
-
</>
159
-
);
160
-
};
161
-
162
const RepoView = () => {
163
const params = useParams();
164
const location = useLocation();
···
168
const [didDoc, setDidDoc] = createSignal<DidDocument>();
169
const [nsids, setNsids] = createSignal<Record<string, { hidden: boolean; nsids: string[] }>>();
170
const [filter, setFilter] = createSignal<string>();
171
-
const [plcOps, setPlcOps] =
172
-
createSignal<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>();
173
-
const [showPlcLogs, setShowPlcLogs] = createSignal(false);
174
-
const [loading, setLoading] = createSignal(false);
175
-
const [notice, setNotice] = createSignal<string>();
176
const [validHandles, setValidHandles] = createStore<Record<string, boolean>>({});
177
let rpc: Client;
178
let pds: string;
179
const did = params.repo;
180
181
-
const RepoTab = (props: { tab: Tab; label: string; icon: string }) => (
182
-
<A class="group flex flex-1 justify-center" href={`/at://${params.repo}#${props.tab}`}>
183
<span
184
classList={{
185
-
"flex gap-1 items-center border-b-2": true,
186
"border-transparent group-hover:border-neutral-400 dark:group-hover:border-neutral-600":
187
(location.hash !== `#${props.tab}` && !!location.hash) ||
188
(!location.hash && props.tab !== "collections"),
189
}}
190
>
191
-
<span class={"iconify " + props.icon}></span>
192
{props.label}
193
</span>
194
</A>
···
294
</div>
295
</Show>
296
<div
297
-
class={`dark:shadow-dark-800 dark:bg-dark-300 flex ${error() ? "justify-around" : "justify-between"} rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700`}
298
>
299
-
<Show when={!error()}>
300
-
<RepoTab tab="collections" label="Collections" icon="lucide--folder-open" />
301
-
</Show>
302
-
<RepoTab tab="identity" label="Identity" icon="lucide--id-card" />
303
-
<Show when={!error()}>
304
-
<RepoTab tab="blobs" label="Blobs" icon="lucide--file-digit" />
305
-
</Show>
306
-
<RepoTab tab="backlinks" label="Backlinks" icon="lucide--send-to-back" />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
307
</div>
0
0
0
0
0
0
0
0
0
0
0
308
<Show when={location.hash === "#backlinks"}>
309
<ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}>
310
<Suspense
···
328
</ErrorBoundary>
329
</Show>
330
<Show when={nsids() && (!location.hash || location.hash === "#collections")}>
331
-
<div class="flex items-center gap-1">
332
-
<Tooltip text="Jetstream">
333
-
<A
334
-
href={`/jetstream?dids=${params.repo}`}
335
-
class="-ml-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"
336
-
>
337
-
<span class="iconify lucide--radio-tower text-lg"></span>
338
-
</A>
339
-
</Tooltip>
340
-
<TextInput
341
-
name="filter"
342
-
placeholder="Filter collections"
343
-
onInput={(e) => setFilter(e.currentTarget.value)}
344
-
class="grow"
345
-
/>
346
-
</div>
347
<div class="flex flex-col font-mono">
348
<div class="grid grid-cols-[min-content_1fr] items-center gap-x-2 overflow-hidden text-sm">
349
<For
···
399
<Show when={location.hash === "#identity"}>
400
<Show when={didDoc()}>
401
{(didDocument) => (
402
-
<div class="flex flex-col gap-y-2 wrap-anywhere">
403
-
<div class="flex flex-col gap-y-1">
404
-
<div class="flex items-baseline justify-between gap-2">
405
-
<div>
406
-
<div class="flex items-center gap-1">
407
-
<div class="iconify lucide--id-card" />
408
-
<p class="font-semibold">ID</p>
409
-
</div>
410
-
<div class="text-sm">{didDocument().id}</div>
411
-
</div>
412
-
<Tooltip text="DID document">
413
-
<a
414
-
href={
415
-
did.startsWith("did:plc") ?
416
-
`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}`
417
-
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
418
-
}
419
-
target="_blank"
420
-
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"
421
-
>
422
-
<span class="iconify lucide--external-link"></span>
423
-
</a>
424
-
</Tooltip>
425
-
</div>
426
<div>
427
<div class="flex items-center gap-1">
428
-
<div class="iconify lucide--at-sign" />
429
-
<p class="font-semibold">Aliases</p>
430
</div>
431
-
<ul>
432
-
<For each={didDocument().alsoKnownAs}>
433
-
{(alias) => (
434
-
<li class="flex items-center gap-1 text-sm">
435
-
<span>{alias}</span>
436
-
<Show when={alias.startsWith("at://")}>
437
-
<Tooltip
438
-
text={
439
-
validHandles[alias] === true ? "Valid handle"
440
-
: validHandles[alias] === undefined ?
441
-
"Validating"
442
-
: "Invalid handle"
443
-
}
444
-
>
445
-
<span
446
-
classList={{
447
-
"iconify lucide--circle-check": validHandles[alias] === true,
448
-
"iconify lucide--circle-x text-red-500 dark:text-red-400":
449
-
validHandles[alias] === false,
450
-
"iconify lucide--loader-circle animate-spin":
451
-
validHandles[alias] === undefined,
452
-
}}
453
-
></span>
454
-
</Tooltip>
455
-
</Show>
456
-
</li>
457
-
)}
458
-
</For>
459
-
</ul>
460
</div>
461
-
<div>
462
-
<div class="flex items-center gap-1">
463
-
<div class="iconify lucide--hard-drive" />
464
-
<p class="font-semibold">Services</p>
465
-
</div>
466
-
<ul>
467
-
<For each={didDocument().service}>
468
-
{(service) => (
469
-
<li class="flex flex-col text-sm">
470
-
<span>#{service.id.split("#")[1]}</span>
471
-
<a
472
-
class="w-fit text-blue-400 hover:underline active:underline"
473
-
href={service.serviceEndpoint.toString()}
474
-
target="_blank"
475
-
>
476
-
{service.serviceEndpoint.toString()}
477
-
</a>
478
-
</li>
479
-
)}
480
-
</For>
481
-
</ul>
482
</div>
483
-
<div>
484
-
<div class="flex items-center gap-1">
485
-
<div class="iconify lucide--shield-check" />
486
-
<p class="font-semibold">Verification methods</p>
487
-
</div>
488
-
<ul>
489
-
<For each={didDocument().verificationMethod}>
490
-
{(verif) => (
491
-
<Show when={verif.publicKeyMultibase}>
492
-
{(key) => (
493
-
<li class="flex flex-col text-sm">
494
-
<span>#{verif.id.split("#")[1]}</span>
495
-
<span class="flex items-center gap-0.5">
496
-
<div class="iconify lucide--key-round" />
497
-
<ErrorBoundary fallback={<>unknown</>}>
498
-
{parsePublicMultikey(key()).type}
499
-
</ErrorBoundary>
500
-
</span>
501
-
<span class="truncate">{key()}</span>
502
-
</li>
503
-
)}
0
0
0
504
</Show>
505
-
)}
506
-
</For>
507
-
</ul>
0
0
0
0
0
0
508
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
509
</div>
510
-
<div class="flex justify-between">
511
-
<Show when={did.startsWith("did:plc")}>
512
-
<div class="flex items-center gap-1">
513
-
<Button
514
-
onClick={async () => {
515
-
if (!plcOps()) {
516
-
setLoading(true);
517
-
const response = await fetch(
518
-
`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`,
519
-
);
520
-
const json = await response.json();
521
-
try {
522
-
const logs = defs.indexedEntryLog.parse(json);
523
-
try {
524
-
await processIndexedEntryLog(did as any, logs);
525
-
} catch (e) {
526
-
console.error(e);
527
-
}
528
-
const opHistory = createOperationHistory(logs).reverse();
529
-
setPlcOps(Array.from(groupBy(opHistory, (item) => item.orig)));
530
-
setLoading(false);
531
-
} catch (e: any) {
532
-
setNotice(e);
533
-
console.error(e);
534
-
setLoading(false);
535
-
}
536
-
}
537
-
538
-
setShowPlcLogs(!showPlcLogs());
539
-
}}
540
-
>
541
-
<span class="iconify lucide--logs text-sm"></span>
542
-
{showPlcLogs() ? "Hide" : "Show"} PLC Logs
543
-
</Button>
544
-
<Show when={loading()}>
545
-
<div class="iconify lucide--loader-circle animate-spin text-xl" />
546
-
</Show>
547
-
</div>
548
-
</Show>
549
-
<Show when={error()?.length === 0 || error() === undefined}>
550
-
<div
551
-
classList={{
552
-
"flex items-center gap-1": true,
553
-
"flex-row-reverse": did.startsWith("did:web"),
554
-
}}
555
-
>
556
-
<Show when={downloading()}>
557
-
<div class="iconify lucide--loader-circle animate-spin text-xl" />
558
-
</Show>
559
-
<Button onClick={() => downloadRepo()}>
560
-
<span class="iconify lucide--download text-sm"></span>
561
-
Export Repo
562
-
</Button>
563
-
</div>
564
-
</Show>
565
</div>
566
-
<Show when={showPlcLogs()}>
567
-
<Show when={notice()}>
568
-
<div>{notice()}</div>
569
-
</Show>
570
-
<PlcLogView plcOps={plcOps() ?? []} did={did} />
571
-
</Show>
572
</div>
573
)}
574
</Show>
···
1
import { Client, CredentialManager } from "@atcute/client";
2
import { parsePublicMultikey } from "@atcute/crypto";
0
0
0
0
0
0
3
import { DidDocument } from "@atcute/identity";
4
import { ActorIdentifier, Did, Handle } from "@atcute/lexicons";
5
import { A, useLocation, useNavigate, useParams } from "@solidjs/router";
···
14
} from "solid-js";
15
import { createStore } from "solid-js/store";
16
import { Backlinks } from "../components/backlinks.jsx";
17
+
import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx";
18
import { TextInput } from "../components/text-input.jsx";
19
import Tooltip from "../components/tooltip.jsx";
20
import { didDocCache, resolveHandle, resolvePDS, validateHandle } from "../utils/api.js";
0
0
21
import { BlobView } from "./blob.jsx";
22
+
import { PlcLogView } from "./logs.jsx";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
23
24
+
type Tab = "collections" | "backlinks" | "identity" | "blobs" | "logs";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
25
const RepoView = () => {
26
const params = useParams();
27
const location = useLocation();
···
31
const [didDoc, setDidDoc] = createSignal<DidDocument>();
32
const [nsids, setNsids] = createSignal<Record<string, { hidden: boolean; nsids: string[] }>>();
33
const [filter, setFilter] = createSignal<string>();
0
0
0
0
0
34
const [validHandles, setValidHandles] = createStore<Record<string, boolean>>({});
35
let rpc: Client;
36
let pds: string;
37
const did = params.repo;
38
39
+
const RepoTab = (props: { tab: Tab; label: string }) => (
40
+
<A class="group flex justify-center" href={`/at://${params.repo}#${props.tab}`}>
41
<span
42
classList={{
43
+
"flex flex-1 border-b-2": true,
44
"border-transparent group-hover:border-neutral-400 dark:group-hover:border-neutral-600":
45
(location.hash !== `#${props.tab}` && !!location.hash) ||
46
(!location.hash && props.tab !== "collections"),
47
}}
48
>
0
49
{props.label}
50
</span>
51
</A>
···
151
</div>
152
</Show>
153
<div
154
+
class={`dark:shadow-dark-800 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm shadow-xs dark:border-neutral-700`}
155
>
156
+
<div class="flex gap-2 sm:gap-4">
157
+
<Show when={!error()}>
158
+
<RepoTab tab="collections" label="Collections" />
159
+
</Show>
160
+
<RepoTab tab="identity" label="Identity" />
161
+
<Show when={did.startsWith("did:plc")}>
162
+
<RepoTab tab="logs" label="Logs" />
163
+
</Show>
164
+
<Show when={!error()}>
165
+
<RepoTab tab="blobs" label="Blobs" />
166
+
</Show>
167
+
<RepoTab tab="backlinks" label="Backlinks" />
168
+
</div>
169
+
<MenuProvider>
170
+
<DropdownMenu
171
+
icon="lucide--ellipsis-vertical"
172
+
buttonClass="rounded-sm p-1"
173
+
menuClass="top-8 p-2 text-sm"
174
+
>
175
+
<NavMenu
176
+
href={`/jetstream?dids=${params.repo}`}
177
+
label="Jetstream"
178
+
icon="lucide--radio-tower"
179
+
/>
180
+
<Show when={error()?.length === 0 || error() === undefined}>
181
+
<ActionMenu
182
+
label="Export Repo"
183
+
icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"}
184
+
onClick={() => downloadRepo()}
185
+
/>
186
+
</Show>
187
+
</DropdownMenu>
188
+
</MenuProvider>
189
</div>
190
+
<Show when={location.hash === "#logs"}>
191
+
<ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}>
192
+
<Suspense
193
+
fallback={
194
+
<div class="iconify lucide--loader-circle animate-spin self-center text-xl" />
195
+
}
196
+
>
197
+
<PlcLogView did={did} />
198
+
</Suspense>
199
+
</ErrorBoundary>
200
+
</Show>
201
<Show when={location.hash === "#backlinks"}>
202
<ErrorBoundary fallback={(err) => <div class="break-words">Error: {err.message}</div>}>
203
<Suspense
···
221
</ErrorBoundary>
222
</Show>
223
<Show when={nsids() && (!location.hash || location.hash === "#collections")}>
224
+
<TextInput
225
+
name="filter"
226
+
placeholder="Filter collections"
227
+
onInput={(e) => setFilter(e.currentTarget.value)}
228
+
class="grow"
229
+
/>
0
0
0
0
0
0
0
0
0
0
230
<div class="flex flex-col font-mono">
231
<div class="grid grid-cols-[min-content_1fr] items-center gap-x-2 overflow-hidden text-sm">
232
<For
···
282
<Show when={location.hash === "#identity"}>
283
<Show when={didDoc()}>
284
{(didDocument) => (
285
+
<div class="flex flex-col gap-y-1 wrap-anywhere">
286
+
<div class="flex items-baseline justify-between gap-2">
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
287
<div>
288
<div class="flex items-center gap-1">
289
+
<div class="iconify lucide--id-card" />
290
+
<p class="font-semibold">ID</p>
291
</div>
292
+
<div class="text-sm">{didDocument().id}</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
293
</div>
294
+
<Tooltip text="DID document">
295
+
<a
296
+
href={
297
+
did.startsWith("did:plc") ?
298
+
`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}`
299
+
: `https://${did.split("did:web:")[1]}/.well-known/did.json`
300
+
}
301
+
target="_blank"
302
+
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"
303
+
>
304
+
<span class="iconify lucide--external-link"></span>
305
+
</a>
306
+
</Tooltip>
307
+
</div>
308
+
<div>
309
+
<div class="flex items-center gap-1">
310
+
<div class="iconify lucide--at-sign" />
311
+
<p class="font-semibold">Aliases</p>
0
0
0
312
</div>
313
+
<ul>
314
+
<For each={didDocument().alsoKnownAs}>
315
+
{(alias) => (
316
+
<li class="flex items-center gap-1 text-sm">
317
+
<span>{alias}</span>
318
+
<Show when={alias.startsWith("at://")}>
319
+
<Tooltip
320
+
text={
321
+
validHandles[alias] === true ? "Valid handle"
322
+
: validHandles[alias] === undefined ?
323
+
"Validating"
324
+
: "Invalid handle"
325
+
}
326
+
>
327
+
<span
328
+
classList={{
329
+
"iconify lucide--circle-check": validHandles[alias] === true,
330
+
"iconify lucide--circle-x text-red-500 dark:text-red-400":
331
+
validHandles[alias] === false,
332
+
"iconify lucide--loader-circle animate-spin":
333
+
validHandles[alias] === undefined,
334
+
}}
335
+
></span>
336
+
</Tooltip>
337
</Show>
338
+
</li>
339
+
)}
340
+
</For>
341
+
</ul>
342
+
</div>
343
+
<div>
344
+
<div class="flex items-center gap-1">
345
+
<div class="iconify lucide--hard-drive" />
346
+
<p class="font-semibold">Services</p>
347
</div>
348
+
<ul>
349
+
<For each={didDocument().service}>
350
+
{(service) => (
351
+
<li class="flex flex-col text-sm">
352
+
<span>#{service.id.split("#")[1]}</span>
353
+
<a
354
+
class="w-fit text-blue-400 hover:underline active:underline"
355
+
href={service.serviceEndpoint.toString()}
356
+
target="_blank"
357
+
>
358
+
{service.serviceEndpoint.toString()}
359
+
</a>
360
+
</li>
361
+
)}
362
+
</For>
363
+
</ul>
364
</div>
365
+
<div>
366
+
<div class="flex items-center gap-1">
367
+
<div class="iconify lucide--shield-check" />
368
+
<p class="font-semibold">Verification methods</p>
369
+
</div>
370
+
<ul>
371
+
<For each={didDocument().verificationMethod}>
372
+
{(verif) => (
373
+
<Show when={verif.publicKeyMultibase}>
374
+
{(key) => (
375
+
<li class="flex flex-col text-sm">
376
+
<span>#{verif.id.split("#")[1]}</span>
377
+
<span class="flex items-center gap-0.5">
378
+
<div class="iconify lucide--key-round" />
379
+
<ErrorBoundary fallback={<>unknown</>}>
380
+
{parsePublicMultikey(key()).type}
381
+
</ErrorBoundary>
382
+
</span>
383
+
<span class="truncate">{key()}</span>
384
+
</li>
385
+
)}
386
+
</Show>
387
+
)}
388
+
</For>
389
+
</ul>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
390
</div>
0
0
0
0
0
0
391
</div>
392
)}
393
</Show>