import { useState } from "react"; import { useFetcher } from "@remix-run/react"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime"; import { redirect } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { Button } from "~/components/primitives/Buttons"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "~/components/primitives/Dialog"; import { Input } from "~/components/primitives/Input"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; import { Table, TableBlankRow, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; import { prisma } from "~/db.server"; import { requireUser } from "~/services/session.server"; import { ClickhouseConnectionSchema } from "~/services/clickhouse/clickhouseSecretSchemas.server"; import { organizationDataStoresRegistry } from "~/services/dataStores/organizationDataStoresRegistryInstance.server"; import { tryCatch } from "@trigger.dev/core/utils"; // --------------------------------------------------------------------------- // Loader // --------------------------------------------------------------------------- export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireUser(request); if (!user.admin) throw redirect("/"); const dataStores = await prisma.organizationDataStore.findMany({ orderBy: { createdAt: "desc" }, }); return typedjson({ dataStores }); }; // --------------------------------------------------------------------------- // Action // --------------------------------------------------------------------------- const AddSchema = z.object({ _action: z.literal("add"), key: z.string().min(1), organizationIds: z.string().min(1), connectionUrl: z.string().url(), }); const UpdateSchema = z.object({ _action: z.literal("update"), key: z.string().min(1), organizationIds: z.string().min(1), connectionUrl: z.string().url().optional(), }); const DeleteSchema = z.object({ _action: z.literal("delete"), key: z.string().min(1), }); const FormSchema = z.discriminatedUnion("_action", [AddSchema, UpdateSchema, DeleteSchema]); export async function action({ request }: ActionFunctionArgs) { const user = await requireUser(request); if (!user.admin) throw redirect("/"); const formData = await request.formData(); const result = FormSchema.safeParse(Object.fromEntries(formData)); if (!result.success) { return typedjson( { error: result.error.issues.map((i) => i.message).join(", ") }, { status: 400 } ); } switch (result.data._action) { case "add": { const { key, organizationIds: rawOrgIds, connectionUrl } = result.data; const organizationIds = rawOrgIds .split(",") .map((s) => s.trim()) .filter(Boolean); const parsedConfig = ClickhouseConnectionSchema.safeParse({ url: connectionUrl }); if (!parsedConfig.success) { return typedjson( { error: parsedConfig.error.issues.map((i) => i.message).join(", ") }, { status: 400 } ); } const [error, _] = await tryCatch( organizationDataStoresRegistry.addDataStore({ key, kind: "CLICKHOUSE", organizationIds, config: parsedConfig.data, }) ); if (error) { return typedjson({ error: error.message }, { status: 400 }); } return typedjson({ success: true }); } case "update": { const { key, organizationIds: rawOrgIds, connectionUrl } = result.data; const organizationIds = rawOrgIds .split(",") .map((s) => s.trim()) .filter(Boolean); let config: ReturnType | undefined; if (connectionUrl) { const parsedConfig = ClickhouseConnectionSchema.safeParse({ url: connectionUrl }); if (!parsedConfig.success) { return typedjson( { error: parsedConfig.error.issues.map((i) => i.message).join(", ") }, { status: 400 } ); } config = parsedConfig.data; } const [error, _] = await tryCatch( organizationDataStoresRegistry.updateDataStore({ key, kind: "CLICKHOUSE", organizationIds, config, }) ); if (error) { return typedjson({ error: error.message }, { status: 400 }); } return typedjson({ success: true }); } case "delete": { const { key } = result.data; const [error, _] = await tryCatch( organizationDataStoresRegistry.deleteDataStore({ key, kind: "CLICKHOUSE", }) ); if (error) { return typedjson({ error: error.message }, { status: 400 }); } return typedjson({ success: true }); } default: { return typedjson({ error: "Unknown action" }, { status: 400 }); } } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function AdminDataStoresRoute() { const { dataStores } = useTypedLoaderData(); const [addOpen, setAddOpen] = useState(false); return (
{dataStores.length} data store{dataStores.length !== 1 ? "s" : ""}
Key Kind Organizations Created Updated Actions {dataStores.length === 0 ? ( No data stores configured ) : ( dataStores.map((ds) => ( {ds.key} {ds.kind} {ds.organizationIds.length} org{ds.organizationIds.length !== 1 ? "s" : ""} {ds.organizationIds.length > 0 && ( ({ds.organizationIds.slice(0, 2).join(", ")} {ds.organizationIds.length > 2 ? ` +${ds.organizationIds.length - 2} more` : ""} ) )} {new Date(ds.createdAt).toLocaleString()} {new Date(ds.updatedAt).toLocaleString()}
)) )}
); } // --------------------------------------------------------------------------- // Delete button with popover confirmation // --------------------------------------------------------------------------- function DeleteButton({ name }: { name: string }) { const [open, setOpen] = useState(false); const fetcher = useFetcher<{ success?: boolean; error?: string }>(); const isDeleting = fetcher.state !== "idle"; return ( Delete {name}? This will remove the data store and its secret. Organizations using it will fall back to the default ClickHouse instance.
setOpen(false)}>
); } // --------------------------------------------------------------------------- // Edit button with dialog // --------------------------------------------------------------------------- function EditButton({ name, organizationIds }: { name: string; organizationIds: string[] }) { const [open, setOpen] = useState(false); const fetcher = useFetcher<{ success?: boolean; error?: string }>(); const isSubmitting = fetcher.state !== "idle"; if (fetcher.data?.success && open) { setOpen(false); } return ( <> Edit data store

Comma-separated organization IDs.

{fetcher.data?.error &&

{fetcher.data.error}

}
); } // --------------------------------------------------------------------------- // Add data store dialog // --------------------------------------------------------------------------- function AddDataStoreDialog({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { const fetcher = useFetcher<{ success?: boolean; error?: string }>(); const isSubmitting = fetcher.state !== "idle"; // Close dialog on success if (fetcher.data?.success && open) { onOpenChange(false); } return ( Add data store

Unique identifier for this data store. Used as the secret key prefix.

Comma-separated organization IDs.

Stored encrypted in SecretStore. Never logged or displayed again.

{fetcher.data?.error &&

{fetcher.data.error}

}
); }