Skip to content

Commit f77f75a

Browse files
authored
Merge pull request #7 from deploy-cat/feature/cnpg
Implement Managed Postgres
2 parents 9b75bc8 + e13d030 commit f77f75a

File tree

17 files changed

+1071
-80
lines changed

17 files changed

+1071
-80
lines changed

src/components/EnvVarsInput.tsx

Lines changed: 200 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
1-
import { For, type Signal, createSignal } from "solid-js";
1+
import { For, Show, type Signal, createEffect, createSignal } from "solid-js";
22
import { TrashIcon } from "@deploy-cat/heroicons-solid/24/solid/esm";
3+
import { cache, createAsync } from "@solidjs/router";
4+
import { getUser } from "~/lib/auth";
5+
import { k8sCore } from "~/lib/k8s";
6+
import { CircleStackIcon } from "@deploy-cat/heroicons-solid/24/solid/esm";
7+
8+
const getDatabseSecrets = cache(async () => {
9+
"use server";
10+
try {
11+
const user = await getUser();
12+
const secrets = await k8sCore.listNamespacedSecret(
13+
user.name,
14+
undefined,
15+
undefined,
16+
undefined,
17+
undefined,
18+
"cnpg.io/cluster"
19+
);
20+
return secrets.body.items.map(({ data, metadata: { name, labels } }) => ({
21+
name,
22+
labels,
23+
values: Object.keys(data),
24+
}));
25+
} catch (e) {
26+
console.error(e);
27+
}
28+
}, "db-secrets");
329

430
const parseClipboard = (text: string) =>
531
text
@@ -14,13 +40,46 @@ const parseClipboard = (text: string) =>
1440
});
1541

1642
export const EnvVarsInput = ({ data }) => {
43+
const dbSecrets = createAsync(() => getDatabseSecrets());
44+
1745
const [env, setEnv] = createSignal(
1846
(data &&
19-
Object.entries(data).map(([key, value]) => ({
20-
key: createSignal(key),
21-
value: createSignal(value),
22-
}))) ??
23-
([] as Array<{ key: Signal<string>; value: Signal<string> }>)
47+
Object.entries(data).map(([k, v]) => {
48+
if (typeof v == "string") {
49+
return {
50+
key: createSignal(k),
51+
value: createSignal(v),
52+
};
53+
}
54+
55+
const value = createSignal({
56+
secretKeyRef: {
57+
name: v.valueFrom.secretKeyRef.name,
58+
key: v.valueFrom.secretKeyRef.key,
59+
},
60+
} as any);
61+
const secretName = createSignal(v.valueFrom.secretKeyRef.name);
62+
const secretKey = createSignal(v.valueFrom.secretKeyRef.key);
63+
64+
createEffect(() => {
65+
value[1]({
66+
valueFrom: {
67+
secretKeyRef: {
68+
name: secretName[0](),
69+
key: secretKey[0](),
70+
},
71+
},
72+
});
73+
});
74+
return {
75+
key: createSignal(k),
76+
value,
77+
type: "secret",
78+
secretName,
79+
secretKey,
80+
};
81+
})) ??
82+
([] as Array<{ key: Signal<string>; value: Signal<string | any> }>)
2483
);
2584

