mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 18:18:29 +00:00
feat(dashboard): add server metrics charts and polish UI feedback
This commit is contained in:
1 parent
2949b8e158
commit
f412b2bf0d
24 files changed
+493
-104
No files matched your search
@@ -14,12 +14,18 @@ test('custom color theme is available and applied across theme controls', functi
|
||||
->toContain("['value' => 'custom', 'label' => 'Custom'")
|
||||
->toContain('type="color"')
|
||||
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
|
||||
->toContain("localStorage.setItem('themeColor', this.themeColor)")
|
||||
->toContain('requestAnimationFrame(() =>')
|
||||
->toContain('@input="previewThemeColor($event.target.value)"')
|
||||
->toContain('@change="saveThemeColor($event.target.value)"')
|
||||
->toContain("localStorage.setItem('themeColor', color)")
|
||||
->toContain("this.theme === 'custom'")
|
||||
->and($accountMenu)
|
||||
->toContain("['value' => 'custom', 'label' => 'Custom']")
|
||||
->toContain('aria-label="Custom theme color"')
|
||||
->toContain("localStorage.setItem('themeColor', this.themeColor)")
|
||||
->toContain('requestAnimationFrame(() =>')
|
||||
->toContain('@input="previewThemeColor($event.target.value)"')
|
||||
->toContain('@change="saveThemeColor($event.target.value)"')
|
||||
->toContain("localStorage.setItem('themeColor', color)")
|
||||
->toContain('absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0')
|
||||
->and($layout)
|
||||
->toContain("t === 'custom'")
|
||||
|
||||
@@ -53,6 +53,16 @@ function createDashboardDeployment(array $overrides = []): ApplicationDeployment
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
it('shows a visible yellow inset focus state on deployment rows', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/dashboard/active-deployments.blade.php'));
|
||||
|
||||
expect(substr_count($view, 'focus-visible:bg-warning/[0.06]'))->toBe(4)
|
||||
->and(substr_count($view, 'focus-visible:ring-inset'))->toBe(2)
|
||||
->and(substr_count($view, 'focus-visible:ring-warning/70'))->toBe(2)
|
||||
->and(substr_count($view, 'focus-visible:ring-offset-0'))->toBe(2)
|
||||
->and(substr_count($view, 'dark:focus-visible:bg-warning/[0.06]'))->toBe(2);
|
||||
});
|
||||
|
||||
it('hides the deployments section when there is nothing to show', function () {
|
||||
Livewire::test(ActiveDeployments::class)
|
||||
->assertDontSee('Active and recent deployment activity')
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
|
||||
$user = User::factory()->create();
|
||||
$team = Team::factory()->create();
|
||||
$user->teams()->attach($team, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
$this->privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
$this->team = $team;
|
||||
});
|
||||
|
||||
it('renders a metrics chart only for servers with metrics enabled', function () {
|
||||
$enabledServer = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
]);
|
||||
$enabledServer->settings->update(['is_metrics_enabled' => true]);
|
||||
|
||||
$disabledServer = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
]);
|
||||
$disabledServer->settings->update(['is_metrics_enabled' => false]);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertSeeHtml("dashboard-server-metrics-{$enabledServer->uuid}")
|
||||
->assertDontSeeHtml("dashboard-server-metrics-{$disabledServer->uuid}");
|
||||
});
|
||||
|
||||
it('configures the dashboard chart as a ten minute cpu and memory sparkline with hover details', function () {
|
||||
$chart = file_get_contents(resource_path('views/livewire/dashboard/server-metrics-chart.blade.php'));
|
||||
$component = file_get_contents(app_path('Livewire/Dashboard/ServerMetricsChart.php'));
|
||||
|
||||
expect($component)
|
||||
->toContain('$this->server->getCpuMetrics(10)')
|
||||
->toContain('$this->server->getMemoryMetrics(10)');
|
||||
expect($chart)
|
||||
->toContain('new ApexCharts')
|
||||
->toContain('sparkline: { enabled: true }')
|
||||
->toContain('absolute right-0 bottom-0 h-2/3 w-full')
|
||||
->toContain('[&_.apexcharts-svg]:overflow-hidden')
|
||||
->not->toContain('w-full overflow-hidden rounded-b-xl')
|
||||
->toContain('CPU:')
|
||||
->toContain('Memory:')
|
||||
->toContain('formatTimestamp(timestamp)')
|
||||
->toContain("min: 0,\n max: 100,")
|
||||
->toContain('labels: { show: false }');
|
||||
});
|
||||
|
||||
it('keeps the status badge only on server cards without metrics', function () {
|
||||
$dashboard = file_get_contents(resource_path('views/livewire/dashboard.blade.php'));
|
||||
|
||||
expect($dashboard)->toContain('@unless ($server->isMetricsEnabled())');
|
||||
});
|
||||
@@ -7,10 +7,31 @@ it('uses the custom tooltip for icon actions across layouts', function () {
|
||||
expect($baseLayout)->toContain('<x-icon-tooltip />')
|
||||
->and($tooltip)
|
||||
->toContain("closest('button, a, [data-tooltip]')")
|
||||
->toContain("target.querySelector('svg')")
|
||||
->toContain("target.matches('[data-tooltip], .icon-button')")
|
||||
->toContain("target.hasAttribute('aria-label')")
|
||||
->toContain('target.childElementCount === 1')
|
||||
->toContain("target.firstElementChild?.matches('svg')")
|
||||
->not->toContain("target.querySelector('svg')")
|
||||
->toContain("target.matches('[data-icon-tooltip-ignore]')")
|
||||
->toContain("target.removeAttribute('title')")
|
||||
->toContain('role="tooltip"')
|
||||
->toContain('aria-label')
|
||||
->toContain('fixed z-[100]');
|
||||
});
|
||||
|
||||
it('does not reposition an already active tooltip during nested mouseover events', function () {
|
||||
$tooltip = file_get_contents(resource_path('views/components/icon-tooltip.blade.php'));
|
||||
|
||||
expect($tooltip)
|
||||
->toContain('if (target === this.activeTarget && this.visible) return;');
|
||||
});
|
||||
|
||||
it('keeps a tooltip hidden until its measured position is applied', function () {
|
||||
$tooltip = file_get_contents(resource_path('views/components/icon-tooltip.blade.php'));
|
||||
|
||||
expect($tooltip)
|
||||
->toContain('positioned: false')
|
||||
->toContain('this.positioned = false;')
|
||||
->toContain('this.$nextTick(() => this.positioned = true);')
|
||||
->toContain("positioned ? 'visible' : 'invisible'");
|
||||
});
|
||||
@@ -19,7 +19,7 @@ it('uses a single Alpine result renderer for every command palette result type',
|
||||
expect($view)
|
||||
->not->toContain('Create mode (server-rendered path)')
|
||||
->not->toContain('!$wire.isCreateMode')
|
||||
->toContain("<!-- Command palette -->\n <div x-show=\"modalOpen\"")
|
||||
->toContain("<!-- Command palette -->\n <div x-cloak")
|
||||
->not->toContain("<!-- Command palette -->\n <template x-teleport=\"body\">")
|
||||
->toContain("<div wire:ignore>\n <template x-if=\"searchQuery.length")
|
||||
->toContain('x-for="(result, index) in searchResults"')
|
||||
@@ -32,3 +32,29 @@ it('skips hidden command palette results during keyboard navigation', function (
|
||||
expect($view)
|
||||
->toContain('filter(item => item.offsetParent !== null)');
|
||||
});
|
||||
|
||||
it('opens the command palette without changing page scrollbar visibility', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain("document.body.style.overflow = value ? 'hidden' : ''")
|
||||
->not->toContain("document.body.style.overflow = ''");
|
||||
});
|
||||
|
||||
it('animates the command palette with tw animate utilities', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('<div x-show="modalOpen" @click="closeModal()"')
|
||||
->toContain('x-transition:enter="animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-150"')
|
||||
->toContain('x-transition:leave="animate-out fade-out-0 zoom-out-95 slide-out-to-top-2 duration-100"')
|
||||
->not->toContain('<div x-show="modalOpen" x-cloak\n class="fixed inset-0');
|
||||
});
|
||||
|
||||
it('closes the client-side command palette without a Livewire request', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain('$wire.closeSearchModal()')
|
||||
->not->toContain('closeTimer');
|
||||
});
|
||||
@@ -1,25 +1,20 @@
|
||||
<?php
|
||||
|
||||
test('loading indicators use yellow throughout dark mode', function () {
|
||||
test('loading indicators use coollabs purple throughout dark mode', function () {
|
||||
$utilities = file_get_contents(resource_path('css/utilities.css'));
|
||||
$appCss = file_get_contents(resource_path('css/app.css'));
|
||||
$loading = file_get_contents(resource_path('views/components/loading.blade.php'));
|
||||
$pageLoading = file_get_contents(resource_path('views/components/page-loading.blade.php'));
|
||||
$views = collect(new RecursiveIteratorIterator(new RecursiveDirectoryIterator(resource_path('views'))))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->isFile() && $file->getExtension() === 'php')
|
||||
->map(fn (SplFileInfo $file): string => file_get_contents($file->getPathname()))
|
||||
->implode("\n");
|
||||
|
||||
expect($utilities)
|
||||
->toContain('@utility loading-indicator')
|
||||
->toContain('@apply text-coollabs dark:text-warning;')
|
||||
->toContain('@apply text-coollabs dark:text-coollabs;')
|
||||
->and($loading)->toContain('loading-indicator')
|
||||
->and($pageLoading)->toContain('loading-indicator')
|
||||
->and($appCss)->toContain('.dark .animate-spin')
|
||||
->toContain('color: var(--color-warning) !important;')
|
||||
->toContain('color: var(--color-coollabs) !important;')
|
||||
->toContain('.dark #nprogress .bar')
|
||||
->toContain('background: var(--color-warning) !important;')
|
||||
->and($views)->not->toMatch('/animate-spin[^"\n]*dark:text-coollabs|dark:text-coollabs[^"\n]*animate-spin/');
|
||||
->toContain('background: var(--color-coollabs) !important;')
|
||||
->not->toContain('color: var(--color-warning) !important;');
|
||||
});
|
||||
|
||||
test('livewire navigation progress bar uses coollabs purple', function () {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
it('keeps horizontal space around sidebar items for focus rings', function () {
|
||||
$navbar = file_get_contents(resource_path('views/components/navbar.blade.php'));
|
||||
|
||||
expect($navbar)
|
||||
->toContain('class="-mx-1 flex min-h-0 flex-1 flex-col gap-y-0.5 overflow-y-auto px-1 pb-2 scrollbar"');
|
||||
});
|
||||
@@ -155,6 +155,19 @@ it('starts the global terminal only once when a target is selected', function ()
|
||||
->toMatch('/public function updatedSelectedUuid\(\).*?\$this->connectToContainer\(\);/s');
|
||||
});
|
||||
|
||||
it('groups global terminal targets by type with labelled sections', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('get filteredTargetGroups()')
|
||||
->toContain("label: 'Servers'")
|
||||
->toContain("label: 'Containers'")
|
||||
->toContain('x-for="group in filteredTargetGroups"')
|
||||
->toContain('x-text="group.label"')
|
||||
->toContain('x-for="target in group.targets"')
|
||||
->not->toContain('x-for="target in filteredTargets"');
|
||||
});
|
||||
|
||||
it('uses the redesigned terminal canvas and controls on resource terminal pages', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/shared/execute-container-command.blade.php'));
|
||||
|
||||
|
||||
@@ -19,6 +19,18 @@ it('still shows the user name and email inside the dropdown panel', function ()
|
||||
->toContain('{{ $userEmail }}');
|
||||
});
|
||||
|
||||
it('animates the dropdown panel when the user menu opens', function () {
|
||||
$menu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
|
||||
$stylesheet = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($menu)
|
||||
->toContain('animate-in fade-in zoom-in-95 duration-150')
|
||||
->toContain("'origin-bottom-left' => \$sidebar")
|
||||
->toContain("'origin-top-right' => ! \$sidebar");
|
||||
|
||||
expect($stylesheet)->toContain('@import "tw-animate-css";');
|
||||
});
|
||||
|
||||
it('changes appearance from a submenu instead of navigating to a separate page', function () {
|
||||
$menu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
it('uses a subtle warning yellow focus ring in dark mode', function () {
|
||||
$appStyles = file_get_contents(dirname(__DIR__, 2).'/resources/css/app.css');
|
||||
$v5Styles = file_get_contents(dirname(__DIR__, 2).'/resources/css/v5/app.css');
|
||||
|
||||
expect($appStyles)
|
||||
->toContain('ring-coollabs dark:ring-warning/70')
|
||||
->and($v5Styles)
|
||||
->toMatch('/\.dark\s*\{[^}]*--ring:\s*color-mix\(in srgb, var\(--warning\) 70%, transparent\);/s');
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
it('uses coollabs purple for scrollbars in dark mode', function () {
|
||||
$utilities = file_get_contents(dirname(__DIR__, 2).'/resources/css/utilities.css');
|
||||
|
||||
expect($utilities)
|
||||
->toContain('dark:scrollbar-thumb-coollabs-100')
|
||||
->not->toContain('dark:scrollbar-thumb-warning');
|
||||
});
|
||||
Reference in new issue
Block a user