A focused Docker Compose management web application.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: project page error if first page

Brooke 651ef819 10a952eb

+96 -48
-39
packages/node/src/api/compose.rs
··· 1 - //! Manages retrieving and updating project compose files. 2 - 3 - use std::str::from_utf8; 4 - 5 - use eyre::Context; 6 - use salvo::Request; 7 - use salvo::Writer; 8 - use salvo::{ 9 - Depot, Router, 10 - oapi::{endpoint, extract::PathParam}, 11 - }; 12 - 13 - use crate::{api::response::LuminaryResponse, core::LuminaryEngine, obtain}; 14 - 15 - /// Returns the router for compose related endpoints. 16 - pub fn router() -> Router { 17 - return Router::with_path("compose").get(get_compose).put(put_compose); 18 - } 19 - 20 - /// Retrieves the compose file for a given project. 21 - #[endpoint] 22 - async fn get_compose(project: PathParam<String>, depot: &mut Depot) -> LuminaryResponse<String> { 23 - let engine = obtain!(depot, LuminaryEngine); 24 - return Ok(engine.get_compose(&project.into_inner()).await?.into()); 25 - } 26 - 27 - /// Updates the compose file for a given project. 28 - #[endpoint] 29 - async fn put_compose( 30 - project: PathParam<String>, 31 - req: &mut Request, 32 - depot: &mut Depot, 33 - ) -> LuminaryResponse<()> { 34 - let engine = obtain!(depot, LuminaryEngine); 35 - let bytes = req.payload().await.wrap_err("Failed to read request body")?; 36 - let compose = from_utf8(bytes).wrap_err("Failed to decode error")?; 37 - engine.put_compose(&project.into_inner(), compose).await?; 38 - return Ok(().into()); 39 - }
+2 -2
packages/node/src/api/mod.rs
··· 16 16 17 17 mod action; 18 18 mod auth; 19 - mod compose; 19 + mod project; 20 20 mod realtime; 21 21 mod response; 22 22 ··· 72 72 .push(Router::with_path("realtime").get(app_subscribe)) 73 73 .push( 74 74 Router::with_path("/project/{project}") 75 - .push(compose::router()) 75 + .push(project::router()) 76 76 .push(Router::with_path("logs").get(logs_subscribe)) 77 77 .push(Router::with_path("restart").post(action::restart_project)) 78 78 .push(Router::with_path("start").post(action::start_project))
+60
packages/node/src/api/project.rs
··· 1 + //! Manages retrieving and updating project compose files. 2 + 3 + use std::str::from_utf8; 4 + 5 + use eyre::Context; 6 + use salvo::Request; 7 + use salvo::Router; 8 + use salvo::Writer; 9 + use salvo::oapi::ToSchema; 10 + use salvo::{ 11 + Depot, 12 + oapi::{endpoint, extract::PathParam}, 13 + }; 14 + use serde::Serialize; 15 + 16 + use crate::core::LuminaryProject; 17 + use crate::{api::response::LuminaryResponse, core::LuminaryEngine, obtain}; 18 + 19 + /// Returns the router for compose related endpoints. 20 + pub fn router() -> Router { 21 + return Router::new().get(get_project).put(put_compose); 22 + } 23 + 24 + /// Retrieves the compose file for a given project. 25 + #[endpoint] 26 + pub async fn get_project( 27 + project: PathParam<String>, 28 + depot: &mut Depot, 29 + ) -> LuminaryResponse<LuminaryProjectWithCompose> { 30 + let engine = obtain!(depot, LuminaryEngine); 31 + 32 + let name = project.into_inner(); 33 + let compose = engine.get_compose(&name).await?; 34 + let project = engine.get_project(&name).await?; 35 + 36 + return Ok(LuminaryProjectWithCompose { project, compose }.into()); 37 + } 38 + 39 + #[derive(Debug, Clone, Serialize, ToSchema)] 40 + struct LuminaryProjectWithCompose { 41 + #[serde(flatten)] 42 + project: LuminaryProject, 43 + 44 + /// The compose file for this project. 45 + compose: String, 46 + } 47 + 48 + /// Updates the compose file for a given project. 49 + #[endpoint] 50 + pub async fn put_compose( 51 + project: PathParam<String>, 52 + req: &mut Request, 53 + depot: &mut Depot, 54 + ) -> LuminaryResponse<()> { 55 + let engine = obtain!(depot, LuminaryEngine); 56 + let bytes = req.payload().await.wrap_err("Failed to read request body")?; 57 + let compose = from_utf8(bytes).wrap_err("Failed to decode error")?; 58 + engine.put_compose(&project.into_inner(), compose).await?; 59 + return Ok(().into()); 60 + }
+12 -1
packages/node/src/core/state.rs
··· 7 7 secret::ContainerSummaryStateEnum, 8 8 }; 9 9 use docker_compose_types::Compose; 10 - use eyre::{Result, WrapErr}; 10 + use eyre::{ContextCompat, Result, WrapErr}; 11 11 use futures_util::{StreamExt, stream::BoxStream}; 12 12 use log::{debug, error, warn}; 13 13 use luminary_macros::wrap_err; ··· 26 26 const COMPOSE_SERVICE_LABEL: &str = "com.docker.compose.service"; 27 27 28 28 impl LuminaryEngine { 29 + pub async fn get_project(&self, name: &String) -> Result<LuminaryProject> { 30 + return Ok(self 31 + .state 32 + .read() 33 + .await 34 + .0 35 + .get(name) 36 + .cloned() 37 + .wrap_err("Failed to fetch project")?); 38 + } 39 + 29 40 pub async fn state_subscribe<'a>(&'_ self) -> BoxStream<'a, LuminaryStateList> { 30 41 let mut reciever = self.state_channel.subscribe(); 31 42 let initial = self.state.read().await.clone();
+6
packages/panel/src/lib/api/index.ts
··· 16 16 17 17 type ExcludeFail<T> = T extends LuminaryFailResponse ? never : T; 18 18 type UnwrapSuccess<T> = T extends { success: true; data: infer D } ? D : T; 19 + 20 + /** 21 + * Transform OpenAPI type definition to unwrap responses as if they could never fail. 22 + * 23 + * This is done because the handling of errors is done in the middleware. 24 + */ 19 25 type CaughtPaths = { 20 26 [P in keyof paths]: { 21 27 [M in keyof paths[P]]: paths[P][M] extends { responses: infer R }
+2 -1
packages/panel/src/lib/api/realtime.svelte.ts
··· 20 20 */ 21 21 export const getList = () => list; 22 22 23 + export const putProject = (project: LuminaryProject) => (list = { ...list, [project.name]: project }); 24 + 23 25 /** 24 26 * Subscribes to real-time updates from the server. 25 27 */ ··· 46 48 }); 47 49 48 50 backoff.reset(); 49 - list = {}; 50 51 51 52 try { 52 53 for await (const event of parseServerSentEvents(
+1 -1
packages/panel/src/routes/(authenticated)/projects/[project]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { faCircleInfo, faGears, faLayerGroup, faPencil } from "@fortawesome/free-solid-svg-icons"; 3 + import LogTerminal from "$lib/component/LogTerminal.svelte"; 3 4 import StatusIcon from "$lib/component/StatusIcon.svelte"; 4 5 import Tabs from "$lib/component/Tabs.svelte"; 5 6 import StatusTab from "./ProjectStatus.svelte"; 6 7 import { getList } from "$lib/api"; 7 8 import { page } from "$app/state"; 8 9 import Fa from "svelte-fa"; 9 - import LogTerminal from "$lib/component/LogTerminal.svelte"; 10 10 11 11 let project = $derived(getList()[page.params.project!]); 12 12 let { data } = $props();
+13 -4
packages/panel/src/routes/(authenticated)/projects/[project]/+page.ts
··· 1 + import type { components } from "$lib/api/openapi"; 2 + import type { LuminaryProject } from "$lib/api"; 1 3 import type { PageLoad } from "./$types"; 2 4 import { error } from "@sveltejs/kit"; 3 5 import { api } from "$lib"; 4 6 5 7 export const prerender = false; 6 8 9 + type LuminaryProjectWithCompose = components["schemas"]["luminary_node.api.project.LuminaryProjectWithCompose"]; 10 + 7 11 export const load: PageLoad = async ({ params }) => { 8 12 const response = await api.client 9 - .GET("/api/project/{project}/compose", { 13 + .GET("/api/project/{project}", { 10 14 params: { path: { project: params.project } }, 11 15 }) 12 16 .catch(() => error(404, "Project not found")); 13 17 14 - return { 15 - compose: response.data!, 16 - }; 18 + const data = response.data! as LuminaryProject & Partial<LuminaryProjectWithCompose>; 19 + const compose = data.compose!; 20 + delete data.compose; 21 + 22 + // Update global project list immediately in case realtime patches haven't arrived yet 23 + api.putProject(data); 24 + 25 + return { compose }; 17 26 };