Implement portal frontend for events
This commit is contained in:
@@ -367,3 +367,4 @@ app.listen(3000, async () => {
|
||||
});
|
||||
|
||||
export type API = typeof app
|
||||
export { type EventStatus } from "./services/events";
|
||||
|
||||
@@ -54,7 +54,7 @@ export class EventsService {
|
||||
} = {}, trx?: Transaction<DB>) {
|
||||
return await (trx ?? db).selectFrom("events")
|
||||
.select([
|
||||
"id", "type", "source", "started_at", "ended_at", "available_at",
|
||||
"id", "group", "type", "source", "started_at", "ended_at", "available_at",
|
||||
"errors", "rate_limits", "has_failed", "last_error", "created_at",
|
||||
])
|
||||
.$if(opt.filter?.group !== undefined, (qb) => qb.where("group", "=", opt.filter!.group!))
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
{
|
||||
name: "Apparel",
|
||||
path: "/apparel",
|
||||
links: [{ name: "Products", path: "/products" }],
|
||||
links: [
|
||||
{ name: "Products", path: "/products" },
|
||||
{ name: "Activity", path: "/activity" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<script lang="ts">
|
||||
import { api } from "$lib/api.js";
|
||||
import type { EventStatus } from "@blade-and-brawn/api";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type EventRow = NonNullable<
|
||||
Awaited<ReturnType<typeof api.events.get>>["data"]
|
||||
>[number];
|
||||
|
||||
const LIMIT = 25;
|
||||
|
||||
const STATUS_BADGE: Record<EventStatus, string> = {
|
||||
waiting: "badge-ghost",
|
||||
available: "badge-info",
|
||||
processing: "badge-warning",
|
||||
failed: "badge-error",
|
||||
fulfilled: "badge-success",
|
||||
};
|
||||
|
||||
let events = $state<EventRow[]>([]);
|
||||
let loading = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
let offset = $state(0);
|
||||
let group = $state("");
|
||||
let status = $state<EventStatus | "">("");
|
||||
let replayingIds = $state<string[]>([]);
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
const value = (err as { value?: { error?: string } })?.value;
|
||||
return (
|
||||
value?.error ??
|
||||
(err instanceof Error ? err.message : "Unknown error")
|
||||
);
|
||||
}
|
||||
|
||||
function formatLabel(value: string): string {
|
||||
return value
|
||||
.replace(/^apparel_/, "")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function formatDate(value: string | Date | null): string {
|
||||
if (!value) return "—";
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function errorSummary(lastError: unknown): string {
|
||||
if (
|
||||
lastError &&
|
||||
typeof lastError === "object" &&
|
||||
"message" in lastError
|
||||
) {
|
||||
return String((lastError as { message: unknown }).message);
|
||||
}
|
||||
return "View error";
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
loading = true;
|
||||
loadError = null;
|
||||
try {
|
||||
const res = await api.events.get({
|
||||
query: {
|
||||
group: group || undefined,
|
||||
status: status || undefined,
|
||||
limit: LIMIT,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
events = res.data as EventRow[];
|
||||
} catch (err) {
|
||||
loadError = errorMessage(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setGroup(value: string) {
|
||||
group = value;
|
||||
offset = 0;
|
||||
refresh();
|
||||
}
|
||||
|
||||
function setStatus(value: string) {
|
||||
status = value as EventStatus | "";
|
||||
offset = 0;
|
||||
refresh();
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
offset = Math.max(0, offset - LIMIT);
|
||||
refresh();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
offset += LIMIT;
|
||||
refresh();
|
||||
}
|
||||
|
||||
async function replay(id: string) {
|
||||
replayingIds = [...replayingIds, id];
|
||||
try {
|
||||
const res = await api.events({ id }).replay.post();
|
||||
if (res.error) throw res.error;
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
loadError = errorMessage(err);
|
||||
} finally {
|
||||
replayingIds = replayingIds.filter((x) => x !== id);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
</script>
|
||||
|
||||
<div class="w-full flex items-center gap-2 flex-wrap mt-10 mb-5">
|
||||
<select
|
||||
class="select select-sm w-40"
|
||||
value={group}
|
||||
onchange={(e) => setGroup(e.currentTarget.value)}
|
||||
>
|
||||
<option value="">All groups</option>
|
||||
<option value="apparel_sync">Sync</option>
|
||||
<option value="apparel_order">Orders</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
class="select select-sm w-40"
|
||||
value={status}
|
||||
onchange={(e) => setStatus(e.currentTarget.value)}
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
<option value="waiting">Waiting</option>
|
||||
<option value="available">Available</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="fulfilled">Fulfilled</option>
|
||||
</select>
|
||||
|
||||
<button class="btn btn-sm" onclick={refresh} disabled={loading}>
|
||||
{loading ? "Refreshing..." : "Refresh"}
|
||||
</button>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
onclick={prevPage}
|
||||
disabled={offset === 0 || loading}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
onclick={nextPage}
|
||||
disabled={events.length < LIMIT || loading}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loadError}
|
||||
<div role="alert" class="alert alert-error w-full mb-4">
|
||||
<span>{loadError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Source</th>
|
||||
<th>Created</th>
|
||||
<th>Started</th>
|
||||
<th>Ended</th>
|
||||
<th>Errors</th>
|
||||
<th>Rate limits</th>
|
||||
<th>Last error</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each events as event (event.id)}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td>{formatLabel(event.group)}</td>
|
||||
<td>{formatLabel(event.type)}</td>
|
||||
<td
|
||||
><div class="badge {STATUS_BADGE[event.status]}">
|
||||
{event.status}
|
||||
</div></td
|
||||
>
|
||||
<td>{event.source}</td>
|
||||
<td class="whitespace-nowrap"
|
||||
>{formatDate(event.created_at)}</td
|
||||
>
|
||||
<td class="whitespace-nowrap"
|
||||
>{formatDate(event.started_at)}</td
|
||||
>
|
||||
<td class="whitespace-nowrap"
|
||||
>{formatDate(event.ended_at)}</td
|
||||
>
|
||||
<td>{event.errors}</td>
|
||||
<td>{event.rate_limits}</td>
|
||||
<td>
|
||||
{#if event.last_error}
|
||||
<details>
|
||||
<summary
|
||||
class="cursor-pointer text-error text-sm"
|
||||
>
|
||||
{errorSummary(event.last_error)}
|
||||
</summary>
|
||||
<pre
|
||||
class="text-xs whitespace-pre-wrap max-w-md">{JSON.stringify(
|
||||
event.last_error,
|
||||
null,
|
||||
2,
|
||||
)}</pre>
|
||||
</details>
|
||||
{:else}
|
||||
—
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if event.status === "failed"}
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
disabled={replayingIds.includes(event.id)}
|
||||
onclick={() => replay(event.id)}
|
||||
>
|
||||
{replayingIds.includes(event.id)
|
||||
? "Replaying..."
|
||||
: "Replay"}
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="11" class="text-center opacity-60">
|
||||
{loading ? "Loading..." : "No events found"}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -2,18 +2,14 @@
|
||||
import type { Printful } from "@blade-and-brawn/commerce";
|
||||
import { onMount } from "svelte";
|
||||
import { api } from "$lib/api.js";
|
||||
|
||||
type SyncStatus =
|
||||
| "waiting"
|
||||
| "available"
|
||||
| "processing"
|
||||
| "failed"
|
||||
| "fulfilled";
|
||||
import type { EventStatus } from "@blade-and-brawn/api";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
type SyncStatus = EventStatus | "none";
|
||||
|
||||
const synchronizer = $state({
|
||||
status: "none" as SyncStatus | "none",
|
||||
status: "none" as SyncStatus,
|
||||
syncingPProductIds: [] as number[],
|
||||
get isInProgress() {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user