A focused Docker Compose management web application.
0
fork

Configure Feed

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

fix: improve accessibility

renamed redeploy to recreate, fixed mobile detection, added tooltips, added aria-labels

Brooke 8599fb0b f1a1b2ca

+207 -99
+6 -6
packages/node/src/api/action.rs
··· 78 78 return Ok(().into()); 79 79 } 80 80 81 - /// Redeploys the given project and all its services. 81 + /// recreates the given project and all its services. 82 82 #[endpoint] 83 - pub async fn redeploy_project(project: PathParam<String>, depot: &mut Depot) -> LuminaryResponse<()> { 83 + pub async fn recreate_project(project: PathParam<String>, depot: &mut Depot) -> LuminaryResponse<()> { 84 84 let engine = obtain!(depot, LuminaryEngine); 85 85 86 - engine.redeploy(&project.into_inner(), None).await?; 86 + engine.recreate(&project.into_inner(), None).await?; 87 87 return Ok(().into()); 88 88 } 89 89 90 - /// Redeploys the given service of the project. 90 + /// recreates the given service of the project. 91 91 #[endpoint] 92 - pub async fn redeploy_service( 92 + pub async fn recreate_service( 93 93 project: PathParam<String>, 94 94 service: PathParam<String>, 95 95 depot: &mut Depot, ··· 97 97 let engine = obtain!(depot, LuminaryEngine); 98 98 99 99 engine 100 - .redeploy(&project.into_inner(), Some(&service.into_inner())) 100 + .recreate(&project.into_inner(), Some(&service.into_inner())) 101 101 .await?; 102 102 return Ok(().into()); 103 103 }
+2 -2
packages/node/src/api/mod.rs
··· 77 77 .push(Router::with_path("restart").post(action::restart_project)) 78 78 .push(Router::with_path("start").post(action::start_project)) 79 79 .push(Router::with_path("stop").post(action::stop_project)) 80 - .push(Router::with_path("redeploy").post(action::redeploy_project)) 80 + .push(Router::with_path("recreate").post(action::recreate_project)) 81 81 .push(Router::with_path("pull").post(action::pull_project)) 82 82 .push(Router::with_path("build").post(action::build_project)) 83 83 .push( ··· 85 85 .push(Router::with_path("restart").post(action::restart_service)) 86 86 .push(Router::with_path("start").post(action::start_service)) 87 87 .push(Router::with_path("stop").post(action::stop_service)) 88 - .push(Router::with_path("redeploy").post(action::redeploy_service)) 88 + .push(Router::with_path("recreate").post(action::recreate_service)) 89 89 .push(Router::with_path("pull").post(action::pull_service)) 90 90 .push(Router::with_path("build").post(action::build_service)), 91 91 ),
+4 -4
packages/node/src/core/action.rs
··· 87 87 Ok(()) 88 88 } 89 89 90 - #[wrap_err("Failed to redeploy project/service")] 91 - pub async fn redeploy(&self, project: &str, service: Option<&str>) -> Result<()> { 90 + #[wrap_err("Failed to recreate project/service")] 91 + pub async fn recreate(&self, project: &str, service: Option<&str>) -> Result<()> { 92 92 self.stop(project, service).await?; 93 93 self.start(project, service).await?; 94 94 Ok(()) ··· 99 99 pub async fn pull(&self, project: &str, service: Option<&str>) -> Result<()> { 100 100 self.run(LuminaryAction::Pulling, project, service, vec!["pull"]) 101 101 .await?; 102 - self.redeploy(project, service).await?; 102 + self.recreate(project, service).await?; 103 103 Ok(()) 104 104 } 105 105 ··· 114 114 ) 115 115 .await?; 116 116 117 - self.redeploy(project, service).await?; 117 + self.recreate(project, service).await?; 118 118 Ok(()) 119 119 } 120 120 }
+3 -1
packages/panel/src/lib/component/LoaderButton.svelte
··· 8 8 import type { Snippet } from "svelte"; 9 9 10 10 let { 11 + "aria-label": ariaLabel, 11 12 style = "button", 12 13 loading = false, 13 14 children, ··· 17 18 style?: "button" | "a" | "outline"; 18 19 children: Snippet<[boolean]> | string; 19 20 onclick?: () => Promise<void>; 21 + "aria-label"?: string; 20 22 disabled?: boolean; 21 23 loading?: boolean; 22 24 } = $props(); 23 25 </script> 24 26 25 - <button class="full {style}" disabled={loading || disabled} {onclick}> 27 + <button class="full {style}" disabled={loading || disabled} {onclick} aria-label={ariaLabel}> 26 28 {#if loading} 27 29 <span class="loader"></span> 28 30 {/if}
+3 -1
packages/panel/src/lib/component/PromiseButton.svelte
··· 27 27 import LoaderButton from "./LoaderButton.svelte"; 28 28 29 29 let { 30 + "aria-label": ariaLabel, 30 31 onclick, 31 32 children, 32 33 disabled, ··· 36 37 style?: ComponentProps<typeof LoaderButton>["style"]; 37 38 children: Snippet<[boolean]> | string; 38 39 onclick: () => Promise<any>; 40 + "aria-label"?: string; 39 41 disabled?: boolean; 40 42 loading?: boolean; 41 43 } = $props(); ··· 52 54 } 53 55 </script> 54 56 55 - <LoaderButton onclick={handleClick} {disabled} {style} {children} loading={waiting || loading} /> 57 + <LoaderButton onclick={handleClick} {disabled} {style} {children} loading={waiting || loading} aria-label={ariaLabel} />
+11 -15
packages/panel/src/lib/component/Tabs.svelte
··· 8 8 --> 9 9 10 10 <script lang="ts"> 11 + import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; 11 12 import type { IconDefinition } from "@fortawesome/fontawesome-common-types"; 12 - import { Accordion } from "melt/builders"; 13 + import { isMobile } from "../../routes/+layout.svelte"; 13 14 import { fade, slide } from "svelte/transition"; 15 + import { Accordion } from "melt/builders"; 14 16 import type { Snippet } from "svelte"; 15 17 import Fa from "svelte-fa"; 16 - import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; 17 18 18 19 let { tabs }: { tabs: { label: string; icon: IconDefinition; content: Snippet<[]> }[] } = $props(); 19 20 20 21 const accordion = new Accordion(); 21 22 22 - let windowWidth = $state(0); 23 - let mobile = $derived(windowWidth <= 425); 24 - 25 23 $effect(() => { 26 - if (accordion.value === undefined && !mobile) { 24 + if (accordion.value === undefined && !isMobile()) { 27 25 accordion.value = tabs[0].label; 28 26 } 29 27 }); 30 28 </script> 31 29 32 - <svelte:window bind:innerWidth={windowWidth} /> 33 - 34 30 <div {...accordion.root}> 35 31 <div 36 - class:container={!mobile} 37 - class:flexc={mobile} 32 + class:container={!isMobile()} 33 + class:flexc={isMobile()} 38 34 class="flexr gap-10" 39 - style:width={mobile ? "100%" : "fit-content"} 35 + style:width={isMobile() ? "100%" : "fit-content"} 40 36 > 41 37 {#each tabs as tab (tab.label)} 42 38 {@const item = accordion.getItem({ id: tab.label })} 43 - <div class:container={mobile}> 39 + <div class:container={isMobile()}> 44 40 <button 45 41 class="a switch" 46 42 {...item.trigger} ··· 50 46 <Fa icon={tab.icon} translateY="0.1" /> 51 47 <span {...item.heading}>{tab.label}</span> 52 48 53 - {#if mobile} 49 + {#if isMobile()} 54 50 <Fa icon={item.isExpanded ? faChevronUp : faChevronDown} style="margin-left: auto;" /> 55 51 {/if} 56 52 </button> 57 - {#if mobile} 53 + {#if isMobile()} 58 54 {#if item.isExpanded} 59 55 <div style="overflow: hidden;" transition:slide={{ duration: 250 }}> 60 56 <hr style="margin-top: 15px;" /> ··· 68 64 {/each} 69 65 </div> 70 66 71 - {#if !mobile} 67 + {#if !isMobile()} 72 68 <div class="container"> 73 69 {#each tabs as tab (tab.label)} 74 70 {@const item = accordion.getItem({ id: tab.label })}
+72
packages/panel/src/lib/component/Tooltip.svelte
··· 1 + <script lang="ts"> 2 + import { Tooltip } from "melt/builders"; 3 + import type { Snippet } from "svelte"; 4 + 5 + type Placement = NonNullable<NonNullable<NonNullable<Tooltip["floatingConfig"]>["computePosition"]>["placement"]>; 6 + 7 + let { 8 + children, 9 + content, 10 + placement = "top", 11 + }: { content: string; children: Snippet<[]>; placement?: Placement } = $props(); 12 + 13 + const tooltip = new Tooltip({ 14 + openDelay: 0, 15 + disableHoverableContent: true, 16 + // svelte-ignore state_referenced_locally 17 + floatingConfig: { computePosition: { placement } }, 18 + }); 19 + </script> 20 + 21 + <div class="trigger" {...tooltip.trigger}> 22 + {@render children()} 23 + </div> 24 + 25 + <div {...tooltip.content} class="tooltip"> 26 + <div class="arrow" {...tooltip.arrow}></div> 27 + <span>{content}</span> 28 + </div> 29 + 30 + <style lang="scss"> 31 + .trigger { 32 + width: fit-content; 33 + cursor: help; 34 + } 35 + 36 + .tooltip { 37 + position: relative; 38 + background-color: var(--overlay0); 39 + box-shadow: 0 -2px 10px #00000080; 40 + color: inherit; 41 + 42 + border-radius: 5px; 43 + border: none; 44 + 45 + padding: 5px; 46 + margin: 0; 47 + } 48 + 49 + .arrow { 50 + position: absolute; 51 + width: 8px; 52 + height: 8px; 53 + background-color: var(--overlay2); 54 + transform: rotate(45deg); 55 + } 56 + 57 + .tooltip[data-side="top"] .arrow { 58 + bottom: -4px; 59 + } 60 + 61 + .tooltip[data-side="bottom"] .arrow { 62 + top: -4px; 63 + } 64 + 65 + .tooltip[data-side="left"] .arrow { 66 + right: -4px; 67 + } 68 + 69 + .tooltip[data-side="right"] .arrow { 70 + left: -4px; 71 + } 72 + </style>
+2
packages/panel/src/lib/index.ts
··· 6 6 7 7 export * as api from "./api"; 8 8 9 + export { isMobile } from "../routes/+layout.svelte"; 10 + 9 11 export function trim(str: string, maxLength: number) { 10 12 if (str.length <= maxLength) return str; 11 13 return str.slice(0, maxLength - 3) + "...";
+7 -1
packages/panel/src/lib/style.scss
··· 87 87 88 88 &.outline { 89 89 background-color: transparent; 90 - border: 1px solid var(--subtext0); 91 90 color: inherit; 91 + 92 + transition: border-color 250ms; 93 + border: 1px solid var(--subtext0); 94 + 95 + &:hover { 96 + border-color: var(--mauve); 97 + } 92 98 } 93 99 } 94 100
+4 -8
packages/panel/src/routes/(authenticated)/Navbar.svelte
··· 6 6 --> 7 7 8 8 <script lang="ts"> 9 + import { onNavigate } from "$app/navigation"; 10 + import { slide } from "svelte/transition"; 9 11 import { page } from "$app/state"; 12 + import { isMobile } from "$lib"; 10 13 import Fa from "svelte-fa"; 11 14 import { 12 15 faBars, ··· 17 20 faMagnifyingGlass, 18 21 faXmark, 19 22 } from "@fortawesome/free-solid-svg-icons"; 20 - import { slide } from "svelte/transition"; 21 - import { onNavigate } from "$app/navigation"; 22 23 23 24 const EXPANDED_KEY = "luminary-navbar-expanded"; 24 25 ··· 32 33 let open = $state(false); 33 34 34 35 let navbarWidth = $state(0); 35 - let windowWidth = $state(0); 36 - 37 - let mobile = $derived(windowWidth <= 425); 38 36 39 37 function toggleExpanded() { 40 38 expanded = !expanded; ··· 50 48 }); 51 49 </script> 52 50 53 - <svelte:window bind:innerWidth={windowWidth} /> 54 - 55 51 {#snippet links()} 56 52 {#each PAGES as { icon, label, href }} 57 53 <a class="entry" {href} class:current={page.url.pathname.startsWith(href)}> ··· 63 59 {/each} 64 60 {/snippet} 65 61 66 - {#if mobile} 62 + {#if isMobile()} 67 63 <div style:min-height="48px"></div> 68 64 69 65 <nav class:open>
+5 -2
packages/panel/src/routes/(authenticated)/projects/[project]/+page.svelte
··· 6 6 import StatusTab from "./ProjectStatus.svelte"; 7 7 import { getProjects } from "$lib/api"; 8 8 import { page } from "$app/state"; 9 + import { isMobile } from "$lib"; 9 10 import Fa from "svelte-fa"; 10 11 11 12 let project = $derived(getProjects()[page.params.project!]); ··· 36 37 37 38 {#snippet status()} 38 39 <StatusTab {project} /> 39 - <h2>Logs</h2> 40 - <LogTerminal project={project.name} /> 40 + {#if !isMobile()} 41 + <h2>Logs</h2> 42 + <LogTerminal project={project.name} /> 43 + {/if} 41 44 {/snippet} 42 45 43 46 {#snippet compose()}
+67 -53
packages/panel/src/routes/(authenticated)/projects/[project]/ProjectStatus.svelte
··· 4 4 import Fa from "svelte-fa"; 5 5 import { api } from "$lib"; 6 6 import StatusIcon from "$lib/component/StatusIcon.svelte"; 7 + import Tooltip from "$lib/component/Tooltip.svelte"; 7 8 8 9 let { project }: { project: api.LuminaryProject } = $props(); 9 10 ··· 71 72 style="outline" 72 73 disabled={project.busy} 73 74 onclick={() => 74 - api.client.POST("/api/project/{project}/redeploy", { params: { path: { project: project.name } } })} 75 + api.client.POST("/api/project/{project}/recreate", { params: { path: { project: project.name } } })} 75 76 > 76 77 {#snippet children(loading)} 77 78 <div class="flexr center gap-10"> 78 79 {#if !loading}<Fa icon={faRocket} />{/if} 79 - Redeploy All 80 + Recreate All 80 81 </div> 81 82 {/snippet} 82 83 </PromiseButton> ··· 97 98 </div> 98 99 99 100 <div class="flexr center gap-5"> 100 - <PromiseButton 101 - style="a" 102 - disabled={service.action !== "idle"} 103 - loading={service.action === "starting"} 104 - onclick={() => 105 - api.client.POST("/api/project/{project}/service/{service}/start", { 106 - params: { path: { project: project.name, service: service.serviceName } }, 107 - })} 108 - > 109 - {#snippet children(loading)} 110 - {#if !loading}<Fa icon={faPlay} />{/if} 111 - {/snippet} 112 - </PromiseButton> 113 - <PromiseButton 114 - style="a" 115 - disabled={service.action !== "idle"} 116 - loading={service.action === "restarting"} 117 - onclick={() => 118 - api.client.POST("/api/project/{project}/service/{service}/restart", { 119 - params: { path: { project: project.name, service: service.serviceName } }, 120 - })} 121 - > 122 - {#snippet children(loading)} 123 - {#if !loading}<Fa icon={faArrowsRotate} />{/if} 124 - {/snippet} 125 - </PromiseButton> 101 + <Tooltip placement="left" content="Start Service"> 102 + <PromiseButton 103 + style="a" 104 + aria-label="Start Service" 105 + disabled={service.action !== "idle"} 106 + loading={service.action === "starting"} 107 + onclick={() => 108 + api.client.POST("/api/project/{project}/service/{service}/start", { 109 + params: { path: { project: project.name, service: service.serviceName } }, 110 + })} 111 + > 112 + {#snippet children(loading)} 113 + {#if !loading}<Fa icon={faPlay} />{/if} 114 + {/snippet} 115 + </PromiseButton> 116 + </Tooltip> 126 117 127 - <PromiseButton 128 - style="a" 129 - disabled={service.action !== "idle"} 130 - loading={service.action === "stopping"} 131 - onclick={() => 132 - api.client.POST("/api/project/{project}/service/{service}/stop", { 133 - params: { path: { project: project.name, service: service.serviceName } }, 134 - })} 135 - > 136 - {#snippet children(loading)} 137 - {#if !loading}<Fa icon={faStop} />{/if} 138 - {/snippet} 139 - </PromiseButton> 118 + <Tooltip placement="left" content="Restart Service"> 119 + <PromiseButton 120 + style="a" 121 + aria-label="Restart Service" 122 + disabled={service.action !== "idle"} 123 + loading={service.action === "restarting"} 124 + onclick={() => 125 + api.client.POST("/api/project/{project}/service/{service}/restart", { 126 + params: { path: { project: project.name, service: service.serviceName } }, 127 + })} 128 + > 129 + {#snippet children(loading)} 130 + {#if !loading}<Fa icon={faArrowsRotate} />{/if} 131 + {/snippet} 132 + </PromiseButton> 133 + </Tooltip> 140 134 141 - <PromiseButton 142 - style="a" 143 - disabled={service.action !== "idle"} 144 - onclick={() => 145 - api.client.POST("/api/project/{project}/service/{service}/redeploy", { 146 - params: { path: { project: project.name, service: service.serviceName } }, 147 - })} 148 - > 149 - {#snippet children(loading)} 150 - {#if !loading}<Fa icon={faRocket} />{/if} 151 - {/snippet} 152 - </PromiseButton> 135 + <Tooltip placement="left" content="Stop Service"> 136 + <PromiseButton 137 + style="a" 138 + aria-label="Stop Service" 139 + disabled={service.action !== "idle"} 140 + loading={service.action === "stopping"} 141 + onclick={() => 142 + api.client.POST("/api/project/{project}/service/{service}/stop", { 143 + params: { path: { project: project.name, service: service.serviceName } }, 144 + })} 145 + > 146 + {#snippet children(loading)} 147 + {#if !loading}<Fa icon={faStop} />{/if} 148 + {/snippet} 149 + </PromiseButton> 150 + </Tooltip> 151 + 152 + <Tooltip placement="left" content="Recreate Service"> 153 + <PromiseButton 154 + style="a" 155 + aria-label="Recreate Service" 156 + disabled={service.action !== "idle"} 157 + onclick={() => 158 + api.client.POST("/api/project/{project}/service/{service}/recreate", { 159 + params: { path: { project: project.name, service: service.serviceName } }, 160 + })} 161 + > 162 + {#snippet children(loading)} 163 + {#if !loading}<Fa icon={faRocket} />{/if} 164 + {/snippet} 165 + </PromiseButton> 166 + </Tooltip> 153 167 </div> 154 168 </div> 155 169 {/each}
+15
packages/panel/src/routes/+layout.svelte
··· 1 + <script lang="ts" module> 2 + let windowWidth = $state(0); 3 + let mobile = $derived(windowWidth <= 425); 4 + 5 + /** 6 + * @returns true if the screen is small enough to be considered a mobile device. 7 + */ 8 + export const isMobile = () => mobile; 9 + </script> 10 + 1 11 <script lang="ts"> 2 12 import "@fontsource-variable/open-sans"; 3 13 import "@fontsource/dejavu-mono"; ··· 5 15 6 16 import Toaster from "./Toaster.svelte"; 7 17 import Dialog from "./Dialog.svelte"; 18 + import { AnimationFrames } from "runed"; 8 19 9 20 let { children } = $props(); 21 + 22 + new AnimationFrames(() => { 23 + windowWidth = window.innerWidth; 24 + }); 10 25 </script> 11 26 12 27 <Toaster />
+3 -3
yaak/yaak.rq_Dwcf7c4335.yaml
··· 9 9 authenticationType: null 10 10 body: {} 11 11 bodyType: null 12 - description: '' 12 + description: "" 13 13 headers: [] 14 14 method: POST 15 - name: Redeploy 15 + name: Recreate 16 16 sortPriority: 5000.0 17 - url: ${[ BASE_URL ]}/project/${[ PROJECT_NAME ]}/redeploy 17 + url: ${[ BASE_URL ]}/project/${[ PROJECT_NAME ]}/recreate 18 18 urlParameters: []
+3 -3
yaak/yaak.rq_GzNcq2wE8i.yaml
··· 9 9 authenticationType: null 10 10 body: {} 11 11 bodyType: null 12 - description: '' 12 + description: "" 13 13 headers: [] 14 14 method: POST 15 - name: Redeploy 15 + name: Recreate 16 16 sortPriority: 1000.003 17 - url: ${[ BASE_URL ]}/project/${[ PROJECT_NAME ]}/service/${[ SERVICE_NAME ]}/redeploy 17 + url: ${[ BASE_URL ]}/project/${[ PROJECT_NAME ]}/service/${[ SERVICE_NAME ]}/recreate 18 18 urlParameters: []