mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
feat(ui): rework dashboard columns and polish the deploy canvas
- Dashboard: projects and servers fill the left, recent deployments sit in a fixed-width right column; cards stretch to fill rows; the server table shows live CPU and memory usage when metrics are on. - Deploy canvas: purple accent in dark mode, the Steps panel floats over the canvas, and both canvases use the flask beta icon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9db8d55473
commit
2a295d573b
7 files changed
+92
-24
No files matched your search
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
// The flask icon of the Blade <x-beta-badge> component, so canvases mark beta the same way.
|
||||
</script>
|
||||
|
||||
<span role="img" aria-label="Beta" title="Beta" class="inline-flex shrink-0 items-center justify-center rounded-full p-0.5 text-coollabs cursor-help dark:text-brand-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -4,6 +4,7 @@
|
||||
import Dropdown from './Dropdown.svelte';
|
||||
import Edges from './Edges.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import BetaBadge from '../BetaBadge.svelte';
|
||||
import Node from './Node.svelte';
|
||||
import PaletteItem from './PaletteItem.svelte';
|
||||
import RunPanel from './RunPanel.svelte';
|
||||
@@ -82,7 +83,7 @@
|
||||
<!-- Opened from an environment's New resource page: back to that environment. -->
|
||||
<a class="cv-back button px-2!" href="/project/{cv.target.project}/environment/{cv.target.environment}" title="Back to the environment" aria-label="Back to the environment"><Icon name="back" cls="size-4" /></a>
|
||||
{/if}
|
||||
<span class="shrink-0 rounded-full border border-coollabs/30 bg-coollabs/10 px-2 py-0.5 text-xs font-medium text-coollabs dark:border-warning/30 dark:bg-warning/15 dark:text-warning">Beta</span>
|
||||
<BetaBadge />
|
||||
</div>
|
||||
<button type="button" class="button shrink-0" title="Start again with an empty canvas" onclick={askReset}>Reset</button>
|
||||
{/snippet}
|
||||
|
||||
@@ -121,6 +121,15 @@ const height = n => els.nodeEls.get(n.id)?.offsetHeight || 160;
|
||||
// A card's box in world units (its rendered size, or a usual card size).
|
||||
const boxOf = n => ({ x: n.x, y: n.y, w: width(n), h: height(n) });
|
||||
|
||||
// The part of the viewport that the floating Steps panel does not cover,
|
||||
// relative to the viewport (on small screens the panel is above it).
|
||||
function openArea() {
|
||||
const r = els.viewport.getBoundingClientRect();
|
||||
const p = els.root.querySelector('.cv-palette').getBoundingClientRect();
|
||||
const left = p.bottom > r.top ? Math.max(0, p.right - r.left) : 0;
|
||||
return { left, width: r.width - left, height: r.height };
|
||||
}
|
||||
|
||||
export function fit() {
|
||||
if (!els.nodeEls.size) return;
|
||||
let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity;
|
||||
@@ -136,11 +145,11 @@ export function fit() {
|
||||
maxX = Math.max(maxX, pill.offsetLeft + pill.offsetWidth);
|
||||
maxY = Math.max(maxY, pill.offsetTop + pill.offsetHeight);
|
||||
}
|
||||
const r = els.viewport.getBoundingClientRect();
|
||||
const r = openArea();
|
||||
const pad = 60;
|
||||
const view = cv.view;
|
||||
view.z = Math.min(1.2, Math.max(MIN_ZOOM, Math.min((r.width - pad * 2) / (maxX - minX), (r.height - pad * 2) / (maxY - minY))));
|
||||
view.x = (r.width - (maxX - minX) * view.z) / 2 - minX * view.z;
|
||||
view.x = r.left + (r.width - (maxX - minX) * view.z) / 2 - minX * view.z;
|
||||
view.y = (r.height - (maxY - minY) * view.z) / 2 - minY * view.z;
|
||||
els.pz.apply();
|
||||
}
|
||||
@@ -259,7 +268,8 @@ function appendToChain(type) {
|
||||
}
|
||||
function centre() {
|
||||
const r = els.viewport.getBoundingClientRect();
|
||||
const p = toWorld(r.left + r.width / 2, r.top + r.height / 2);
|
||||
const a = openArea();
|
||||
const p = toWorld(r.left + a.left + a.width / 2, r.top + r.height / 2);
|
||||
return [p.x - 140, p.y - 60];
|
||||
}
|
||||
|
||||
@@ -299,6 +309,7 @@ export function dragFromPalette(type, event) {
|
||||
ghost.remove();
|
||||
const r = els.viewport.getBoundingClientRect();
|
||||
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return;
|
||||
if (e.target.closest?.('.cv-palette')) return;
|
||||
const p = toWorld(e.clientX, e.clientY);
|
||||
addNode(type, p.x - 30, p.y - 16);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@ const CSS = `
|
||||
--cv-line: var(--coollabs-hairline, light-dark(#e5e5e5, rgb(255 255 255 / 0.08)));
|
||||
--cv-input-bg: light-dark(#fafafa, rgb(255 255 255 / 0.04));
|
||||
--cv-input-line: light-dark(#e5e5e5, rgb(255 255 255 / 0.08));
|
||||
--cv-brand: light-dark(var(--color-coollabs), var(--color-warning));
|
||||
--cv-brand-text: var(--cv-brand);
|
||||
--cv-brand: light-dark(var(--color-coollabs), var(--color-brand));
|
||||
--cv-brand-text: light-dark(var(--color-coollabs), var(--color-brand-light));
|
||||
position: absolute; inset: 0; z-index: 10;
|
||||
display: grid; grid-template-columns: 15.5rem minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 8px; padding: 8px;
|
||||
@@ -100,6 +100,13 @@ const CSS = `
|
||||
|
||||
/* Canvas island */
|
||||
.cv-stage { position: relative; overflow: hidden; border: 0; background: transparent; }
|
||||
/* Wide screens: the canvas fills the whole area to the screen edges and the
|
||||
Steps panel floats over it, so cards slide under the panel, not cut off. */
|
||||
@media (min-width: 768px) {
|
||||
.cv-stage { grid-column: 1 / -1; grid-row: -2 / -1; margin: 0 -8px -8px; }
|
||||
.cv-docked .cv-stage { margin-top: -8px; }
|
||||
.cv-palette { grid-column: 1; grid-row: -2 / -1; position: relative; z-index: 15; box-shadow: 0 16px 40px -20px rgb(0 0 0 / 0.35); }
|
||||
}
|
||||
.cv-viewport {
|
||||
position: absolute; inset: 0; cursor: grab; touch-action: none;
|
||||
/* The dots are on .cv-root, so the viewport has none of its own. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import BetaBadge from '../BetaBadge.svelte';
|
||||
// Filters and view toggles. The root docks it (use:dock) into the zoom controls bar
|
||||
// (CanvasControls, bottom left) on desktop, or in a rounded box above the canvas on small screens.
|
||||
|
||||
@@ -17,9 +18,7 @@
|
||||
|
||||
<div use:dock data-ec-toolbar>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="inline-flex items-center">
|
||||
<span class="px-2 py-0.5 text-xs font-medium leading-normal rounded-full bg-coollabs/10 text-coollabs border border-coollabs/30 dark:bg-warning/15 dark:text-warning dark:border-warning/30">Beta</span>
|
||||
</span>
|
||||
<BetaBadge />
|
||||
<span class="rounded-full border border-neutral-200 bg-neutral-100 px-2 py-0.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-fg-dim" class:hidden={!viewOnly} title={viewOnlyTitle}>View only</span>
|
||||
</div>
|
||||
<!-- Small screens: the actions wrap onto more rows instead of covering the title. -->
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Wide screens: activity takes the left third, projects and servers the other two thirds.
|
||||
It stays one column while the left side has no section (no deployments, no traffic). --}}
|
||||
<div class="grid min-w-0 gap-8 xl:items-start xl:has-[.dashboard-activity_section]:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]">
|
||||
<div class="dashboard-activity hidden min-w-0 has-[section]:flex flex-col gap-8">
|
||||
{{-- Wide screens: projects and servers fill the left, activity is a fixed-width column on the right.
|
||||
It stays one column while the activity side has no section (no deployments, no traffic). --}}
|
||||
<div class="grid min-w-0 gap-8 xl:items-start xl:has-[.dashboard-activity_section]:grid-cols-[minmax(0,1fr)_22rem] 2xl:has-[.dashboard-activity_section]:grid-cols-[minmax(0,1fr)_26rem]">
|
||||
<div class="dashboard-activity hidden min-w-0 has-[section]:flex flex-col gap-8 xl:order-last">
|
||||
<livewire:dashboard.active-deployments />
|
||||
|
||||
@if ($hasTrafficAnalytics)
|
||||
@@ -112,7 +112,7 @@
|
||||
icon-name="projects" size="sm" />
|
||||
@else
|
||||
<div x-cloak x-show="viewMode === 'grid'"
|
||||
class="grid min-w-0 grid-cols-1 gap-3 @lg:grid-cols-2 @4xl:grid-cols-4">
|
||||
class="grid min-w-0 grid-cols-[repeat(auto-fit,minmax(min(17rem,100%),1fr))] gap-3">
|
||||
@foreach ($dashboardProjects as $project)
|
||||
@php
|
||||
$firstEnvironment = $project->environments->first();
|
||||
@@ -315,7 +315,7 @@
|
||||
@endif
|
||||
@else
|
||||
<div x-cloak x-show="viewMode === 'grid'"
|
||||
class="grid min-w-0 grid-cols-1 gap-3 @lg:grid-cols-2 @4xl:grid-cols-4">
|
||||
class="grid min-w-0 grid-cols-[repeat(auto-fit,minmax(min(17rem,100%),1fr))] gap-3">
|
||||
@foreach ($dashboardServers as $server)
|
||||
@php
|
||||
[$serverStatus, $serverStatusType] = $serverStatuses[$server->id];
|
||||
@@ -360,10 +360,20 @@
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@php
|
||||
// A Usage column (latest CPU and memory) only when a listed server collects metrics.
|
||||
$showServerUsage = $dashboardServers->contains(fn ($server) => $server->isMetricsEnabled());
|
||||
$serverTableColumns = $showServerUsage
|
||||
? 'grid-cols-[minmax(0,1fr)_9.5rem] @xl:grid-cols-[minmax(0,1fr)_9rem_9.5rem] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9rem_9.5rem]'
|
||||
: 'grid-cols-[minmax(0,1fr)_9.5rem] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9.5rem]';
|
||||
@endphp
|
||||
<div x-show="viewMode === 'table'" class="{{ $tableShell }}">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_9.5rem] gap-4 @2xl:min-w-[480px] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9.5rem] {{ $tableHead }}">
|
||||
<div class="grid {{ $serverTableColumns }} gap-4 @2xl:min-w-[480px] {{ $tableHead }}">
|
||||
<div>Server</div>
|
||||
<div class="hidden @2xl:block">Description</div>
|
||||
@if ($showServerUsage)
|
||||
<div class="hidden @xl:block">Usage</div>
|
||||
@endif
|
||||
<div>Status</div>
|
||||
</div>
|
||||
@foreach ($dashboardServers as $server)
|
||||
@@ -372,7 +382,7 @@
|
||||
@endphp
|
||||
<a href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}
|
||||
wire:key="dashboard-server-row-{{ $server->uuid }}"
|
||||
class="grid grid-cols-[minmax(0,1fr)_9.5rem] gap-4 @2xl:min-w-[480px] @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_9.5rem] hover:no-underline {{ $tableRow }}">
|
||||
class="grid {{ $serverTableColumns }} gap-4 @2xl:min-w-[480px] hover:no-underline {{ $tableRow }}">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="resource-glyph flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500">
|
||||
@@ -385,6 +395,43 @@
|
||||
<p class="hidden truncate text-neutral-500 @2xl:block dark:text-fg-dim">
|
||||
{{ filled($server->description) ? $server->description : '-' }}
|
||||
</p>
|
||||
@if ($showServerUsage)
|
||||
<div class="hidden @xl:block">
|
||||
@if ($server->isMetricsEnabled())
|
||||
{{-- The latest values from the grid card's chart (server-metrics-chart), so the table sends no requests of its own. --}}
|
||||
<div class="flex flex-col gap-1 text-[11px] text-neutral-500 tabular-nums dark:text-fg-dim"
|
||||
x-data="{
|
||||
cpu: null,
|
||||
memory: null,
|
||||
stop: null,
|
||||
init() {
|
||||
const latest = points => points?.length ? points[points.length - 1][1] : null;
|
||||
this.stop = Livewire.on(@js('dashboard-server-metrics-'.$server->uuid), data => {
|
||||
this.cpu = latest(data[0]?.cpu);
|
||||
this.memory = latest(data[0]?.memory);
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
this.stop?.();
|
||||
},
|
||||
}">
|
||||
@foreach (['cpu' => ['CPU', 'bg-[rgb(30,144,255)]'], 'memory' => ['Mem', 'bg-[rgb(168,85,247)]']] as $metric => [$metricLabel, $metricColor])
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-7 shrink-0">{{ $metricLabel }}</span>
|
||||
<span class="h-1 min-w-0 flex-1 overflow-hidden rounded-full bg-neutral-200 dark:bg-white/[0.08]">
|
||||
<span class="block h-full rounded-full {{ $metricColor }} transition-[width] duration-500"
|
||||
:style="{ width: `${Math.min(100, {{ $metric }} ?? 0)}%` }"></span>
|
||||
</span>
|
||||
<span class="w-8 shrink-0 text-right"
|
||||
x-text="{{ $metric }} === null ? '-' : `${Math.round({{ $metric }})}%`">-</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<span class="text-neutral-500 dark:text-fg-dim">-</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-[11px] font-medium text-neutral-600 dark:text-fg-dim">
|
||||
<span @class([
|
||||
'size-2 shrink-0 rounded-full',
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
@if ($hasAnyDeployments)
|
||||
{{-- Container breakpoints: the table needs room, which the dashboard column may not have. --}}
|
||||
<section class="@container mb-0! min-w-0">
|
||||
<x-section-heading title="Deployments" class="py-1.5" />
|
||||
<x-section-heading :title="$hasRecentDeployments ? 'Recent deployments' : 'Deployments'" class="py-1.5" />
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-4">
|
||||
@if ($hasActiveDeployments)
|
||||
@@ -103,12 +103,6 @@
|
||||
@if ($hasRecentDeployments)
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
||||
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
|
||||
<h3 class="text-[11px]! leading-4! font-medium! text-neutral-500! dark:text-fg-faint!">
|
||||
Recent deployments
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="dashboard-deployment-table-grid hidden items-center gap-4 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 @2xl:grid dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<span>Application</span>
|
||||
|
||||
Reference in new issue
Block a user