Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: single checker page #1028

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 8 additions & 14 deletions apps/server/src/v1/check/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ export function registerPostCheck(api: typeof checkAPI) {
const workspaceId = Number(c.get("workspaceId"));
const input = c.req.valid("json");

const { headers, regions, runCount, aggregated, ...rest } = data;
const { headers, metadata, regions, runCount, aggregated, ...rest } = data;

const newCheck = await db
.insert(check)
.values({
workspaceId: Number(workspaceId),
regions: regions.join(","),
countRequests: runCount,
headers: headers ? JSON.stringify(headers) : undefined,
metadata: metadata ? JSON.stringify(metadata) : undefined,
...rest,
})
.returning()
Expand All @@ -78,16 +80,8 @@ export function registerPostCheck(api: typeof checkAPI) {
workspaceId: workspaceId,
url: input.url,
method: input.method,
headers: input.headers?.reduce((acc, { key, value }) => {
if (!key) return acc; // key === "" is an invalid header

return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...acc,
[key]: value,
};
}, {}),
body: input.body ? input.body : undefined,
headers: input.headers ?? undefined,
body: input.body ?? undefined,
}),
});
currentFetch.push(r);
Expand Down Expand Up @@ -157,10 +151,10 @@ function getTiming(data: z.infer<typeof ResponseSchema>[]): ReturnGetTiming {
prev.dns.push(curr.timing.dnsDone - curr.timing.dnsStart);
prev.connect.push(curr.timing.connectDone - curr.timing.connectStart);
prev.tls.push(
curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart,
curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart
);
prev.firstByte.push(
curr.timing.firstByteDone - curr.timing.firstByteStart,
curr.timing.firstByteDone - curr.timing.firstByteStart
);
prev.transfer.push(curr.timing.transferDone - curr.timing.transferStart);
prev.latency.push(curr.latency);
Expand All @@ -173,7 +167,7 @@ function getTiming(data: z.infer<typeof ResponseSchema>[]): ReturnGetTiming {
firstByte: [],
transfer: [],
latency: [],
} as ReturnGetTiming,
} as ReturnGetTiming
);
}

Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/v1/check/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MonitorSchema } from "../monitors/schema";
export const CheckSchema = MonitorSchema.pick({
url: true,
body: true,
headers: true,
method: true,
regions: true,
})
Expand All @@ -19,6 +18,17 @@ export const CheckSchema = MonitorSchema.pick({
.boolean()
.optional()
.openapi({ description: "Whether to aggregate the results or not" }),
metadata: z
.record(z.string())
.optional()
.openapi({
description:
"The metadata of the check. Makes it easier to search for checks",
example: { env: "production", platform: "github" },
}),
headers: z.record(z.string()).optional().openapi({
description: "The headers to send with the request",
}),
// webhook: z
// .string()
// .optional()
Expand Down Expand Up @@ -132,6 +142,7 @@ export const AggregatedResult = z.object({

export const CheckPostResponseSchema = z.object({
id: z.number().int().openapi({ description: "The id of the check" }),
// TBD: include the region + more (e.g. statusCode, latency, ... ) in here!
raw: z.array(TimingSchema).openapi({
description: "The raw data of the check",
}),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/v1/monitors/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const MonitorSchema = z
method: z.enum(monitorMethods).default("GET").openapi({ example: "GET" }),
body: z
.preprocess((val) => {
if (typeof val === "object") return JSON.stringify(val);
return String(val);
}, z.string())
.nullish()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ReactNode } from "react";

import { Header } from "@/components/dashboard/header";
import AppPageLayout from "@/components/layout/app-page-layout";
import { Button } from "@openstatus/ui";
import { ArrowUpRight } from "lucide-react";

export default async function Layout({ children }: { children: ReactNode }) {
return (
<AppPageLayout>
<Header
title="Single Checks"
description="Built custom CI/CD pipelines with OpenStatus API. Access your checks within the table."
actions={
<Button asChild>
<a
href="https://docs.openstatus.dev/api-reference/check/post-check"
target="_blank"
rel="noreferrer"
className="whitespace-nowrap"
>
API Docs <ArrowUpRight className="ml-1 h-4 w-4" />
</a>
</Button>
}
/>
{children}
</AppPageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton";

export default function Loading() {
return <DataTableSkeleton />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { columns } from "@/components/data-table/check/columns";
import { DataTable } from "@/components/data-table/check/data-table";
import { env } from "@/env";
import { api } from "@/trpc/server";
import { OSTinybird } from "@openstatus/tinybird";
import { searchParamsCache } from "./search-params";

const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY });

type Props = {
searchParams: { [key: string]: string | string[] | undefined };
};

export default async function Page({ searchParams }: Props) {
const { page, pageSize } = searchParamsCache.parse(searchParams);
const data = await api.check.getChecksByWorkspace.query();

return <div>{data ? <DataTable columns={columns} data={data} /> : null}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createSearchParamsCache, parseAsInteger } from "nuqs/server";

export const searchParamsParsers = {
pageSize: parseAsInteger.withDefault(10),
page: parseAsInteger.withDefault(0),
};
export const searchParamsCache = createSearchParamsCache(searchParamsParsers);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { useQueryStates } from "nuqs";
import { useEffect } from "react";
import { searchParamsParsers } from "./search-params";

export function Client({ totalRows }: { totalRows: number }) {
const [search, setSearch] = useQueryStates(searchParamsParsers);

useEffect(() => {
if (typeof window === "undefined") return;

function onScroll() {
// TODO: add a threshold for the "Load More" button
const onPageBottom =
window.innerHeight + Math.round(window.scrollY) >=
document.body.offsetHeight;
if (onPageBottom && search.pageSize * search.page <= totalRows) {
setSearch(
{
// page: search.page + 1
pageSize: search.pageSize + 10,
},
{ shallow: false },
);
}
}

window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [search, setSearch, totalRows]);

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ReactNode } from "react";

import { Header } from "@/components/dashboard/header";
import AppPageLayout from "@/components/layout/app-page-layout";
import { api } from "@/trpc/server";
import { notFound } from "next/navigation";
import { RequestDetailsDialog } from "./request-details-dialog";

export default async function Layout({
params,
children,
}: {
params: { id: string };
children: ReactNode;
}) {
const check = await api.check.getCheckById.query({
id: Number.parseInt(params.id),
});

if (!check) return notFound();

return (
<AppPageLayout>
<Header
title={`Check #${check.id}`}
description={<div className="font-mono truncate">{check.url}</div>}
actions={<RequestDetailsDialog check={check} />}
/>
{children}
</AppPageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton";

export default function Loading() {
return <DataTableSkeleton />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { columns } from "@/components/data-table/single-check/columns";
import { DataTable } from "@/components/data-table/single-check/data-table";
import { env } from "@/env";
import { api } from "@/trpc/server";
import { OSTinybird } from "@openstatus/tinybird";
import { Client } from "./client";
import { searchParamsCache } from "./search-params";

const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY });

type Props = {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
};

export default async function Page({ params, searchParams }: Props) {
const { page, pageSize } = searchParamsCache.parse(searchParams);
const workspace = await api.workspace.getWorkspace.query();
const data = await tb.endpointSingleCheckList()({
workspaceId: workspace.id,
requestId: Number.parseInt(params.id),
page,
pageSize,
});

return (
<div>
{data ? <DataTable columns={columns} data={data} /> : null}
<Client totalRows={data?.length || 0} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Check } from "@openstatus/db/src/schema";
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@openstatus/ui";
import { format } from "date-fns";
import { Minus } from "lucide-react";

export function RequestDetailsDialog({ check }: { check: Check }) {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="whitespace-nowrap">Request Details</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Request Details</DialogTitle>
<DialogDescription>Details of #{check.id}</DialogDescription>
</DialogHeader>
<dl className="grid gap-2">
<div>
<dt className="text-sm text-muted-foreground">URL</dt>
<dd className="font-mono">{check.url}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Method</dt>
<dd className="font-mono">{check.method}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Created At</dt>
<dd className="font-mono">
{!check.createdAt ? (
<Minus className="h-4 w-4" />
) : (
format(new Date(check.createdAt), "LLL dd, y HH:mm:ss")
)}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Regions</dt>
<dd className="font-mono">
{check.regions.length ? (
check.regions.join(", ")
) : (
<Minus className="h-4 w-4" />
)}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Headers</dt>
<dd>
{!check.headers ? (
<Minus className="h-4 w-4" />
) : (
<pre>{JSON.stringify(check.headers, null, 2)}</pre>
)}
</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Body</dt>
<dd>
{!check.body ? (
<Minus className="h-4 w-4" />
) : (
<pre>{JSON.stringify(check.body, null, 2)}</pre>
)}
</dd>
</div>
</dl>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createSearchParamsCache, parseAsInteger } from "nuqs/server";

export const searchParamsParsers = {
pageSize: parseAsInteger.withDefault(10),
page: parseAsInteger.withDefault(0),
};
export const searchParamsCache = createSearchParamsCache(searchParamsParsers);
Loading
Loading