Skip to content

Commit

Permalink
Merge pull request #7 from deploy-cat/feature/cnpg
Browse files Browse the repository at this point in the history
Implement Managed Postgres
  • Loading branch information
adb-sh authored Nov 16, 2024
2 parents 9b75bc8 + e13d030 commit f77f75a
Show file tree
Hide file tree
Showing 17 changed files with 1,071 additions and 80 deletions.
232 changes: 200 additions & 32 deletions src/components/EnvVarsInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { For, type Signal, createSignal } from "solid-js";
import { For, Show, type Signal, createEffect, createSignal } from "solid-js";
import { TrashIcon } from "@deploy-cat/heroicons-solid/24/solid/esm";
import { cache, createAsync } from "@solidjs/router";
import { getUser } from "~/lib/auth";
import { k8sCore } from "~/lib/k8s";
import { CircleStackIcon } from "@deploy-cat/heroicons-solid/24/solid/esm";

const getDatabseSecrets = cache(async () => {
"use server";
try {
const user = await getUser();
const secrets = await k8sCore.listNamespacedSecret(
user.name,
undefined,
undefined,
undefined,
undefined,
"cnpg.io/cluster"
);
return secrets.body.items.map(({ data, metadata: { name, labels } }) => ({
name,
labels,
values: Object.keys(data),
}));
} catch (e) {
console.error(e);
}
}, "db-secrets");

const parseClipboard = (text: string) =>
text
Expand All @@ -14,13 +40,46 @@ const parseClipboard = (text: string) =>
});

export const EnvVarsInput = ({ data }) => {
const dbSecrets = createAsync(() => getDatabseSecrets());

const [env, setEnv] = createSignal(
(data &&
Object.entries(data).map(([key, value]) => ({
key: createSignal(key),
value: createSignal(value),
}))) ??
([] as Array<{ key: Signal<string>; value: Signal<string> }>)
Object.entries(data).map(([k, v]) => {
if (typeof v == "string") {
return {
key: createSignal(k),
value: createSignal(v),
};
}

const value = createSignal({
secretKeyRef: {
name: v.valueFrom.secretKeyRef.name,
key: v.valueFrom.secretKeyRef.key,
},
} as any);
const secretName = createSignal(v.valueFrom.secretKeyRef.name);
const secretKey = createSignal(v.valueFrom.secretKeyRef.key);

createEffect(() => {
value[1]({
valueFrom: {
secretKeyRef: {
name: secretName[0](),
key: secretKey[0](),
},
},
});
});
return {
key: createSignal(k),
value,
type: "secret",
secretName,
secretKey,
};
})) ??
([] as Array<{ key: Signal<string>; value: Signal<string | any> }>)
);

