Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 20 additions & 15 deletions packages/frontend/src/lib/components/LabeledInput.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
<script lang="ts">
import { type Icon, TriangleAlertIcon } from "@lucide/svelte";
import type { RemoteFormIssue } from "@sveltejs/kit";
import type { HTMLInputAttributes } from "svelte/elements";
import { type Icon, TriangleAlertIcon } from "@lucide/svelte";
import type { RemoteFormIssue } from "@sveltejs/kit";
import type { Snippet } from "svelte";
import type { HTMLInputAttributes } from "svelte/elements";

interface Props extends HTMLInputAttributes {
label: string;
icon?: typeof Icon;
issues?: RemoteFormIssue[];
}
interface Props extends HTMLInputAttributes {
label: string;
icon?: typeof Icon;
issues?: RemoteFormIssue[];
suffix?: Snippet;
}

let { label, icon, issues, ...props }: Props = $props();
let { label, icon, issues, suffix, ...props }: Props = $props();
</script>

<label class="label">
<div class={[
"ml-1 flex gap-1",
issues && "text-error-500"
]}>
<div class={["ml-1 flex gap-1", issues && "text-error-500"]}>
{#if issues}
<TriangleAlertIcon size={16} />
{:else if icon}
Expand All @@ -25,9 +24,15 @@ let { label, icon, issues, ...props }: Props = $props();
{/if}

<span class="label-text">
{label}<span class="text-primary-500">{props.required && "*"}</span>
{label}
{#if props.required}
<span class="text-primary-500">*</span>
{/if}
</span>
</div>

<input class="input" {...props} />
<div class="input flex items-center gap-1">
<input class="grow" {...props} />
{@render suffix?.()}
</div>
</label>
8 changes: 4 additions & 4 deletions packages/frontend/src/lib/remotes/auth.remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { env } from "$env/dynamic/private";
import { loginSchema, registerSchema } from "@hallmaster/backend/dto";
import { error, invalid, redirect } from "@sveltejs/kit";

export const register = form(registerSchema, async (data) => {
export const register = form(registerSchema, async (payload) => {
const response = await fetch(`${env.API_URL}/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
body: JSON.stringify(payload),
});

switch (response.status) {
Expand All @@ -30,13 +30,13 @@ export const register = form(registerSchema, async (data) => {
}
});

export const login = form(loginSchema, async (data, issue) => {
export const login = form(loginSchema, async (payload, issue) => {
const response = await fetch(`${env.API_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
body: JSON.stringify(payload),
});

switch (response.status) {
Expand Down
90 changes: 84 additions & 6 deletions packages/frontend/src/lib/remotes/bot.remote.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { command, form, getRequestEvent } from "$app/server";
import { command, form, getRequestEvent, query } from "$app/server";
import { env } from "$env/dynamic/private";
import { CreateBotSchema, UpdateBotSchema } from "@hallmaster/backend/dto";
import { CreateBotSchema, UpdateBotSchema, type GetBotDto } from "@hallmaster/backend/dto";
import { error, redirect } from "@sveltejs/kit";

import { getClusters } from "./clusters.remote";

export const createBot = form(CreateBotSchema, async (data) => {
export const createBot = form(CreateBotSchema, async (payload) => {
const token = getRequestEvent().cookies.get("token");

const response = await fetch(`${env.API_URL}/bot`, {
Expand All @@ -14,7 +14,7 @@ export const createBot = form(CreateBotSchema, async (data) => {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
body: JSON.stringify(payload),
});

switch (response.status) {
Expand All @@ -30,7 +30,85 @@ export const createBot = form(CreateBotSchema, async (data) => {
}
});

export const updateBotLayout = command(UpdateBotSchema.pick({ layout: true }), async (layout) => {
export const getBot = query<GetBotDto>(async () => {
const token = getRequestEvent().cookies.get("token");

const response = await fetch(new URL("/bot", env.API_URL), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});

switch (response.status) {
case 200:
return response.json();
case 401:
return error(401, "Unauthorized");
case 404:
return error(404, "Bot not found");

default:
return error(500, "An error occurred");
}
});

export const updateBotToken = form(UpdateBotSchema.pick({ token: true }), async (payload) => {
const token = getRequestEvent().cookies.get("token");

const response = await fetch(new URL("/bot", env.API_URL), {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});

switch (response.status) {
case 202:
getBot().set(await response.json());
return;
case 401:
return error(401, "Unauthorized");
case 404:
return error(404, "Bot not found");

default:
console.error(await response.text());
return error(500, "An error occurred");
}
});

export const updateBotImage = form(UpdateBotSchema.pick({ dockerImage: true }), async (payload) => {
const token = getRequestEvent().cookies.get("token");

const response = await fetch(new URL("/bot", env.API_URL), {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});

switch (response.status) {
case 202:
getBot().set(await response.json());
return;
case 401:
return error(401, "Unauthorized");
case 404:
return error(404, "Bot not found");

default:
console.error(await response.text());
return error(500, "An error occured");
}
});

export const updateBotLayout = command(UpdateBotSchema.pick({ layout: true }), async (payload) => {
const token = getRequestEvent().cookies.get("token");

const response = await fetch(new URL("/bot", env.API_URL), {
Expand All @@ -39,7 +117,7 @@ export const updateBotLayout = command(UpdateBotSchema.pick({ layout: true }), a
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(layout),
body: JSON.stringify(payload),
});

switch (response.status) {
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/routes/(app)/settings/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import type { LayoutProps } from "./$types";

let { children }: LayoutProps = $props();
</script>

<main class="max-w-2xl flex flex-col gap-4 mx-auto">
{@render children()}
</main>
106 changes: 106 additions & 0 deletions packages/frontend/src/routes/(app)/settings/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script lang="ts">
import LabeledInput from "$lib/components/LabeledInput.svelte";
import {
getBot,
updateBotImage,
updateBotToken,
} from "$lib/remotes/bot.remote";
import {
BracesIcon,
ContainerIcon,
LoaderCircleIcon,
RectangleEllipsisIcon,
SaveIcon,
TextCursorIcon,
} from "@lucide/svelte";
import { Switch } from "@skeletonlabs/skeleton-svelte";
import { slide } from "svelte/transition";

const { dockerImage: defaultValues } = await getBot();
updateBotImage.fields.dockerImage.username.set(
defaultValues.username ?? undefined,
);
updateBotImage.fields.dockerImage.image.set(defaultValues.image);

let authentication: boolean = $state(!!defaultValues.username);
</script>

<h6 class="h6 font-normal">Deployment</h6>

<div class="flex flex-col gap-4">
<form {...updateBotImage} class="flex flex-col gap-3">
<LabeledInput
label="Container image url"
icon={ContainerIcon}
placeholder="ghcr.io/image:tag"
required
{...updateBotImage.fields.dockerImage.image.as("text")}
issues={updateBotImage.fields.dockerImage.image.issues()}
>
{#snippet suffix()}
<button class="btn-icon btn-icon-sm text-primary-700-300">
{#if updateBotImage.pending}
<LoaderCircleIcon class="animate-spin" />
{:else}
<SaveIcon />
{/if}
</button>
{/snippet}
</LabeledInput>

<Switch
defaultChecked={authentication}
checked={authentication}
onCheckedChange={(details) => (authentication = details.checked)}
>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Registry authentication</Switch.Label>
<Switch.HiddenInput />
</Switch>

{#if authentication}
<div class="grid grid-cols-2 gap-2" transition:slide={{ duration: 150 }}>
<LabeledInput
label="Username"
icon={TextCursorIcon}
required
{...updateBotImage.fields.dockerImage.username.as("text")}
issues={updateBotImage.fields.dockerImage.username.issues()}
/>

<LabeledInput
label="Password"
icon={RectangleEllipsisIcon}
required
{...updateBotImage.fields.dockerImage.password.as("password")}
issues={updateBotImage.fields.dockerImage.password.issues()}
/>
</div>
{/if}
</form>

<hr class="hr" />

<form {...updateBotToken}>
<LabeledInput
label="Bot token"
icon={BracesIcon}
placeholder="8f0a2f30b4e738362ee19e8e572edb8a"
required
{...updateBotToken.fields.token.as("password")}
issues={updateBotToken.fields.token.issues()}
>
{#snippet suffix()}
<button class="btn-icon btn-icon-sm text-primary-700-300">
{#if updateBotToken.pending}
<LoaderCircleIcon class="animate-spin" />
{:else}
<SaveIcon />
{/if}
</button>
{/snippet}
</LabeledInput>
</form>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</Switch>

{#if authentication}
<div class="flex gap-2" transition:slide={{ duration: 150 }}>
<div class="grid grid-cols-2 gap-2" transition:slide={{ duration: 150 }}>
<LabeledInput
label="Username"
icon={TextCursorIcon}
Expand Down
Loading