2685
return (
@@ -53,32 +112,95 @@ export const EnvVarsInput = ({ data }) => {
53112
</div>
54113
}
55114
>
56-
{({ key, value }, index) => (
57-
<div class="join my-1">
58-
<input
59-
type="text"
60-
value={key[0]()}
61-
onInput={(event) => key[1](event.target.value)}
62-
class="input input-bordered join-item w-32"
63-
placeholder="Env"
64-
required
65-
/>
66-
<input
67-
type="text"
68-
name={`env-${key[0]()}}`}
69-
value={value[0]()}
70-
onInput={(event) => value[1](event.target.value)}
71-
class="input select-bordered join-item w-full"
72-
placeholder="Value"
73-
/>
74-
<div
75-
class="btn btn-error join-item"
76-
tabindex={0}
77-
onClick={() => setEnv(env().filter((_, i) => i !== index()))}
78-
>
79-
<TrashIcon class="flex-shrink-0 w-5 h-5" />
80-
</div>
81-
</div>
115+
{({ key, value, type, secretKey, secretName }, index) => (
116+
<>
117+
<Show when={!type}>
118+
<div class="join my-1">
119+
<input
120+
type="text"
121+
value={key[0]()}
122+
onInput={(event) => key[1](event.target.value)}
123+
class="input input-bordered join-item w-32"
124+
placeholder="Env"
125+
required
126+
/>
127+
<input
128+
type="text"
129+
name={`env-${key[0]()}}`}
130+
value={value[0]()}
131+
onInput={(event) => value[1](event.target.value)}
132+
class="input select-bordered join-item w-full"
133+
placeholder="Value"
134+
/>
135+
<div
136+
class="btn btn-error join-item"
137+
tabindex={0}
138+
onClick={() =>
139+
setEnv(env().filter((_, i) => i !== index()))
140+
}
141+
>
142+
<TrashIcon class="flex-shrink-0 w-5 h-5" />
143+
</div>
144+
</div>
145+
</Show>
146+
<Show when={type === "secret"}>
147+
<div class="join my-1">
148+
<input
149+
type="text"
150+
value={key[0]()}
151+
onInput={(event) => key[1](event.target.value)}
152+
class="input input-bordered join-item w-32"
153+
placeholder="Env"
154+
required
155+
/>
156+
<select
157+
onchange={(event) => secretName[1](event.target.value)}
158+
class="select select-bordered join-item"
159+
>
160+
<For each={dbSecrets()}>
161+
{(secret) => (
162+
<option
163+
selected={value === secretName[0]()}
164+
value={secret.name}
165+
>
166+
{secret.labels["cnpg.io/cluster"]} (cnpg)
167+
</option>
168+
)}
169+
</For>
170+
</select>
171+
<select
172+
onchange={(event) => secretKey[1](event.target.value)}
173+
class="select select-bordered join-item w-full"
174+
>
175+
<For
176+
each={
177+
dbSecrets()?.find(
178+
(secret) => secret.name === secretName[0]()
179+
)?.values
180+
}
181+
>
182+
{(value) => (
183+
<option
184+
selected={value === secretKey[0]()}
185+
value={value}
186+
>
187+
{value}
188+
</option>
189+
)}
190+
</For>
191+
</select>
192+
<div
193+
class="btn btn-error join-item"
194+
tabindex={0}
195+
onClick={() =>
196+
setEnv(env().filter((_, i) => i !== index()))
197+
}
198+
>
199+
<TrashIcon class="flex-shrink-0 w-5 h-5" />
200+
</div>
201+
</div>
202+
</Show>
203+
</>
82204
)}
83205
</For>
84206

@@ -98,6 +220,38 @@ export const EnvVarsInput = ({ data }) => {
98220
>
99221
Add Variable
100222
</div>
223+
<div
224+
class="btn btn-outline btn-secondary join-item"
225+
tabindex={0}
226+
onClick={() => {
227+
const value = createSignal("");
228+
const secretName = createSignal(dbSecrets()?.[0]?.name);
229+
const secretKey = createSignal("");
230+
231+
createEffect(() => {
232+
value[1]({
233+
valueFrom: {
234+
secretKeyRef: {
235+
name: secretName[0](),
236+
key: secretKey[0](),
237+
},
238+
},
239+
});
240+
});
241+
setEnv([
242+
...env(),
243+
{
244+
key: createSignal(""),
245+
value,
246+
type: "secret",
247+
secretName,
248+
secretKey,
249+
},
250+
]);
251+
}}
252+
>
253+
Add Secret
254+
</div>
101255
{/* <div
102256
class="btn btn-outline btn-secondary join-item"
103257
onClick={async () =>
@@ -107,6 +261,20 @@ export const EnvVarsInput = ({ data }) => {
107261
Paste Env File
108262
</div> */}
109263
</div>
264+
{/* <div class="join">
265+
<select class="select select-bordered join-item">
266+
<For each={dbSecrets()}>
267+
{(secret) => (
268+
<option value={secret.name}>
269+
{secret.labels["cnpg.io/cluster"]}
270+
</option>
271+
)}
272+
</For>
273+
</select>
274+
<button class="btn join-item btn-info btn-outline">
275+
Connect Postgres
276+
</button>
277+
</div> */}
110278
</div>
111279
</>
112280
);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Show } from "solid-js";
2+
import { action, redirect, useSubmission } from "@solidjs/router";
3+
import { getUser } from "~/lib/auth";
4+
import { cnpg } from "~/lib/k8s";
5+
import { schemaDatabse } from "~/lib/types";
6+
7+
const createDatabaseFromForm = async (form: FormData) => {
8+
"use server";
9+
try {
10+
const database = await schemaDatabse.parseAsync({
11+
name: form.get("name"),
12+
init: {
13+
database: form.get("database"),
14+
owner: form.get("owner"),
15+
},
16+
instances: Number(form.get("instances")),
17+
size: Number(form.get("size")),
18+
});
19+
const user = await getUser();
20+
await cnpg.createDatabase(database, user.name);
21+
} catch (e) {
22+
console.error(e);
23+
}
24+
};
25+
26+
const createDatabaseAction = action(createDatabaseFromForm, "createDatabase");
27+
28+
export const CreateDatabaseForm = () => {
29+
const createDatabaseStatus = useSubmission(createDatabaseAction);
30+
31+
return (
32+
<dialog id="create-database-modal" class="modal">
33+
<div class="modal-box">
34+
<form action={createDatabaseAction} method="post">
35+
<h3 class="font-bold text-lg">Deploy new Database</h3>
36+
<div class="collapse collapse-arrow bg-base-200 my-2">
37+
<input type="radio" name="my-accordion-2" checked={true} />
38+
<div class="collapse-title text-xl font-medium">General</div>
39+
<div class="collapse-content">
40+
<label class="form-control w-full">
41+
<div class="label">
42+
<span class="label-text">Name</span>
43+
</div>
44+
<input
45+
type="text"
46+
name="name"
47+
required
48+
placeholder="my-database"
49+
class="input input-bordered w-full"
50+
/>
51+
</label>
52+
<label class="form-control w-full">
53+
<div class="label">
54+
<span class="label-text">Instances</span>
55+
</div>
56+
<input
57+
type="number"
58+
name="instances"
59+
required
60+
placeholder="3"
61+
class="input input-bordered w-full"
62+
/>
63+
</label>
64+
<label class="form-control w-full">
65+
<div class="label">
66+
<span class="label-text">Size in GiB</span>
67+
</div>
68+
<input
69+
type="number"
70+
name="size"
71+
required
72+
placeholder="1"
73+
class="input input-bordered w-full"
74+
/>
75+
</label>
76+
</div>
77+
</div>
78+
<div class="collapse collapse-arrow bg-base-200 my-2">
79+
<input type="radio" name="my-accordion-2" />
80+
<div class="collapse-title text-xl font-medium">Bootstrap</div>
81+
<div class="collapse-content">
82+
<label class="form-control w-full">
83+
<div class="label">
84+
<span class="label-text">Database Name</span>
85+
</div>
86+
<input
87+
type="text"
88+
name="database"
89+
required
90+
placeholder="my-database"
91+
class="input input-bordered w-full"
92+
/>
93+
</label>
94+
<label class="form-control w-full">
95+
<div class="label">
96+
<span class="label-text">Database Owner</span>
97+
</div>
98+
<input
99+
type="text"
100+
name="owner"
101+
required
102+
placeholder="my-user"
103+
class="input input-bordered w-full"
104+
/>
105+
</label>
106+
</div>
107+
</div>
108+
<button type="submit" id="submitForm" class="hidden" />
109+
</form>
110+
<div class="modal-action">
111+
<form method="dialog">
112+
<button id="closeDialog" class="btn">
113+
Close
114+
</button>
115+
</form>
116+
<label for="submitForm" tabindex={0} class="btn btn-primary">
117+
<Show when={createDatabaseStatus.pending}>
118+
<span class="loading loading-spinner"></span>
119+
</Show>
120+
Deploy
121+
</label>
122+
</div>
123+
</div>
124+
</dialog>
125+
);
126+
};

0 commit comments

Comments
 (0)