Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
860270b
refactor: initial migration to remix
mundume Apr 7, 2026
893bd30
feat: upgrade to react router v7 from remix
mundume Apr 7, 2026
68f913d
refactor: add shared routes for the dashboard and change
mundume Apr 7, 2026
1105282
feat: enhance dashboard ui for settings page, overview page and logs …
mundume Apr 10, 2026
0556749
feat: upgrade the sidebar and settings ui
mundume Apr 10, 2026
274f0ae
refactor: move the create webhook modal and the deliveries drawer to…
mundume Apr 10, 2026
f7c5bb8
fix
mundume Apr 10, 2026
4bbbc6a
feat: move overview page from client data to server side data
mundume Apr 13, 2026
e319203
feat: add endpoint configurations to the api.ts, consume all the api …
mundume Apr 13, 2026
8570ffb
refactor: remove "use client" indications on the components
mundume Apr 14, 2026
af2c877
feat: implement pagination on all the logs
mundume Apr 17, 2026
cd99c33
feat: change the default limit from 20 to 10
mundume Apr 17, 2026
774170f
Merge remote-tracking branch 'origin/main' into api-integration
mundume May 11, 2026
eb96227
feat: add webhook detail page with event types, test dispatch, and re…
mundume May 12, 2026
cf23cdd
feat: add disable of webhooks and enhance button ui
mundume May 12, 2026
8e4d4f2
feat: add polling to events
mundume May 12, 2026
2b473dd
fix: add polling
mundume May 12, 2026
06b15d4
feat: add zod for schema validations
mundume May 12, 2026
ce73f22
feat: fix width for large screens
mundume May 12, 2026
a977b1c
fea: add edit webhooks
mundume May 13, 2026
85e18d2
feat: add edit functionality to webhoooks, move from manual polling t…
mundume May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/deliveries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub async fn get_webhook_deliveries(
#[utoipa::path(
get,
path = "/api/v1/deliveries/{id}",
params(("id" = i64, Path, description = "Delivery ID")),
params(("id" = String, Path, description = "Delivery ID")),
responses(
(status = 200, description = "Delivery", body = Delivery),
(status = 404, description = "Not found")
Expand All @@ -99,7 +99,7 @@ pub async fn get_webhook_deliveries(
)]
pub async fn get_delivery(
State(state): State<AppState>,
Path(id): Path<i64>,
Path(id): Path<String>,
) -> Result<Json<Delivery>, StatusCode> {
let delivery = sqlx::query_as::<_, Delivery>("SELECT * FROM deliveries WHERE id = ?")
.bind(id)
Expand Down
4 changes: 2 additions & 2 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub async fn get_webhook_events(
#[utoipa::path(
get,
path = "/api/v1/events/{id}",
params(("id" = i64, Path, description = "Event ID")),
params(("id" = String, Path, description = "Event ID")),
responses(
(status = 200, description = "Event", body = Event),
(status = 404, description = "Not found")
Expand All @@ -98,7 +98,7 @@ pub async fn get_webhook_events(
)]
pub async fn get_event(
State(state): State<AppState>,
Path(id): Path<i64>,
Path(id): Path<String>,
) -> Result<Json<Event>, StatusCode> {
let event = sqlx::query_as::<_, Event>("SELECT * FROM events WHERE id = ?")
.bind(id)
Expand Down
2 changes: 1 addition & 1 deletion src/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::app::AppState;

#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, ToSchema)]
pub struct Log {
id: i64,
id: String,
webhook_id: Option<String>,
level: String,
message: String,
Expand Down
10 changes: 5 additions & 5 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct UpsertSettings {
#[utoipa::path(
get,
path = "/api/v1/event-types/{id}/settings",
params(("id" = i64, Path, description = "Event type ID")),
params(("id" = String, Path, description = "Event type ID")),
responses(
(status = 200, description = "Settings for event type", body = Settings),
(status = 404, description = "No settings found for event type")
Expand All @@ -36,10 +36,10 @@ pub struct UpsertSettings {
)]
pub async fn get_event_type_settings(
State(state): State<AppState>,
Path(event_type_id): Path<i64>,
Path(event_type_id): Path<String>,
) -> Result<Json<Settings>, StatusCode> {
let settings = sqlx::query_as::<_, Settings>("SELECT * FROM settings WHERE event_type_id = ?")
.bind(event_type_id)
.bind(&event_type_id)
.fetch_one(&state.pool)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
Expand All @@ -49,7 +49,7 @@ pub async fn get_event_type_settings(
#[utoipa::path(
put,
path = "/api/v1/event-types/{id}/settings",
params(("id" = i64, Path, description = "Event type ID")),
params(("id" = String, Path, description = "Event type ID")),
request_body = UpsertSettings,
responses(
(status = 200, description = "Upserted settings", body = Settings)
Expand All @@ -58,7 +58,7 @@ pub async fn get_event_type_settings(
)]
pub async fn upsert_event_type_settings(
State(state): State<AppState>,
Path(event_type_id): Path<i64>,
Path(event_type_id): Path<String>,
Json(payload): Json<UpsertSettings>,
) -> Result<Json<Settings>, StatusCode> {
let setting_id = generate_id("ST");
Expand Down
13 changes: 9 additions & 4 deletions web/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
import type { ReactNode } from "react";
import { useState, type ReactNode } from "react";

export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient());

return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</QueryClientProvider>
);
}
4 changes: 4 additions & 0 deletions web/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export default [
index("routes/overview.tsx"),
route("webhooks", "routes/webhooks.tsx"),
route("webhooks/new", "routes/create-webhook.tsx"),
route("webhooks/:id/edit", "routes/edit-webhook.tsx"),
route("webhooks/:id", "routes/webhook-detail.tsx"),
route("webhooks/:webhookId/settings/:eventTypeId", "routes/event-settings.tsx"),
route("deliveries", "routes/deliveries.tsx"),
route("deliveries/:id", "routes/delivery-detail.tsx"),
route("events/:id", "routes/event-detail.tsx"),
route("logs", "routes/logs.tsx"),
route("settings", "routes/settings.tsx"),
]),
Expand Down
61 changes: 61 additions & 0 deletions web/app/routes/create-webhook.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,65 @@
import { redirect } from "react-router";
import { z } from "zod";
import { createWebhook } from "@/lib/api";
import { CreateWebhookPage } from "@/components/pages/create-webhook";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";

const createWebhookSchema = z.object({
name: z.string().trim().min(1, "Name is required."),
url: z.string().trim().url("Enter a valid URL."),
});

export function HydrateFallback() {
return (
<div className="mx-auto max-w-5xl px-6 py-8 space-y-8">
<div className="flex items-start gap-4">
<Skeleton className="h-10 w-10 mt-1" />
<div className="space-y-1">
<Skeleton className="h-9 w-48" />
<Skeleton className="h-5 w-80" />
</div>
</div>
<div className="max-w-2xl space-y-6">
<Card>
<CardHeader>
<Skeleton className="h-6 w-36" />
<Skeleton className="h-4 w-64 mt-1" />
</CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2">
<Skeleton className="h-4 w-12" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
</CardContent>
</Card>
<div className="flex items-center justify-end gap-3">
<Skeleton className="h-10 w-20" />
<Skeleton className="h-10 w-36" />
</div>
</div>
</div>
);
}

export async function clientAction({ request }: { request: Request }) {
const formData = await request.formData();
const result = createWebhookSchema.safeParse({
name: formData.get("name"),
url: formData.get("url"),
});

if (!result.success) {
return { ok: false, error: result.error.issues[0]?.message };
}

await createWebhook(result.data);
return redirect("/webhooks");
}

export default function CreateWebhook() {
return <CreateWebhookPage />;
Expand Down
47 changes: 45 additions & 2 deletions web/app/routes/deliveries.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
import { getDeliveries } from "@/lib/api";
import type { Delivery } from "@/lib/types";
import { DeliveriesPage } from "@/components/pages/deliveries";
import { Skeleton } from "@/components/ui/skeleton";
import { Card } from "@/components/ui/card";

export default function Deliveries() {
return <DeliveriesPage />;
const DEFAULT_LIMIT = 10;

export async function clientLoader({ request }: { request: Request }) {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page")) || 0;
const limit = Number(url.searchParams.get("limit")) || DEFAULT_LIMIT;
const deliveries = await getDeliveries({ page, limit });
return { deliveries, page, limit };
}

export function HydrateFallback() {
return (
<div className="mx-auto max-w-5xl px-6 py-8 space-y-6">
<div>
<Skeleton className="h-9 w-32 mb-2" />
<Skeleton className="h-5 w-72" />
</div>
<Card className="overflow-hidden">
<div className="p-4 space-y-3">
<Skeleton className="h-8 w-full" />
{["del-1", "del-2", "del-3", "del-4"].map((id) => (
<Skeleton key={id} className="h-12 w-full" />
))}
</div>
</Card>
</div>
);
}

export default function Deliveries({
loaderData,
}: {
loaderData: { deliveries: Delivery[]; page: number; limit: number };
}) {
return (
<DeliveriesPage
deliveries={loaderData.deliveries}
page={loaderData.page}
limit={loaderData.limit}
/>
);
}
72 changes: 70 additions & 2 deletions web/app/routes/delivery-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
import { getDelivery } from "@/lib/api";
import type { Delivery } from "@/lib/types";
import { DeliveryDetailPage } from "@/components/pages/delivery-detail";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";

export default function DeliveryDetail() {
return <DeliveryDetailPage />;
export async function clientLoader({
params,
}: { params: { id: string } }) {
const delivery = await getDelivery(params.id);
return { delivery };
}

export function HydrateFallback() {
return (
<div className="mx-auto max-w-5xl px-6 py-8 space-y-6">
<div className="flex items-center gap-4">
<Skeleton className="h-10 w-10" />
<div>
<Skeleton className="h-9 w-44" />
<Skeleton className="h-5 w-32 mt-1" />
</div>
</div>
<div className="max-w-2xl space-y-6">
<Card>
<CardHeader>
<Skeleton className="h-6 w-36" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
{["ev-name", "status", "duration", "timestamp"].map((id) => (
<div key={id}>
<Skeleton className="h-4 w-24 mb-1" />
<Skeleton className="h-5 w-32" />
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-5 w-32" />
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
<Separator />
<div>
<Skeleton className="h-5 w-28 mb-3" />
<Card>
<CardContent className="flex items-center gap-3 pt-4">
<Skeleton className="h-6 w-16" />
<div>
<Skeleton className="h-5 w-20" />
<Skeleton className="h-4 w-14 mt-1" />
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

export default function DeliveryDetail({
loaderData,
}: {
loaderData: { delivery: Delivery };
}) {
return <DeliveryDetailPage delivery={loaderData.delivery} />;
}
Loading