Skip to content

Commit 1f3b54d

Browse files
aggmoulikmxkaske
andauthored
feat: Betterstack Importer (#2016)
* Updated BetterStack Importer Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> * Updated documentation Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> * Updated formatting code Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> * Fixed the code duplication Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> * Update according to the claude review Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> * fix: format * chore: minor improvements * fix: minor issue * fix: mapper * fix: periodicity clamp --------- Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com> Co-authored-by: Maximilian Kaske <maximilian@kaske.org>
1 parent aeb006c commit 1f3b54d

File tree

19 files changed

+2741
-56
lines changed

19 files changed

+2741
-56
lines changed

apps/dashboard/src/components/forms/components/form-import.tsx

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
} from "@/components/forms/form-card";
1313
import { useTRPC } from "@/lib/trpc/client";
1414
import { zodResolver } from "@hookform/resolvers/zod";
15-
import { InstatusIcon, StatuspageIcon } from "@openstatus/icons";
15+
import {
16+
BetterstackIcon,
17+
InstatusIcon,
18+
StatuspageIcon,
19+
} from "@openstatus/icons";
1620
import type { ImportSummary } from "@openstatus/importers/types";
1721
import { Badge } from "@openstatus/ui/components/ui/badge";
1822
import { Button } from "@openstatus/ui/components/ui/button";
@@ -40,10 +44,12 @@ import { toast } from "sonner";
4044
import { z } from "zod";
4145

