Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
96 changes: 73 additions & 23 deletions apps/dashboard/src/components/forms/components/form-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@/components/forms/form-card";
import { useTRPC } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { StatuspageIcon } from "@openstatus/icons";
import { InstatusIcon, StatuspageIcon } from "@openstatus/icons";
import type { ImportSummary } from "@openstatus/importers/types";
import { Badge } from "@openstatus/ui/components/ui/badge";
import { Button } from "@openstatus/ui/components/ui/button";
Expand Down Expand Up @@ -40,9 +40,10 @@ import { toast } from "sonner";
import { z } from "zod";

const schema = z.object({
provider: z.enum(["statuspage"]),
provider: z.enum(["statuspage", "instatus"]),
apiKey: z.string().min(1, "API key is required"),
statuspagePageId: z.string().optional(),
instatusPageId: z.string().optional(),
includeStatusReports: z.boolean(),
includeSubscribers: z.boolean(),
includeComponents: z.boolean(),
Expand Down Expand Up @@ -75,6 +76,7 @@ export function FormImport({
provider: undefined,
apiKey: "",
statuspagePageId: "",
instatusPageId: "",
includeStatusReports: true,
includeSubscribers: false,
includeComponents: true,
Expand All @@ -85,6 +87,7 @@ export function FormImport({
const watchProvider = form.watch("provider");
const watchApiKey = form.watch("apiKey");
const watchStatuspagePageId = form.watch("statuspagePageId");
const watchInstatusPageId = form.watch("instatusPageId");

const previewMutation = useMutation(
trpc.import.preview.mutationOptions({
Expand All @@ -105,9 +108,16 @@ export function FormImport({
return;
}
previewMutation.mutate({
provider: "statuspage",
provider: watchProvider,
apiKey: watchApiKey,
statuspagePageId: watchStatuspagePageId || undefined,
statuspagePageId:
watchProvider === "statuspage"
? watchStatuspagePageId || undefined
: undefined,
instatusPageId:
watchProvider === "instatus"
? watchInstatusPageId || undefined
: undefined,
pageId,
});
}
Expand Down Expand Up @@ -178,6 +188,21 @@ export function FormImport({
Atlassian Statuspage
</FormLabel>
</FormItem>
<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">
<FormControl>
<RadioGroupItem
value="instatus"
className="sr-only"
/>
</FormControl>
<InstatusIcon
className="size-4 shrink-0 text-foreground"
aria-hidden="true"
/>
<FormLabel className="cursor-pointer font-medium text-foreground text-xs leading-none after:absolute after:inset-0">
Instatus
</FormLabel>
</FormItem>
<div className="col-span-1 self-end text-muted-foreground text-xs sm:place-self-end">
Missing a provider?{" "}
<a href="mailto:ping@openstatus.dev">Contact us</a>
Expand All @@ -202,34 +227,59 @@ export function FormImport({
<FormControl>
<Input
type="password"
placeholder="OAuth API key"
placeholder={
watchProvider === "instatus"
? "Bearer API key"
: "OAuth API key"
}
{...field}
/>
</FormControl>
<FormMessage />
<FormDescription>
Your Statuspage API key. Found in your Statuspage
account under Manage Account &gt; API.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="statuspagePageId"
render={({ field }) => (
<FormItem>
<FormLabel>Page ID (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g. abc123def456" {...field} />
</FormControl>
<FormDescription>
Import a specific page. Leave empty to import across
pages.
{watchProvider === "instatus"
? "Your Instatus API key. Found in your Instatus account under Settings > API."
: "Your Statuspage API key. Found in your Statuspage account under Manage Account > API."}
</FormDescription>
</FormItem>
)}
/>
{watchProvider === "statuspage" ? (
<FormField
control={form.control}
name="statuspagePageId"
render={({ field }) => (
<FormItem>
<FormLabel>Page ID (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g. abc123def456" {...field} />
</FormControl>
<FormDescription>
Import a specific page. Leave empty to import across
pages.
</FormDescription>
</FormItem>
)}
/>
) : null}
{watchProvider === "instatus" ? (
<FormField
control={form.control}
name="instatusPageId"
render={({ field }) => (
<FormItem>
<FormLabel>Page ID (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g. clx1abc2def3" {...field} />
</FormControl>
<FormDescription>
Import a specific page. Leave empty to import all
pages.
</FormDescription>
</FormItem>
)}
/>
) : null}
<Button
type="button"
variant="secondary"
Expand Down
10 changes: 8 additions & 2 deletions packages/api/src/router/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ export const importRouter = createTRPCRouter({
preview: protectedProcedure
.input(
z.object({
provider: z.enum(["statuspage"]),
provider: z.enum(["statuspage", "instatus"]),
apiKey: z.string().min(1),
statuspagePageId: z.string().nullish(),
instatusPageId: z.string().nullish(),
pageId: z.number().optional(),
}),
)
.mutation(async (opts) => {
return previewImport({
provider: opts.input.provider,
apiKey: opts.input.apiKey,
statuspagePageId: opts.input.statuspagePageId ?? undefined,
instatusPageId: opts.input.instatusPageId ?? undefined,
workspaceId: opts.ctx.workspace.id,
pageId: opts.input.pageId,
limits: opts.ctx.workspace.limits,
Expand All @@ -28,10 +31,11 @@ export const importRouter = createTRPCRouter({
run: protectedProcedure
.input(
z.object({
provider: z.enum(["statuspage"]),
provider: z.enum(["statuspage", "instatus"]),
apiKey: z.string().min(1),
pageId: z.number().optional(),
statuspagePageId: z.string().nullish(),
instatusPageId: z.string().nullish(),
options: z
.object({
includeStatusReports: z.boolean().default(true),
Expand Down Expand Up @@ -64,8 +68,10 @@ export const importRouter = createTRPCRouter({
}

return runImport({
provider: opts.input.provider,
apiKey: opts.input.apiKey,
statuspagePageId: opts.input.statuspagePageId ?? undefined,
instatusPageId: opts.input.instatusPageId ?? undefined,
workspaceId: opts.ctx.workspace.id,
pageId: opts.input.pageId,
options: opts.input.options,
Expand Down
43 changes: 37 additions & 6 deletions packages/api/src/service/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
PhaseResult,
ResourceResult,
} from "@openstatus/importers";
import { createInstatusProvider } from "@openstatus/importers/instatus";
import { createStatuspageProvider } from "@openstatus/importers/statuspage";
import { TRPCError } from "@trpc/server";

Expand All @@ -26,6 +27,28 @@ type ImportOptions = {
includeComponents?: boolean;
};

type ProviderName = "statuspage" | "instatus";

function createProvider(name: ProviderName) {
return name === "instatus"
? createInstatusProvider()
: createStatuspageProvider();
}

function buildProviderConfig(config: {
provider: ProviderName;
apiKey: string;
workspaceId: number;
pageId?: number;
statuspagePageId?: string;
instatusPageId?: string;
}) {
const { provider, ...rest } = config;
return provider === "instatus"
? { ...rest, instatusPageId: config.instatusPageId }
: { ...rest, statuspagePageId: config.statuspagePageId };
}

/**
* Inspect an ImportSummary and push warnings into `summary.errors`
* for any limits that would be hit during import.
Expand Down Expand Up @@ -97,16 +120,19 @@ export async function addLimitWarnings(
}

export async function previewImport(config: {
provider: ProviderName;
apiKey: string;
statuspagePageId?: string;
instatusPageId?: string;
workspaceId: number;
pageId?: number;
limits: Limits;
}): Promise<ImportSummary> {
const provider = createStatuspageProvider();
const provider = createProvider(config.provider);
const providerConfig = buildProviderConfig(config);

const validation = await provider.validate({
...config,
...providerConfig,
dryRun: true,
});
if (!validation.valid) {
Expand All @@ -116,22 +142,25 @@ export async function previewImport(config: {
});
}

const summary = await provider.run({ ...config, dryRun: true });
const summary = await provider.run({ ...providerConfig, dryRun: true });
await addLimitWarnings(summary, config);
return summary;
}

export async function runImport(config: {
provider: ProviderName;
apiKey: string;
statuspagePageId?: string;
instatusPageId?: string;
workspaceId: number;
pageId?: number;
options?: ImportOptions;
limits: Limits;
}): Promise<ImportSummary> {
const provider = createStatuspageProvider();
const provider = createProvider(config.provider);
const providerConfig = buildProviderConfig(config);

const validation = await provider.validate(config);
const validation = await provider.validate(providerConfig);
if (!validation.valid) {
throw new TRPCError({
code: "BAD_REQUEST",
Expand All @@ -140,7 +169,7 @@ export async function runImport(config: {
}

// Fetch and map all data
const summary = await provider.run(config);
const summary = await provider.run(providerConfig);

// Add limit warnings (same as preview)
await addLimitWarnings(summary, config);
Expand Down Expand Up @@ -686,6 +715,7 @@ async function writeSubscribersPhase(
const data = resource.data as {
email: string;
pageId: number;
confirmed: boolean;
sourceComponentIds: string[];
};

Expand Down Expand Up @@ -714,6 +744,7 @@ async function writeSubscribersPhase(
email: data.email,
pageId,
channelType: "email",
acceptedAt: data.confirmed ? new Date() : undefined,
})
.returning({ id: pageSubscriber.id });

Expand Down
1 change: 1 addition & 0 deletions packages/icons/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from "./telegram";
export * from "./whatsapp";
export * from "./markdown";
export * from "./statuspage";
export * from "./instatus";
15 changes: 15 additions & 0 deletions packages/icons/src/instatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function InstatusIcon(props: React.ComponentProps<"svg">) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="currentColor"
{...props}
>
<title>Instatus</title>
<path d="m16.994 21.028c3.5843-1.91 5.471-5.759 5.0561-9.5637-1.3206 1.0851-2.6237 2.3203-3.8709 3.6906-2.0656 2.2694-3.7476 4.6559-4.9953 6.9817 1.2946-0.09715 2.5907-0.45868 3.8101-1.1086zm-13.394-2.5626c-1.3408 1.8191-2.3771 4.4991-1.3032 5.3066 1.5151 1.1394 8.404-2.0133 13.908-8.8051 5.504-6.7918 7.3265-13.796 4.879-14.873-1.1283-0.49644-3.486 1.083-4.8394 2.3943l0.58412 0.31415c1.332-0.85276 3.5528-1.7338 1.4995 1.9758-0.0097 0.01768-0.01962 0.03541-0.02949 0.05317-2.9067-2.2075-6.9471-2.662-10.379-0.8328-4.7026 2.506-6.4831 8.3499-3.9771 13.052 0.58979 1.1067 1.3644 2.0516 2.2655 2.8168-3.5586 2.7493-2.6905 0.35965-2.1925-0.8162z" />
</svg>
);
}
4 changes: 3 additions & 1 deletion packages/importers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
".": "./src/index.ts",
"./types": "./src/types.ts",
"./statuspage": "./src/providers/statuspage/index.ts",
"./statuspage/fixtures": "./src/providers/statuspage/fixtures.ts"
"./statuspage/fixtures": "./src/providers/statuspage/fixtures.ts",
"./instatus": "./src/providers/instatus/index.ts",
"./instatus/fixtures": "./src/providers/instatus/fixtures.ts"
},
"scripts": {
"test": "bun test",
Expand Down
5 changes: 4 additions & 1 deletion packages/importers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ export type {
export { createStatuspageProvider } from "./providers/statuspage";
export type { StatuspageImportConfig } from "./providers/statuspage";

export { createInstatusProvider } from "./providers/instatus";
export type { InstatusImportConfig } from "./providers/instatus";

/**
* Registry of all available import providers.
* Add new providers here as they are implemented.
*/
export const IMPORT_PROVIDERS = ["statuspage"] as const;
export const IMPORT_PROVIDERS = ["statuspage", "instatus"] as const;
export type ImportProviderName = (typeof IMPORT_PROVIDERS)[number];
Loading
Loading