Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add section-otel

authored by

Maximilian Kaske and committed by
Maximilian Kaske
a3474e7d 4abe03fd

+164 -6
+19
apps/web/src/components/banner/coming-soon-banner.tsx
··· 1 + import { Alert, AlertTitle, AlertDescription } from "@openstatus/ui"; 2 + import { Hourglass } from "lucide-react"; 3 + 4 + interface Props { 5 + description?: string; 6 + } 7 + 8 + export function ComingSoonBanner({ description }: Props) { 9 + return ( 10 + <Alert> 11 + <Hourglass className="h-4 w-4" /> 12 + <AlertTitle>Coming Soon</AlertTitle> 13 + <AlertDescription> 14 + {description ?? 15 + "This feature is coming soon. Keep an eye on our changelog."} 16 + </AlertDescription> 17 + </Alert> 18 + ); 19 + }
+19 -6
apps/web/src/components/forms/monitor/form.tsx
··· 39 39 import { SectionRequests } from "./section-requests"; 40 40 import { SectionScheduling } from "./section-scheduling"; 41 41 import { SectionStatusPage } from "./section-status-page"; 42 + import { SectionOtel } from "./section-otel"; 42 43 43 44 interface Props { 44 45 defaultSection?: string; ··· 92 93 // biome-ignore lint/suspicious/noExplicitAny: <explanation> 93 94 headerAssertions: _assertions.filter((a) => a.type === "header") as any, // TS considers a.type === "status" 94 95 textBodyAssertions: _assertions.filter( 95 - (a) => a.type === "textBody", 96 + (a) => a.type === "textBody" 96 97 // biome-ignore lint/suspicious/noExplicitAny: <explanation> 97 98 ) as any, // TS considers a.type === "textBody" 98 99 degradedAfter: defaultValues?.degradedAfter, ··· 106 107 const [pingFailed, setPingFailed] = React.useState(false); 107 108 const type = React.useMemo( 108 109 () => (defaultValues ? "update" : "create"), 109 - [defaultValues], 110 + [defaultValues] 110 111 ); 111 112 112 113 const handleDataUpdateOrInsertion = async (props: InsertMonitor) => { ··· 153 154 finally: () => { 154 155 setPending(false); 155 156 }, 156 - }, 157 + } 157 158 ); 158 159 }; 159 160 ··· 188 189 body && 189 190 body !== "" && 190 191 headers?.some( 191 - (h) => h.key === "Content-Type" && h.value === "application/json", 192 + (h) => h.key === "Content-Type" && h.value === "application/json" 192 193 ) 193 194 ) { 194 195 const validJSON = validateJSON(body); ··· 217 218 ...(statusAssertions || []), 218 219 ...(headerAssertions || []), 219 220 ...(textBodyAssertions || []), 220 - ]), 221 + ]) 221 222 ); 222 223 223 224 const data = (await res.json()) as RegionChecker; ··· 253 254 if (error instanceof Error && error.name === "AbortError") { 254 255 return { 255 256 error: `Abort error: request takes more then ${formatDuration( 256 - ABORT_TIMEOUT, 257 + ABORT_TIMEOUT 257 258 )}.`, 258 259 }; 259 260 } ··· 312 313 </Badge> 313 314 ) : null} 314 315 </TabsTrigger> 316 + <TabsTrigger 317 + value="otel" 318 + disabled={process.env.NODE_ENV === "production"} 319 + > 320 + OTel{" "} 321 + <Badge variant="secondary" className="ml-1"> 322 + Soon 323 + </Badge> 324 + </TabsTrigger> 315 325 {defaultValues?.id ? ( 316 326 <TabsTrigger value="danger">Danger</TabsTrigger> 317 327 ) : null} ··· 324 334 </TabsContent> 325 335 <TabsContent value="scheduling"> 326 336 <SectionScheduling {...{ form, limits, plan }} /> 337 + </TabsContent> 338 + <TabsContent value="otel"> 339 + <SectionOtel {...{ form, limits }} /> 327 340 </TabsContent> 328 341 <TabsContent value="notifications"> 329 342 <SectionNotifications {...{ form, notifications }} />
+121
apps/web/src/components/forms/monitor/section-otel.tsx
··· 1 + "use client"; 2 + 3 + import { X } from "lucide-react"; 4 + import { useFieldArray, type UseFormReturn } from "react-hook-form"; 5 + 6 + import type { InsertMonitor } from "@openstatus/db/src/schema"; 7 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 8 + 9 + import { 10 + Button, 11 + FormControl, 12 + FormDescription, 13 + FormField, 14 + FormItem, 15 + FormLabel, 16 + FormMessage, 17 + Input, 18 + } from "@openstatus/ui"; 19 + 20 + import { SectionHeader } from "../shared/section-header"; 21 + import { ProFeatureAlert } from "@/components/billing/pro-feature-alert"; 22 + import { ComingSoonBanner } from "@/components/banner/coming-soon-banner"; 23 + 24 + interface Props { 25 + form: UseFormReturn<InsertMonitor>; 26 + limits: Limits; 27 + } 28 + 29 + export function SectionOtel({ form, limits }: Props) { 30 + if (process.env.NODE_ENV === "production") { 31 + return <ComingSoonBanner />; 32 + } 33 + 34 + const { fields, append, prepend, remove, update } = useFieldArray({ 35 + // name: "otelHeaders", 36 + name: "headers", 37 + control: form.control, 38 + }); 39 + 40 + if (!limits.otel) { 41 + return <ProFeatureAlert feature="OpenTelemetry" workspacePlan="team" />; 42 + } 43 + 44 + return ( 45 + <div className="grid w-full gap-4"> 46 + <SectionHeader 47 + title="OpenTelemetry" 48 + description="Configure your OpenTelemetry endpoint to send metrics to." 49 + /> 50 + <div className="grid sm:grid-cols-2 md:grid-cols-3"> 51 + <FormField 52 + control={form.control} 53 + // name="otelEndpoint" 54 + name="url" 55 + render={({ field }) => ( 56 + <FormItem className="col-span-full"> 57 + <FormLabel>Endpoint</FormLabel> 58 + <FormControl> 59 + <Input 60 + type="text" 61 + placeholder="https://otel.openstatus.dev/api/v1/metrics" 62 + {...field} 63 + /> 64 + </FormControl> 65 + <FormDescription> 66 + The endpoint to send metrics to. 67 + </FormDescription> 68 + <FormMessage /> 69 + </FormItem> 70 + )} 71 + /> 72 + </div> 73 + <div className="space-y-2 sm:col-span-full"> 74 + <FormLabel>Request Header</FormLabel> 75 + {fields.map((field, index) => ( 76 + <div key={field.id} className="grid grid-cols-6 gap-4"> 77 + <FormField 78 + control={form.control} 79 + name={`headers.${index}.key`} 80 + render={({ field }) => ( 81 + <FormItem className="col-span-2"> 82 + <FormControl> 83 + <Input placeholder="key" {...field} /> 84 + </FormControl> 85 + </FormItem> 86 + )} 87 + /> 88 + <div className="col-span-4 flex items-center space-x-2"> 89 + <FormField 90 + control={form.control} 91 + name={`headers.${index}.value`} 92 + render={({ field }) => ( 93 + <FormItem className="w-full"> 94 + <FormControl> 95 + <Input placeholder="value" {...field} /> 96 + </FormControl> 97 + </FormItem> 98 + )} 99 + /> 100 + <Button 101 + size="icon" 102 + variant="ghost" 103 + type="button" 104 + onClick={() => remove(index)} 105 + > 106 + <X className="h-4 w-4" /> 107 + </Button> 108 + </div> 109 + </div> 110 + ))} 111 + <Button 112 + type="button" 113 + variant="outline" 114 + onClick={() => append({ key: "", value: "" })} 115 + > 116 + Add Custom Header 117 + </Button> 118 + </div> 119 + </div> 120 + ); 121 + }
+4
packages/db/src/schema/plan/config.ts
··· 28 28 maintenance: true, 29 29 "monitor-values-visibility": true, 30 30 screenshots: false, 31 + otel: false, 31 32 "status-subscribers": false, 32 33 "custom-domain": false, 33 34 "password-protection": false, ··· 59 60 maintenance: true, 60 61 "monitor-values-visibility": true, 61 62 screenshots: true, 63 + otel: false, 62 64 "status-subscribers": true, 63 65 "custom-domain": true, 64 66 "password-protection": true, ··· 126 128 maintenance: true, 127 129 "monitor-values-visibility": true, 128 130 screenshots: true, 131 + otel: true, 129 132 "status-subscribers": true, 130 133 "custom-domain": true, 131 134 "password-protection": true, ··· 193 196 maintenance: true, 194 197 "monitor-values-visibility": true, 195 198 screenshots: true, 199 + otel: true, 196 200 "status-subscribers": true, 197 201 "custom-domain": true, 198 202 "password-protection": true,
+1
packages/db/src/schema/plan/schema.ts
··· 22 22 .default(["ams", "gru", "iad", "jnb", "hkg", "syd"]), 23 23 "private-locations": z.boolean().default(false), 24 24 screenshots: z.boolean().default(false), 25 + otel: z.boolean().default(false), 25 26 /** 26 27 * Status page limits 27 28 */