|
2 | 2 | import { onMount } from "svelte"; |
3 | 3 | import { goto } from "$app/navigation"; |
4 | 4 | import { get } from "svelte/store"; |
5 | | - import { api } from "$lib/api"; |
| 5 | + import { api, API_BASE } from "$lib/api"; |
| 6 | + import { fetchSchemas, type OntologySchema } from "$lib/ontology"; |
6 | 7 | import { sessionToken } from "$lib/session"; |
7 | 8 |
|
| 9 | + /** Link straight to the interactive API reference, derived from API_BASE. */ |
| 10 | + const apiDocsUrl = `${API_BASE}/docs`; |
| 11 | +
|
8 | 12 | interface Consumer { |
9 | 13 | id: string; |
10 | 14 | ename: string; |
|
37 | 41 |
|
38 | 42 | // new subscription form |
39 | 43 | let subTarget = $state(""); |
40 | | - let subOntologies = $state(""); |
41 | | - let subEvaults = $state(""); |
| 44 | + let schemas = $state<OntologySchema[]>([]); |
| 45 | + let selectedOntologies = $state<string[]>([]); |
| 46 | + let ontologyPick = $state(""); |
| 47 | + let evaultTags = $state<string[]>([]); |
| 48 | + let evaultInput = $state(""); |
42 | 49 |
|
43 | 50 | const token = () => get(sessionToken); |
44 | 51 |
|
| 52 | + function ontologyTitle(id: string): string { |
| 53 | + return schemas.find((s) => s.id === id)?.title ?? id; |
| 54 | + } |
| 55 | +
|
| 56 | + function addOntology() { |
| 57 | + if (ontologyPick && !selectedOntologies.includes(ontologyPick)) { |
| 58 | + selectedOntologies = [...selectedOntologies, ontologyPick]; |
| 59 | + } |
| 60 | + ontologyPick = ""; |
| 61 | + } |
| 62 | +
|
| 63 | + function removeOntology(id: string) { |
| 64 | + selectedOntologies = selectedOntologies.filter((o) => o !== id); |
| 65 | + } |
| 66 | +
|
| 67 | + function addEvaultTag() { |
| 68 | + const tag = evaultInput.trim(); |
| 69 | + if (tag && !evaultTags.includes(tag)) { |
| 70 | + evaultTags = [...evaultTags, tag]; |
| 71 | + } |
| 72 | + evaultInput = ""; |
| 73 | + } |
| 74 | +
|
| 75 | + function removeEvaultTag(tag: string) { |
| 76 | + evaultTags = evaultTags.filter((t) => t !== tag); |
| 77 | + } |
| 78 | +
|
45 | 79 | async function loadApproved() { |
46 | 80 | const [subs, dels, keys] = await Promise.all([ |
47 | 81 | api<{ subscriptions: Subscription[] }>("/api/subscriptions", { |
|
105 | 139 | token: token(), |
106 | 140 | body: { |
107 | 141 | targetUrl: subTarget || undefined, |
108 | | - ontologyFilter: subOntologies |
109 | | - .split(",") |
110 | | - .map((o) => o.trim()) |
111 | | - .filter(Boolean), |
112 | | - evaultFilter: subEvaults |
113 | | - .split(",") |
114 | | - .map((o) => o.trim()) |
115 | | - .filter(Boolean), |
| 142 | + ontologyFilter: selectedOntologies, |
| 143 | + evaultFilter: evaultTags, |
116 | 144 | }, |
117 | 145 | }); |
118 | | - subTarget = subOntologies = subEvaults = ""; |
| 146 | + subTarget = ""; |
| 147 | + selectedOntologies = []; |
| 148 | + evaultTags = []; |
119 | 149 | await loadApproved(); |
120 | 150 | } catch (e) { |
121 | 151 | error = e instanceof Error ? e.message : "failed to create subscription"; |
|
136 | 166 | return; |
137 | 167 | } |
138 | 168 | void load(); |
| 169 | + // Best-effort: the form still works if the ontology service is down. |
| 170 | + fetchSchemas() |
| 171 | + .then((s) => (schemas = s)) |
| 172 | + .catch(() => (schemas = [])); |
139 | 173 | }); |
140 | 174 | </script> |
141 | 175 |
|
142 | 176 | <section> |
143 | | - <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1> |
| 177 | + <div class="flex items-center justify-between"> |
| 178 | + <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1> |
| 179 | + <a |
| 180 | + href={apiDocsUrl} |
| 181 | + target="_blank" |
| 182 | + rel="noopener" |
| 183 | + class="rounded border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 hover:bg-indigo-100" |
| 184 | + > |
| 185 | + API docs ↗ |
| 186 | + </a> |
| 187 | + </div> |
144 | 188 |
|
145 | 189 | {#if error} |
146 | 190 | <p class="mt-4 rounded bg-red-50 px-4 py-2 text-sm text-red-700">{error}</p> |
|
213 | 257 | <!-- Subscriptions --> |
214 | 258 | <div class="mt-6 rounded-lg border border-gray-200 bg-white p-6"> |
215 | 259 | <h2 class="font-semibold text-gray-900">Webhook subscriptions</h2> |
216 | | - <div class="mt-3 grid gap-2 sm:grid-cols-3"> |
217 | | - <input |
218 | | - bind:value={subTarget} |
219 | | - placeholder="Target URL (optional)" |
220 | | - class="rounded border border-gray-300 px-3 py-2 text-sm" |
221 | | - /> |
222 | | - <input |
223 | | - bind:value={subOntologies} |
224 | | - placeholder="Ontologies (comma, blank = all)" |
225 | | - class="rounded border border-gray-300 px-3 py-2 text-sm" |
226 | | - /> |
227 | | - <input |
228 | | - bind:value={subEvaults} |
229 | | - placeholder="eVaults (comma, blank = all)" |
230 | | - class="rounded border border-gray-300 px-3 py-2 text-sm" |
231 | | - /> |
| 260 | + |
| 261 | + <div class="mt-3 space-y-3"> |
| 262 | + <label class="block"> |
| 263 | + <span class="text-xs font-medium text-gray-600"> |
| 264 | + Target URL (optional — defaults to your webhook base) |
| 265 | + </span> |
| 266 | + <input |
| 267 | + bind:value={subTarget} |
| 268 | + placeholder="https://my-platform.example/api/webhook" |
| 269 | + class="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm" |
| 270 | + /> |
| 271 | + </label> |
| 272 | + |
| 273 | + <!-- Ontology picker, fed by the ontology service --> |
| 274 | + <div> |
| 275 | + <span class="text-xs font-medium text-gray-600"> |
| 276 | + Ontologies (none = all) |
| 277 | + </span> |
| 278 | + <div class="mt-1 flex gap-2"> |
| 279 | + <select |
| 280 | + bind:value={ontologyPick} |
| 281 | + class="flex-1 rounded border border-gray-300 px-3 py-2 text-sm" |
| 282 | + > |
| 283 | + <option value="">Select an ontology…</option> |
| 284 | + {#each schemas as schema (schema.id)} |
| 285 | + <option value={schema.id}> |
| 286 | + {schema.title} ({schema.id}) |
| 287 | + </option> |
| 288 | + {/each} |
| 289 | + </select> |
| 290 | + <button |
| 291 | + type="button" |
| 292 | + class="rounded border border-gray-300 px-3 py-2 text-sm" |
| 293 | + onclick={addOntology} |
| 294 | + > |
| 295 | + Add |
| 296 | + </button> |
| 297 | + </div> |
| 298 | + {#if schemas.length === 0} |
| 299 | + <p class="mt-1 text-xs text-gray-400"> |
| 300 | + Ontology catalogue unavailable. |
| 301 | + </p> |
| 302 | + {/if} |
| 303 | + {#if selectedOntologies.length} |
| 304 | + <div class="mt-2 flex flex-wrap gap-2"> |
| 305 | + {#each selectedOntologies as id (id)} |
| 306 | + <span |
| 307 | + class="inline-flex items-center gap-1 rounded-full bg-indigo-50 px-2.5 py-1 text-xs text-indigo-700" |
| 308 | + > |
| 309 | + {ontologyTitle(id)} |
| 310 | + <button |
| 311 | + type="button" |
| 312 | + class="text-indigo-400 hover:text-indigo-700" |
| 313 | + onclick={() => removeOntology(id)} |
| 314 | + > |
| 315 | + ✕ |
| 316 | + </button> |
| 317 | + </span> |
| 318 | + {/each} |
| 319 | + </div> |
| 320 | + {/if} |
| 321 | + </div> |
| 322 | + |
| 323 | + <!-- eVault filter as a tag input --> |
| 324 | + <div> |
| 325 | + <span class="text-xs font-medium text-gray-600"> |
| 326 | + eVaults (none = all) |
| 327 | + </span> |
| 328 | + <input |
| 329 | + bind:value={evaultInput} |
| 330 | + onkeydown={(e) => { |
| 331 | + if (e.key === "Enter") { |
| 332 | + e.preventDefault(); |
| 333 | + addEvaultTag(); |
| 334 | + } |
| 335 | + }} |
| 336 | + placeholder="Type an eVault (w3id or public key) and press Enter" |
| 337 | + class="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm" |
| 338 | + /> |
| 339 | + {#if evaultTags.length} |
| 340 | + <div class="mt-2 flex flex-wrap gap-2"> |
| 341 | + {#each evaultTags as tag (tag)} |
| 342 | + <span |
| 343 | + class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-xs text-gray-700" |
| 344 | + > |
| 345 | + {tag} |
| 346 | + <button |
| 347 | + type="button" |
| 348 | + class="text-gray-400 hover:text-gray-700" |
| 349 | + onclick={() => removeEvaultTag(tag)} |
| 350 | + > |
| 351 | + ✕ |
| 352 | + </button> |
| 353 | + </span> |
| 354 | + {/each} |
| 355 | + </div> |
| 356 | + {/if} |
| 357 | + </div> |
232 | 358 | </div> |
| 359 | + |
233 | 360 | <button |
234 | 361 | class="mt-3 rounded bg-indigo-600 px-3 py-1.5 text-sm text-white" |
235 | 362 | onclick={createSubscription} |
|
250 | 377 | </div> |
251 | 378 | <p class="mt-1 text-gray-500"> |
252 | 379 | ontologies: {sub.ontologyFilter.length |
253 | | - ? sub.ontologyFilter.join(", ") |
| 380 | + ? sub.ontologyFilter |
| 381 | + .map((o) => ontologyTitle(o)) |
| 382 | + .join(", ") |
254 | 383 | : "all"} · eVaults: {sub.evaultFilter.length |
255 | 384 | ? sub.evaultFilter.join(", ") |
256 | 385 | : "all"} · {sub.active ? "active" : "inactive"} |
|
0 commit comments