Skip to content

Commit d9f64af

Browse files
committed
New admin page, lots of improvements to make it more robust and testable
1 parent 4cb71e3 commit d9f64af

8 files changed

Lines changed: 614 additions & 379 deletions

apps/webapp/app/routes/admin.data-stores.tsx

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import {
2626
} from "~/components/primitives/Table";
2727
import { prisma } from "~/db.server";
2828
import { requireUser } from "~/services/session.server";
29-
import { getSecretStore } from "~/services/secrets/secretStore.server";
3029
import { ClickhouseConnectionSchema } from "~/services/clickhouse/clickhouseSecretSchemas.server";
30+
import { organizationDataStoresRegistry } from "~/services/dataStores/organizationDataStoresRegistryInstance.server";
31+
import { tryCatch } from "@trigger.dev/core";
3132

3233
// ---------------------------------------------------------------------------
3334
// Loader
@@ -55,79 +56,106 @@ const AddSchema = z.object({
5556
connectionUrl: z.string().url(),
5657
});
5758

59+
const UpdateSchema = z.object({
60+
_action: z.literal("update"),
61+
key: z.string().min(1),
62+
organizationIds: z.string().min(1),
63+
connectionUrl: z.string().url().optional(),
64+
});
65+
5866
const DeleteSchema = z.object({
5967
_action: z.literal("delete"),
60-
id: z.string().min(1),
68+
key: z.string().min(1),
6169
});
6270