return (
Expand Down Expand Up @@ -53,32 +112,95 @@ export const EnvVarsInput = ({ data }) => {
</div>
}
>
{({ key, value }, index) => (
<div class="join my-1">
<input
type="text"
value={key[0]()}
onInput={(event) => key[1](event.target.value)}
class="input input-bordered join-item w-32"
placeholder="Env"
required
/>
<input
type="text"
name={`env-${key[0]()}}`}
value={value[0]()}
onInput={(event) => value[1](event.target.value)}
class="input select-bordered join-item w-full"
placeholder="Value"
/>
<div
class="btn btn-error join-item"
tabindex={0}
onClick={() => setEnv(env().filter((_, i) => i !== index()))}
>
<TrashIcon class="flex-shrink-0 w-5 h-5" />
</div>
</div>
{({ key, value, type, secretKey, secretName }, index) => (
<>
<Show when={!type}>
<div class="join my-1">
<input
type="text"
value={key[0]()}
onInput={(event) => key[1](event.target.value)}
class="input input-bordered join-item w-32"
placeholder="Env"
required
/>
<input
type="text"
name={`env-${key[0]()}}`}
value={value[0]()}
onInput={(event) => value[1](event.target.value)}
class="input select-bordered join-item w-full"
placeholder="Value"
/>
<div
class="btn btn-error join-item"
tabindex={0}
onClick={() =>
setEnv(env().filter((_, i) => i !== index()))
}
>
<TrashIcon class="flex-shrink-0 w-5 h-5" />
</div>
</div>
</Show>
<Show when={type === "secret"}>
<div class="join my-1">
<input
type="text"
value={key[0]()}
onInput={(event) => key[1](event.target.value)}
class="input input-bordered join-item w-32"
placeholder="Env"
required
/>
<select
onchange={(event) => secretName[1](event.target.value)}
class="select select-bordered join-item"
>
<For each={dbSecrets()}>
{(secret) => (
<option
selected={value === secretName[0]()}
value={secret.name}
>
{secret.labels["cnpg.io/cluster"]} (cnpg)
</option>
)}
</For>
</select>
<select
onchange={(event) => secretKey[1](event.target.value)}
class="select select-bordered join-item w-full"
>
<For
each={
dbSecrets()?.find(
(secret) => secret.name === secretName[0]()
)?.values
}
>
{(value) => (
<option
selected={value === secretKey[0]()}
value={value}
>
{value}
</option>
)}
</For>
</select>
<div
class="btn btn-error join-item"
tabindex={0}
onClick={() =>
setEnv(env().filter((_, i) => i !== index()))
}
>
<TrashIcon class="flex-shrink-0 w-5 h-5" />
</div>
</div>
</Show>
</>
)}
</For>

Expand All @@ -98,6 +220,38 @@ export const EnvVarsInput = ({ data }) => {
>
Add Variable
</div>
<div
class="btn btn-outline btn-secondary join-item"
tabindex={0}
onClick={() => {
const value = createSignal("");
const secretName = createSignal(dbSecrets()?.[0]?.name);
const secretKey = createSignal("");

createEffect(() => {
value[1]({
valueFrom: {
secretKeyRef: {
name: secretName[0](),
key: secretKey[0](),
},
},
});
});
setEnv([
...env(),
{
key: createSignal(""),
value,
type: "secret",
secretName,
secretKey,
},
]);
}}
>
Add Secret
</div>
{/* <div
class="btn btn-outline btn-secondary join-item"
onClick={async () =>
Expand All @@ -107,6 +261,20 @@ export const EnvVarsInput = ({ data }) => {
Paste Env File
</div> */}
</div>
{/* <div class="join">
<select class="select select-bordered join-item">
<For each={dbSecrets()}>
{(secret) => (
<option value={secret.name}>
{secret.labels["cnpg.io/cluster"]}
</option>
)}
</For>
</select>
<button class="btn join-item btn-info btn-outline">
Connect Postgres
</button>
</div> */}
</div>
</>
);
Expand Down
126 changes: 126 additions & 0 deletions src/components/cloud/CreateDatabaseForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Show } from "solid-js";
import { action, redirect, useSubmission } from "@solidjs/router";
import { getUser } from "~/lib/auth";
import { cnpg } from "~/lib/k8s";
import { schemaDatabse } from "~/lib/types";

const createDatabaseFromForm = async (form: FormData) => {
"use server";
try {
const database = await schemaDatabse.parseAsync({
name: form.get("name"),
init: {
database: form.get("database"),
owner: form.get("owner"),
},
instances: Number(form.get("instances")),
size: Number(form.get("size")),
});
const user = await getUser();
await cnpg.createDatabase(database, user.name);
} catch (e) {
console.error(e);
}
};

const createDatabaseAction = action(createDatabaseFromForm, "createDatabase");

export const CreateDatabaseForm = () => {
const createDatabaseStatus = useSubmission(createDatabaseAction);

return (
<dialog id="create-database-modal" class="modal">
<div class="modal-box">
<form action={createDatabaseAction} method="post">
<h3 class="font-bold text-lg">Deploy new Database</h3>
<div class="collapse collapse-arrow bg-base-200 my-2">
<input type="radio" name="my-accordion-2" checked={true} />
<div class="collapse-title text-xl font-medium">General</div>
<div class="collapse-content">
<label class="form-control w-full">
<div class="label">
<span class="label-text">Name</span>
</div>
<input
type="text"
name="name"
required
placeholder="my-database"
class="input input-bordered w-full"
/>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Instances</span>
</div>
<input
type="number"
name="instances"
required
placeholder="3"
class="input input-bordered w-full"
/>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Size in GiB</span>
</div>
<input
type="number"
name="size"
required
placeholder="1"
class="input input-bordered w-full"
/>
</label>
</div>
</div>
<div class="collapse collapse-arrow bg-base-200 my-2">
<input type="radio" name="my-accordion-2" />
<div class="collapse-title text-xl font-medium">Bootstrap</div>
<div class="collapse-content">
<label class="form-control w-full">
<div class="label">
<span class="label-text">Database Name</span>
</div>
<input
type="text"
name="database"
required
placeholder="my-database"
class="input input-bordered w-full"
/>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Database Owner</span>
</div>
<input
type="text"
name="owner"
required
placeholder="my-user"
class="input input-bordered w-full"
/>
</label>
</div>
</div>
<button type="submit" id="submitForm" class="hidden" />
</form>
<div class="modal-action">
<form method="dialog">
<button id="closeDialog" class="btn">
Close
</button>
</form>
<label for="submitForm" tabindex={0} class="btn btn-primary">
<Show when={createDatabaseStatus.pending}>
<span class="loading loading-spinner"></span>
</Show>
Deploy
</label>
</div>
</div>
</dialog>
);
};
Loading

0 comments on commit f77f75a

Please sign in to comment.