diff --git a/apps/server/src/checker/checker.ts b/apps/server/src/checker/checker.ts index 87e5c288..04aed689 100644 --- a/apps/server/src/checker/checker.ts +++ b/apps/server/src/checker/checker.ts @@ -88,7 +88,7 @@ export const ping = async ( ...headers, }, // Avoid having "TypeError: Request with a GET or HEAD method cannot have a body." error - ...(data.method !== "GET" && { body: data?.body }), + ...(data.method === "POST" && { body: data?.body }), }); return res; diff --git a/apps/server/src/checker/schema.ts b/apps/server/src/checker/schema.ts index d5875ca6..9b85dedd 100644 --- a/apps/server/src/checker/schema.ts +++ b/apps/server/src/checker/schema.ts @@ -1,11 +1,11 @@ import { z } from "zod"; -import { METHODS, status } from "@openstatus/db/src/schema"; +import { methods, status } from "@openstatus/db/src/schema"; export const payloadSchema = z.object({ workspaceId: z.string(), monitorId: z.string(), - method: z.enum(METHODS), + method: z.enum(methods), body: z.string().optional(), headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(), url: z.string(), diff --git a/apps/server/src/v1/monitor.ts b/apps/server/src/v1/monitor.ts index 30b6bd52..d6cad854 100644 --- a/apps/server/src/v1/monitor.ts +++ b/apps/server/src/v1/monitor.ts @@ -3,7 +3,7 @@ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; import { db, eq, sql } from "@openstatus/db"; import { flyRegions, - METHODS, + methods, monitor, periodicity, } from "@openstatus/db/src/schema/monitor"; @@ -63,7 +63,7 @@ const MonitorSchema = z description: "The description of your monitor", }) .nullable(), - method: z.enum(METHODS).default("GET").openapi({ example: "GET" }), + method: z.enum(methods).default("GET").openapi({ example: "GET" }), body: z .preprocess((val) => { return String(val); @@ -120,7 +120,7 @@ const monitorInput = z example: "Documenso website", description: "The description of your monitor", }), - method: z.enum(METHODS).default("GET").openapi({ example: "GET" }), + method: z.enum(methods).default("GET").openapi({ example: "GET" }), body: z.string().openapi({ example: "Hello World", description: "The body", diff --git a/apps/web/.env.example b/apps/web/.env.example index fef95c6b..5b9f12ea 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -66,5 +66,9 @@ AES_KEY= UNKEY_API_ID= UNKEY_TOKEN= -# FIXME: https://github.com/vercel/vercel/issues/10564 -VERCEL_CLI_VERSION=vercel@32.2.5 \ No newline at end of file +# GCP Messaging Queue +GCP_PROJECT_ID= +GCP_LOCATION= +GCP_CLIENT_EMAIL= +GCP_PRIVATE_KEY= +CRON_SECRET= \ No newline at end of file diff --git a/apps/web/src/app/api/checker/cron/_cron.ts b/apps/web/src/app/api/checker/cron/_cron.ts index f6ddefb6..7e330ee7 100644 --- a/apps/web/src/app/api/checker/cron/_cron.ts +++ b/apps/web/src/app/api/checker/cron/_cron.ts @@ -6,7 +6,7 @@ import type { z } from "zod"; import { createTRPCContext } from "@openstatus/api"; import { edgeRouter } from "@openstatus/api/src/edge"; -import { flyRegions, selectMonitorSchema } from "@openstatus/db/src/schema"; +import { selectMonitorSchema } from "@openstatus/db/src/schema"; import { env } from "@/env"; import type { payloadSchema } from "../schema"; @@ -58,8 +58,8 @@ export const cron = async ({ const allPages = await caller.monitor.getAllPagesForMonitor({ monitorId: row.id, }); - - for (const region of flyRegions) { + const selectedRegions = row.regions.length > 1 ? row.regions : ["auto"]; + for (const region of selectedRegions) { const payload: z.infer = { workspaceId: String(row.workspaceId), monitorId: String(row.id), @@ -76,7 +76,7 @@ export const cron = async ({ httpRequest: { headers: { "Content-Type": "application/json", // Set content type to ensure compatibility your application's request parsing - "fly-prefer-region": region, + ...(region !== "auto" && { "fly-prefer-region": region }), // Specify the region you want the request to be sent to Authorization: `Basic ${env.CRON_SECRET}`, }, httpMethod: "POST", @@ -90,40 +90,6 @@ export const cron = async ({ allResult.push(response); } } - // our first legacy monitor - if (periodicity === "10m") { - // Right now we are just checking the ping endpoint - for (const region of flyRegions) { - const payload: z.infer = { - workspaceId: "openstatus", - monitorId: "openstatusPing", - url: `https://api.openstatus.dev/ping`, - cronTimestamp: timestamp, - method: "GET", - pageIds: ["openstatus"], - status: "active", - }; - - const task: google.cloud.tasks.v2beta3.ITask = { - httpRequest: { - headers: { - "Content-Type": "application/json", // Set content type to ensure compatibility your application's request parsing - "fly-prefer-region": region, - Authorization: `Basic ${env.CRON_SECRET}`, - }, - httpMethod: "POST", - url: "https://api.openstatus.dev/checkerV2", - body: Buffer.from(JSON.stringify(payload)).toString("base64"), - }, - }; - - // TODO: fetch + try - catch + retry once - const request = { parent, task }; - const [response] = await client.createTask(request); - - allResult.push(response); - } - } await Promise.all(allResult); console.log(`End cron for ${periodicity} with ${allResult.length} jobs`); }; diff --git a/apps/web/src/app/api/checker/schema.ts b/apps/web/src/app/api/checker/schema.ts index d5875ca6..9b85dedd 100644 --- a/apps/web/src/app/api/checker/schema.ts +++ b/apps/web/src/app/api/checker/schema.ts @@ -1,11 +1,11 @@ import { z } from "zod"; -import { METHODS, status } from "@openstatus/db/src/schema"; +import { methods, status } from "@openstatus/db/src/schema"; export const payloadSchema = z.object({ workspaceId: z.string(), monitorId: z.string(), - method: z.enum(METHODS), + method: z.enum(methods), body: z.string().optional(), headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(), url: z.string(), diff --git a/apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/[id]/data/_components/chart-wrapper.tsx b/apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/[id]/data/_components/chart-wrapper.tsx index 8c195341..f194a70a 100644 --- a/apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/[id]/data/_components/chart-wrapper.tsx +++ b/apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/[id]/data/_components/chart-wrapper.tsx @@ -67,5 +67,6 @@ function renderTimestamp(timestamp: number, period: Period) { day: !isInDay ? "numeric" : undefined, hour: "2-digit", minute: "2-digit", + hour12: false, }); } diff --git a/apps/web/src/app/app/(dashboard)/onboarding/_components/path.tsx b/apps/web/src/app/app/(dashboard)/onboarding/_components/path.tsx deleted file mode 100644 index 317ff6d7..00000000 --- a/apps/web/src/app/app/(dashboard)/onboarding/_components/path.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; - -import { - Badge, - Button, - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@openstatus/ui"; - -import useUpdateSearchParams from "@/hooks/use-update-search-params"; - -export function Path() { - return ( -
- - -
- ); -} - -function HttpCard() { - const updateSearchParams = useUpdateSearchParams(); - const router = useRouter(); - return ( - - - Standard - - - HTTP Endpoint - - Monitor your API or website via POST or GET requests including custom - headers and body payload. - - - - - - - ); -} - -function VercelCard() { - async function trackEvent() { - await fetch("/api/analytics"); - } - - return ( - - - Beta - - - Vercel Integration - - Monitor your Vercel applications with ease. - - - - - - - - - - - Vercel Integration Beta - - - The integration is currently in closed beta. - - -
-

- Please contact us:{" "} - -

-
-
-
-
-
- ); -} diff --git a/apps/web/src/app/app/(dashboard)/onboarding/page.tsx b/apps/web/src/app/app/(dashboard)/onboarding/page.tsx index f0602a76..68dab602 100644 --- a/apps/web/src/app/app/(dashboard)/onboarding/page.tsx +++ b/apps/web/src/app/app/(dashboard)/onboarding/page.tsx @@ -9,13 +9,11 @@ import { MonitorForm } from "@/components/forms/monitor-form"; import { StatusPageForm } from "@/components/forms/status-page-form"; import { api } from "@/trpc/server"; import { Description } from "./_components/description"; -import { Path } from "./_components/path"; /** * allowed URL search params */ const searchParamsSchema = z.object({ - path: z.string().optional(), // "vercel" | "http" id: z.coerce.number().optional(), // monitorId workspaceSlug: z.string().optional(), }); @@ -32,31 +30,16 @@ export default async function Onboarding({ } // Instead of having the workspaceSlug in the search params, we can get it from the auth user - const { workspaceSlug, id: monitorId, path } = search.data; + const { workspaceSlug, id: monitorId } = search.data; if (!workspaceSlug) { - return "Waiting for Slug"; - } - - if (!path) { return ( -
-
- Skip - - } - /> -
-
- -
-
- -
+
+

Waiting for Slug

+
+
); @@ -73,7 +56,7 @@ export default async function Onboarding({ title="Get Started" description="Create your first monitor." actions={ - } diff --git a/apps/web/src/components/data-table/monitor/data-table-row-actions.tsx b/apps/web/src/components/data-table/monitor/data-table-row-actions.tsx index 4dd7e188..69d4949b 100644 --- a/apps/web/src/components/data-table/monitor/data-table-row-actions.tsx +++ b/apps/web/src/components/data-table/monitor/data-table-row-actions.tsx @@ -21,6 +21,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@openstatus/ui"; @@ -55,6 +56,23 @@ export function DataTableRowActions({ }); } + async function onToggleActive() { + startTransition(async () => { + try { + const { jobType, ...rest } = monitor; + if (!monitor.id) return; + await api.monitor.updateMonitor.mutate({ + ...rest, + active: !monitor.active, + }); + toast("success"); + router.refresh(); + } catch { + toast("error"); + } + }); + } + async function onTest() { startTransition(async () => { const { url, body, method, headers } = monitor; @@ -92,7 +110,12 @@ export function DataTableRowActions({ Details - Test + + Test endpoint + + {monitor.active ? "Pause" : "Resume"} monitor + + Delete diff --git a/apps/web/src/components/forms/advanced-monitor-form.tsx b/apps/web/src/components/forms/advanced-monitor-form.tsx deleted file mode 100644 index 65b980f0..00000000 --- a/apps/web/src/components/forms/advanced-monitor-form.tsx +++ /dev/null @@ -1,263 +0,0 @@ -"use client"; - -import * as React from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Wand2, X } from "lucide-react"; -import { useFieldArray, useForm } from "react-hook-form"; -import * as z from "zod"; - -import { - Button, - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Textarea, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@openstatus/ui"; - -import { useToastAction } from "@/hooks/use-toast-action"; -import { api } from "@/trpc/client"; -import { LoadingAnimation } from "../loading-animation"; - -const methods = ["POST", "GET"] as const; -const methodsEnum = z.enum(methods); - -const headersSchema = z - .array(z.object({ key: z.string(), value: z.string() })) - .optional(); - -const advancedSchema = z.discriminatedUnion("method", [ - z.object({ - method: z.literal("GET"), - body: z.string().length(0).optional(), - headers: headersSchema, - }), - z.object({ - method: z.literal("POST"), - body: z.string().optional(), - headers: headersSchema, - }), -]); - -type AdvancedMonitorProps = z.infer; - -interface Props { - defaultValues?: AdvancedMonitorProps; - workspaceSlug: string; -} - -export function AdvancedMonitorForm({ defaultValues, workspaceSlug }: Props) { - const form = useForm({ - resolver: zodResolver(advancedSchema), - defaultValues: { - headers: Boolean(defaultValues?.headers?.length) - ? defaultValues?.headers - : [{ key: "", value: "" }], - body: defaultValues?.body ?? "", - method: defaultValues?.method ?? "GET", - }, - }); - const watchMethod = form.watch("method"); - const router = useRouter(); - const { toast } = useToastAction(); - const searchParams = useSearchParams(); - const monitorId = searchParams?.get("id"); - const [isPending, startTransition] = React.useTransition(); - - const { fields, append, remove } = useFieldArray({ - name: "headers", - control: form.control, - }); - - const onSubmit = ({ headers, ...props }: AdvancedMonitorProps) => { - startTransition(async () => { - try { - if (!monitorId) return; - if (validateJSON(props.body) === false) return; - await api.monitor.updateMonitorAdvanced.mutate({ - id: Number(monitorId), - headers: headers?.filter(({ key }) => key !== ""), // avoid saving empty key headers - ...props, - }); - toast("saved"); - router.refresh(); - } catch (e) { - toast("error"); - } - }); - }; - - const validateJSON = (value?: string) => { - if (!value) return; - try { - const obj = JSON.parse(value) as Record; - form.clearErrors("body"); - return obj; - } catch (e) { - form.setError("body", { - message: "Not a valid JSON object", - }); - return false; - } - }; - - const onPrettifyJSON = () => { - const body = form.getValues("body"); - const obj = validateJSON(body); - if (obj) { - const pretty = JSON.stringify(obj, undefined, 4); - form.setValue("body", pretty); - } - }; - - return ( -
- -
- {/* TODO: add FormDescription for latest key/value */} - Request Header - {fields.map((field, index) => ( -
- ( - - - - - - )} - /> -
- ( - - - - - - )} - /> - -
-
- ))} -
- -
-
- ( - - Method - - What method to use? - - - )} - /> - {watchMethod === "POST" && ( -
- ( - -
- Body - - - - - - -

Prettify JSON

-
-
-
-
- -