71+
const FormSchema = z.discriminatedUnion("_action", [AddSchema, UpdateSchema, DeleteSchema]);
72+
6373
export async function action({ request }: ActionFunctionArgs) {
6474
const user = await requireUser(request);
6575
if (!user.admin) throw redirect("/");
6676

6777
const formData = await request.formData();
68-
const _action = formData.get("_action");
69-
70-
if (_action === "add") {
71-
const result = AddSchema.safeParse(Object.fromEntries(formData));
72-
if (!result.success) {
73-
return typedjson(
74-
{ error: result.error.issues.map((i) => i.message).join(", ") },
75-
{ status: 400 }
76-
);
77-
}
7878

79-
const { key, organizationIds: rawOrgIds, connectionUrl } = result.data;
80-
const organizationIds = rawOrgIds
81-
.split(",")
82-
.map((s) => s.trim())
83-
.filter(Boolean);
79+
const result = FormSchema.safeParse(Object.fromEntries(formData));
8480

85-
const secretKey = `data-store:${key}:clickhouse`;
81+
if (!result.success) {
82+
return typedjson(
83+
{ error: result.error.issues.map((i) => i.message).join(", ") },
84+
{ status: 400 }
85+
);
86+
}
8687

87-
const secretStore = getSecretStore("DATABASE");
88-
await secretStore.setSecret(secretKey, ClickhouseConnectionSchema.parse({ url: connectionUrl }));
88+
switch (result.data._action) {
89+
case "add": {
90+
const { key, organizationIds: rawOrgIds, connectionUrl } = result.data;
91+
const organizationIds = rawOrgIds
92+
.split(",")
93+
.map((s) => s.trim())
94+
.filter(Boolean);
95+
96+
const config = ClickhouseConnectionSchema.parse({ url: connectionUrl });
97+
98+
const [error, _] = await tryCatch(
99+
organizationDataStoresRegistry.addDataStore({
100+
key,
101+
kind: "CLICKHOUSE",
102+
organizationIds,
103+
config,
104+
})
105+
);
89106

90-
await prisma.organizationDataStore.create({
91-
data: {
92-
key,
93-
organizationIds,
94-
kind: "CLICKHOUSE",
95-
config: { version: 1, data: { secretKey } },
96-
},
97-
});
107+
if (error) {
108+
return typedjson({ error: error.message }, { status: 400 });
109+
}
98110

111+
return typedjson({ success: true });
112+
}
113+
case "update": {
114+
const { key, organizationIds: rawOrgIds, connectionUrl } = result.data;
115+
const organizationIds = rawOrgIds
116+
.split(",")
117+
.map((s) => s.trim())
118+
.filter(Boolean);
119+
120+
const config = connectionUrl
121+
? ClickhouseConnectionSchema.parse({ url: connectionUrl })
122+
: undefined;
123+
124+
const [error, _] = await tryCatch(
125+
organizationDataStoresRegistry.updateDataStore({
126+
key,
127+
kind: "CLICKHOUSE",
128+
organizationIds,
129+
config,
130+
})
131+
);
99132

100-
return typedjson({ success: true });
101-
}
133+
if (error) {
134+
return typedjson({ error: error.message }, { status: 400 });
135+
}
102136

103-
if (_action === "delete") {
104-
const result = DeleteSchema.safeParse(Object.fromEntries(formData));
105-
if (!result.success) {
106-
return typedjson({ error: "Invalid request" }, { status: 400 });
137+
return typedjson({ success: true });
107138
}
139+
case "delete": {
140+
const { key } = result.data;
141+
142+
const [error, _] = await tryCatch(
143+
organizationDataStoresRegistry.deleteDataStore({
144+
key,
145+
kind: "CLICKHOUSE",
146+
})
147+
);
108148

109-
const { id } = result.data;
149+
if (error) {
150+
return typedjson({ error: error.message }, { status: 400 });
151+
}
110152

111-
const dataStore = await prisma.organizationDataStore.findFirst({ where: { id } });
112-
if (!dataStore) {
113-
return typedjson({ error: "Data store not found" }, { status: 404 });
153+
return typedjson({ success: true });
114154
}
115-
116-
// Delete secret if config references one
117-
const config = dataStore.config as any;
118-
if (config?.data?.secretKey) {
119-
const secretStore = getSecretStore("DATABASE");
120-
await secretStore.deleteSecret(config.data.secretKey).catch(() => {
121-
// Secret may not exist — proceed with deletion
122-
});
155+
default: {
156+
return typedjson({ error: "Unknown action" }, { status: 400 });
123157
}
124-
125-
await prisma.organizationDataStore.delete({ where: { id } });
126-
127-
return typedjson({ success: true });
128158
}
129-
130-
return typedjson({ error: "Unknown action" }, { status: 400 });
131159
}
132160

133161
// ---------------------------------------------------------------------------
@@ -207,7 +235,7 @@ export default function AdminDataStoresRoute() {
207235
</span>
208236
</TableCell>
209237
<TableCell isSticky>
210-
<DeleteButton id={ds.id} name={ds.key} />
238+
<DeleteButton name={ds.key} />
211239
</TableCell>
212240
</TableRow>
213241
))
@@ -225,7 +253,7 @@ export default function AdminDataStoresRoute() {
225253
// Delete button with popover confirmation
226254
// ---------------------------------------------------------------------------
227255

228-
function DeleteButton({ id, name }: { id: string; name: string }) {
256+
function DeleteButton({ name }: { name: string }) {
229257
const [open, setOpen] = useState(false);
230258
const fetcher = useFetcher<{ success?: boolean; error?: string }>();
231259
const isDeleting = fetcher.state !== "idle";
@@ -251,7 +279,7 @@ function DeleteButton({ id, name }: { id: string; name: string }) {
251279
</Button>
252280
<fetcher.Form method="post" onSubmit={() => setOpen(false)}>
253281
<input type="hidden" name="_action" value="delete" />
254-
<input type="hidden" name="id" value={id} />
282+
<input type="hidden" name="key" value={name} />
255283
<Button type="submit" variant="danger/small">
256284
Confirm delete
257285
</Button>
@@ -311,7 +339,13 @@ function AddDataStoreDialog({
311339
<label className="text-xs font-medium text-text-dimmed">
312340
Kind <span className="text-rose-400">*</span>
313341
</label>
314-
<Input name="kind" value="CLICKHOUSE" readOnly variant="medium" className="opacity-60" />
342+
<Input
343+
name="kind"
344+
value="CLICKHOUSE"
345+
readOnly
346+
variant="medium"
347+
className="opacity-60"
348+
/>
315349
</div>
316350

317351
<div className="space-y-1.5">
@@ -344,9 +378,7 @@ function AddDataStoreDialog({
344378
</p>
345379
</div>
346380

347-
{fetcher.data?.error && (
348-
<p className="text-xs text-rose-400">{fetcher.data.error}</p>
349-
)}
381+
{fetcher.data?.error && <p className="text-xs text-rose-400">{fetcher.data.error}</p>}
350382

351383
<DialogFooter>
352384
<Button

0 commit comments

Comments
 (0)