4246
const schema = z.object({
43-
provider: z.enum(["statuspage", "instatus"]),
47+
provider: z.enum(["statuspage", "betterstack", "instatus"]),
4448
apiKey: z.string().min(1, "API key is required"),
4549
statuspagePageId: z.string().optional(),
50+
betterstackStatusPageId: z.string().optional(),
4651
instatusPageId: z.string().optional(),
52+
includeMonitors: z.boolean(),
4753
includeStatusReports: z.boolean(),
4854
includeSubscribers: z.boolean(),
4955
includeComponents: z.boolean(),
@@ -56,7 +62,10 @@ function getPhaseCount(preview: ImportSummary, phase: string): number {
5662
}
5763

5864
const PHASE_LABELS: Record<string, string> = {
65+
monitors: "Monitors",
5966
componentGroups: "Component Groups",
67+
monitorGroups: "Monitor Groups",
68+
sections: "Sections",
6069
components: "Components",
6170
incidents: "Status Reports",
6271
maintenances: "Maintenances",
@@ -76,7 +85,9 @@ export function FormImport({
7685
provider: undefined,
7786
apiKey: "",
7887
statuspagePageId: "",
88+
betterstackStatusPageId: "",
7989
instatusPageId: "",
90+
includeMonitors: true,
8091
includeStatusReports: true,
8192
includeSubscribers: false,
8293
includeComponents: true,
@@ -87,6 +98,7 @@ export function FormImport({
8798
const watchProvider = form.watch("provider");
8899
const watchApiKey = form.watch("apiKey");
89100
const watchStatuspagePageId = form.watch("statuspagePageId");
101+
const watchBetterstackStatusPageId = form.watch("betterstackStatusPageId");
90102
const watchInstatusPageId = form.watch("instatusPageId");
91103

92104
const previewMutation = useMutation(
@@ -114,6 +126,10 @@ export function FormImport({
114126
watchProvider === "statuspage"
115127
? watchStatuspagePageId || undefined
116128
: undefined,
129+
betterstackStatusPageId:
130+
watchProvider === "betterstack"
131+
? watchBetterstackStatusPageId || undefined
132+
: undefined,
117133
instatusPageId:
118134
watchProvider === "instatus"
119135
? watchInstatusPageId || undefined
@@ -188,6 +204,21 @@ export function FormImport({
188204
Atlassian Statuspage
189205
</FormLabel>
190206
</FormItem>
207+
<FormItem className="relative flex cursor-pointer flex-row items-center gap-3 rounded-md border border-input px-2 py-3 text-center shadow-xs outline-none transition-[color,box-shadow] has-data-[state=checked]:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50">
208+
<FormControl>
209+
<RadioGroupItem
210+
value="betterstack"
211+
className="sr-only"
212+
/>
213+
</FormControl>
214+
<BetterstackIcon
215+
className="size-4 shrink-0 text-foreground"
216+
aria-hidden="true"
217+
/>
218+
<FormLabel className="cursor-pointer font-medium text-foreground text-xs leading-none after:absolute after:inset-0">
219+
Better Stack
220+
</FormLabel>
221+
</FormItem>
191222
<FormItem className="relative flex cursor-pointer flex-row items-center gap-3 rounded-md border border-input px-2 py-3 text-center shadow-xs outline-none transition-[color,box-shadow] has-data-[state=checked]:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50">
192223
<FormControl>
193224
<RadioGroupItem
@@ -228,18 +259,22 @@ export function FormImport({
228259
<Input
229260
type="password"
230261
placeholder={
231-
watchProvider === "instatus"
232-
? "Bearer API key"
233-
: "OAuth API key"
262+
watchProvider === "betterstack"
263+
? "Bearer token"
264+
: watchProvider === "instatus"
265+
? "Bearer API key"
266+
: "OAuth API key"
234267
}
235268
{...field}
236269
/>
237270
</FormControl>
238271
<FormMessage />
239272
<FormDescription>
240-
{watchProvider === "instatus"
241-
? "Your Instatus API key. Found in your Instatus account under Settings > API."
242-
: "Your Statuspage API key. Found in your Statuspage account under Manage Account > API."}
273+
{watchProvider === "betterstack"
274+
? "Your Better Stack API token. Found in Better Stack \u2192 API tokens."
275+
: watchProvider === "instatus"
276+
? "Your Instatus API key. Found in your Instatus account under Settings > API."
277+
: "Your Statuspage API key. Found in your Statuspage account under Manage Account > API."}
243278
</FormDescription>
244279
</FormItem>
245280
)}
@@ -262,6 +297,24 @@ export function FormImport({
262297
)}
263298
/>
264299
) : null}
300+
{watchProvider === "betterstack" ? (
301+
<FormField
302+
control={form.control}
303+
name="betterstackStatusPageId"
304+
render={({ field }) => (
305+
<FormItem>
306+
<FormLabel>Status Page ID (optional)</FormLabel>
307+
<FormControl>
308+
<Input placeholder="e.g. 123456789" {...field} />
309+
</FormControl>
310+
<FormDescription>
311+
Import a specific status page. Leave empty to use the
312+
first available.
313+
</FormDescription>
314+
</FormItem>
315+
)}
316+
/>
317+
) : null}
265318
{watchProvider === "instatus" ? (
266319
<FormField
267320
control={form.control}
@@ -319,6 +372,29 @@ export function FormImport({
319372
</p>
320373
</Note>
321374
) : null}
375+
{watchProvider === "betterstack" ? (
376+
<FormField
377+
control={form.control}
378+
name="includeMonitors"
379+
render={({ field }) => (
380+
<FormItem className="flex flex-row items-center justify-between">
381+
<div className="space-y-0.5">
382+
<FormLabel>Monitors</FormLabel>
383+
<FormDescription>
384+
Import monitors with their URL, frequency, and
385+
regions.
386+
</FormDescription>
387+
</div>
388+
<FormControl>
389+
<Switch
390+
checked={field.value}
391+
onCheckedChange={field.onChange}
392+
/>
393+
</FormControl>
394+
</FormItem>
395+
)}
396+
/>
397+
) : null}
322398
<FormField
323399
control={form.control}
324400
name="includeStatusReports"
@@ -360,26 +436,28 @@ export function FormImport({
360436
</FormItem>
361437
)}
362438
/>
363-
<FormField
364-
control={form.control}
365-
name="includeSubscribers"
366-
render={({ field }) => (
367-
<FormItem className="flex flex-row items-center justify-between">
368-
<div className="space-y-0.5">
369-
<FormLabel>Subscribers</FormLabel>
370-
<FormDescription>
371-
Import email subscribers.
372-
</FormDescription>
373-
</div>
374-
<FormControl>
375-
<Switch
376-
checked={field.value}
377-
onCheckedChange={field.onChange}
378-
/>
379-
</FormControl>
380-
</FormItem>
381-
)}
382-
/>
439+
{watchProvider !== "betterstack" ? (
440+
<FormField
441+
control={form.control}
442+
name="includeSubscribers"
443+
render={({ field }) => (
444+
<FormItem className="flex flex-row items-center justify-between">
445+
<div className="space-y-0.5">
446+
<FormLabel>Subscribers</FormLabel>
447+
<FormDescription>
448+
Import email subscribers.
449+
</FormDescription>
450+
</div>
451+
<FormControl>
452+
<Switch
453+
checked={field.value}
454+
onCheckedChange={field.onChange}
455+
/>
456+
</FormControl>
457+
</FormItem>
458+
)}
459+
/>
460+
) : null}
383461
</FormCardContent>
384462
</>
385463
) : null}

apps/dashboard/src/components/forms/components/update.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export function FormComponentsUpdate() {
1919
const { data: pageComponents, refetch: refetchComponents } = useQuery(
2020
trpc.pageComponent.list.queryOptions({ pageId: Number.parseInt(id) }),
2121
);
22-
const { data: monitors } = useQuery(trpc.monitor.list.queryOptions());
22+
const { data: monitors, refetch: refetchMonitors } = useQuery(
23+
trpc.monitor.list.queryOptions(),
24+
);
2325
const { data: workspace } = useQuery(trpc.workspace.get.queryOptions());
2426

2527
const updateComponentsMutation = useMutation(
@@ -40,7 +42,7 @@ export function FormComponentsUpdate() {
4042
const importMutation = useMutation(
4143
trpc.import.run.mutationOptions({
4244
onSuccess: async () => {
43-
await Promise.all([refetch(), refetchComponents()]);
45+
await Promise.all([refetch(), refetchComponents(), refetchMonitors()]);
4446
setFormKey((k) => k + 1);
4547
},
4648
}),
@@ -137,10 +139,14 @@ export function FormComponentsUpdate() {
137139
apiKey: values.apiKey,
138140
pageId: statusPage.id,
139141
statuspagePageId: values.statuspagePageId ?? undefined,
142+
betterstackStatusPageId:
143+
values.betterstackStatusPageId ?? undefined,
144+
instatusPageId: values.instatusPageId ?? undefined,
140145
options: {
141146
includeStatusReports: values.includeStatusReports,
142147
includeSubscribers: values.includeSubscribers,
143148
includeComponents: values.includeComponents,
149+
includeMonitors: values.includeMonitors,
144150
},
145151
});
146152
}}

packages/api/src/router/import.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ export const importRouter = createTRPCRouter({
99
preview: protectedProcedure
1010
.input(
1111
z.object({
12-
provider: z.enum(["statuspage", "instatus"]),
12+
provider: z.enum(["statuspage", "betterstack", "instatus"]),
1313
apiKey: z.string().min(1),
1414
statuspagePageId: z.string().nullish(),
15+
betterstackStatusPageId: z.string().nullish(),
1516
instatusPageId: z.string().nullish(),
1617
pageId: z.number().optional(),
1718
}),
@@ -21,6 +22,8 @@ export const importRouter = createTRPCRouter({
2122
provider: opts.input.provider,
2223
apiKey: opts.input.apiKey,
2324
statuspagePageId: opts.input.statuspagePageId ?? undefined,
25+
betterstackStatusPageId:
26+
opts.input.betterstackStatusPageId ?? undefined,
2427
instatusPageId: opts.input.instatusPageId ?? undefined,
2528
workspaceId: opts.ctx.workspace.id,
2629
pageId: opts.input.pageId,
@@ -31,16 +34,18 @@ export const importRouter = createTRPCRouter({
3134
run: protectedProcedure
3235
.input(
3336
z.object({
34-
provider: z.enum(["statuspage", "instatus"]),
37+
provider: z.enum(["statuspage", "betterstack", "instatus"]),
3538
apiKey: z.string().min(1),
3639
pageId: z.number().optional(),
3740
statuspagePageId: z.string().nullish(),
41+
betterstackStatusPageId: z.string().nullish(),
3842
instatusPageId: z.string().nullish(),
3943
options: z
4044
.object({
4145
includeStatusReports: z.boolean().default(true),
4246
includeSubscribers: z.boolean().default(false),
4347
includeComponents: z.boolean().default(true),
48+
includeMonitors: z.boolean().default(true),
4449
})
4550
.optional(),
4651
}),
@@ -71,6 +76,8 @@ export const importRouter = createTRPCRouter({
7176
provider: opts.input.provider,
7277
apiKey: opts.input.apiKey,
7378
statuspagePageId: opts.input.statuspagePageId ?? undefined,
79+
betterstackStatusPageId:
80+
opts.input.betterstackStatusPageId ?? undefined,
7481
instatusPageId: opts.input.instatusPageId ?? undefined,
7582
workspaceId: opts.ctx.workspace.id,
7683
pageId: opts.input.pageId,

packages/api/src/service/import.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
22
import { allPlans } from "@openstatus/db/src/schema/plan/config";
33
import type { Limits } from "@openstatus/db/src/schema/plan/schema";
44
import type { ImportSummary } from "@openstatus/importers";
5-
import { addLimitWarnings } from "./import";
5+
import { addLimitWarnings, clampPeriodicity } from "./import";
66

77
function makeSummary(overrides?: Partial<ImportSummary>): ImportSummary {
88
return {
@@ -361,3 +361,48 @@ describe("addLimitWarnings", () => {
361361
expect(summary.errors).toEqual([]);
362362
});
363363
});
364+
365+
describe("clampPeriodicity", () => {
366+
const freePlan = allPlans.free.limits.periodicity as string[];
367+
const starterPlan = allPlans.starter.limits.periodicity as string[];
368+
369+
describe("free plan", () => {
370+
test("allows 10m (already in plan)", () => {
371+
expect(clampPeriodicity("10m", freePlan)).toBe("10m");
372+
});
373+
374+
test("allows 30m (already in plan)", () => {
375+
expect(clampPeriodicity("30m", freePlan)).toBe("30m");
376+
});
377+
378+
test("allows 1h (already in plan)", () => {
379+
expect(clampPeriodicity("1h", freePlan)).toBe("1h");
380+
});
381+
382+
test("clamps 1m to 10m", () => {
383+
expect(clampPeriodicity("1m", freePlan)).toBe("10m");
384+
});
385+
386+
test("clamps 30s to 10m", () => {
387+
expect(clampPeriodicity("30s", freePlan)).toBe("10m");
388+
});
389+
390+
test("clamps 5m to 10m", () => {
391+
expect(clampPeriodicity("5m", freePlan)).toBe("10m");
392+
});
393+
});
394+
395+
describe("starter plan", () => {
396+
test("allows 1m (in plan)", () => {
397+
expect(clampPeriodicity("1m", starterPlan)).toBe("1m");
398+
});
399+
400+
test("allows 5m (in plan)", () => {
401+
expect(clampPeriodicity("5m", starterPlan)).toBe("5m");
402+
});
403+
404+
test("clamps 30s to 1m", () => {
405+
expect(clampPeriodicity("30s", starterPlan)).toBe("1m");
406+
});
407+
});
408+
});

0 commit comments

Comments
 (0)