Files
continuum-app/apps/web/pages/setup.vue
T
octoturge 69f35c2a6b Let the add-printer form take every supported kind
A kind selector (Bambu, Klipper, PrusaLink, OctoPrint) decides which fields
appear: serial and access code for Bambu, an API key for PrusaLink and
OctoPrint, neither for Klipper. Discovered printers of any of these kinds
now fill the form in.
2026-10-30 07:49:31 +00:00

129 lines
3.9 KiB
Vue

<script setup lang="ts">
import {
AddPrinter,
GatewaySetup,
ManagedPrinters,
OnboardingForm,
type AppFarm,
type AppFarmDiscovery,
type AppOrg,
type AppPrinterState,
type NewPrinter,
} from "@continuum/ui";
const apiFetch = useApiFetch();
const { wsBase } = useRuntimeConfig().public;
const { data: orgs, refresh: refreshOrgs } = await useAsyncData("setup-orgs", () => apiFetch<AppOrg[]>("/orgs"));
const { data: farms, refresh: refreshFarms } = await useAsyncData("setup-farms", () => apiFetch<AppFarm[]>("/farms"));
const { data: printers, refresh: refreshPrinters } = await useAsyncData("setup-printers", () =>
apiFetch<AppPrinterState[]>("/printers"),
);
// The gateway's presence and what it found, per farm - the same endpoint the
// fleet page reads. Polled while this page is open so "gateway connected"
// turns true as soon as someone finishes configuring it.
const status = ref<Record<string, AppFarmDiscovery>>({});
async function refreshStatus() {
for (const farm of farms.value ?? []) {
try {
status.value[farm.id] = await apiFetch<AppFarmDiscovery>(`/farms/${farm.id}/discovered-printers`);
} catch {
// Keep the last answer; a failed poll is not "offline".
}
}
}
let poll: ReturnType<typeof setInterval> | undefined;
onMounted(() => {
void refreshStatus();
poll = setInterval(() => void refreshStatus(), 5_000);
});
onBeforeUnmount(() => clearInterval(poll));
/**
* Creates the first farm. Reuses the caller's organisation if they already
* have one (a person who was invited has an org and no farm), and creates one
* named after the farm otherwise.
*/
async function createFarm(farmName: string) {
try {
let orgId = orgs.value?.[0]?.id;
if (!orgId) {
const org = await apiFetch<AppOrg>("/orgs", { method: "POST", body: { name: farmName } });
orgId = org.id;
}
await apiFetch("/farms", { method: "POST", body: { name: farmName, orgId } });
} catch (err) {
throw new Error(apiErrorMessage(err, "the farm could not be created"));
}
await Promise.all([refreshOrgs(), refreshFarms()]);
await refreshStatus();
}
async function mintToken(farmId: string): Promise<string> {
try {
const result = await apiFetch<{ edgeProxyToken: string }>(`/farms/${farmId}/edge-proxy-token`, {
method: "POST",
});
return result.edgeProxyToken;
} catch (err) {
throw new Error(apiErrorMessage(err, "the token could not be created"));
}
}
async function addPrinter(farmId: string, printer: NewPrinter) {
try {
await apiFetch(`/farms/${farmId}/printers`, {
method: "POST",
body: printer,
});
} catch (err) {
throw new Error(apiErrorMessage(err, "the printer could not be added"));
}
await refreshPrinters();
}
async function removePrinter(printerId: string) {
try {
await apiFetch(`/printers/${printerId}`, { method: "DELETE" });
} catch (err) {
throw new Error(apiErrorMessage(err, "the printer could not be removed"));
}
await refreshPrinters();
}
const printersOf = (farmId: string) => (printers.value ?? []).filter((p) => p.farmId === farmId);
</script>
<template>
<div class="setup">
<h1>Setup</h1>
<OnboardingForm v-if="!farms?.length" :create="createFarm" />
<section v-for="farm in farms ?? []" :key="farm.id" class="setup__farm">
<h2>{{ farm.name }}</h2>
<GatewaySetup
:farm="farm"
:gateway-online="status[farm.id]?.gatewayOnline ?? null"
:uplink-url="`${wsBase}/ws/edge/v1`"
:mint="() => mintToken(farm.id)"
/>
<AddPrinter
:gateway-online="status[farm.id]?.gatewayOnline ?? null"
:candidates="status[farm.id]?.discovery?.printers ?? []"
:add="(printer: NewPrinter) => addPrinter(farm.id, printer)"
/>
<ManagedPrinters :printers="printersOf(farm.id)" :remove="removePrinter" />
</section>
</div>
</template>
<style scoped>
.setup__farm {
border-top: 1px solid #1f232c;
margin-top: 1.5rem;
padding-top: 1rem;
}
</style>