mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 04:18:34 +00:00
feat(ui): polish DB public access, backups, and nav
Gate public TCP proxy until a port is set, show loading while saving access, and open proxy logs in process-dialog. Compact backup execution actions, add Postgres general section anchors, and tint resource Actions icons with warning color.
This commit is contained in:
1 parent
7d5c9fdbe1
commit
9c0908f0fc
35 files changed
+413
-143
No files matched your search
@@ -5,6 +5,8 @@ it('uses a database list editor instead of a comma-separated text field', functi
|
||||
|
||||
expect($view)
|
||||
->toContain('<div class="grid w-full gap-4">')
|
||||
->toContain("['value' => true, 'label' => 'All databases']")
|
||||
->toContain("['value' => false, 'label' => 'Specific databases']")
|
||||
->toContain("value: @entangle('databasesToBackup').live")
|
||||
->toContain('class="chip-input"')
|
||||
->toContain('class="chip font-mono"')
|
||||
|
||||
@@ -15,7 +15,12 @@ it('matches the storage backup overview layout', function () {
|
||||
->toContain('placeholder="Search backups"')
|
||||
->toContain('data-table overflow-hidden rounded-xl border')
|
||||
->toContain('<span class="block truncate text-[12px] font-semibold')
|
||||
->toContain('<a class="button" {{ wireNavigate() }} href="{{ $backupRoute }}">Manage</a>')
|
||||
->toContain("route('project.database.backup.execution'")
|
||||
->toContain("route('project.service.database.backup.show'")
|
||||
->toContain("route('project.database.backup.executions'")
|
||||
->toContain("route('project.service.database.backup.executions'")
|
||||
->toContain('<a wire:navigate href="{{ $backupExecutionsRoute }}"')
|
||||
->toContain('<a class="button" wire:navigate href="{{ $backupRoute }}">Manage</a>')
|
||||
->and($component)->toContain("withCount('executions')");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
it('shows a loading indicator while database public access is changing', function () {
|
||||
$databaseTypes = [
|
||||
'clickhouse',
|
||||
'dragonfly',
|
||||
'keydb',
|
||||
'mariadb',
|
||||
'mongodb',
|
||||
'mysql',
|
||||
'postgresql',
|
||||
'redis',
|
||||
];
|
||||
|
||||
foreach ($databaseTypes as $databaseType) {
|
||||
$generalSettings = file_get_contents(resource_path("views/livewire/project/database/{$databaseType}/general.blade.php"));
|
||||
|
||||
expect($generalSettings)
|
||||
->toContain('<x-table.loading target="instantSave" text="Updating public access..." />')
|
||||
->toContain("'label' => blank(\$publicPort) ? 'Public through TCP proxy (set public port first)' : 'Public through TCP proxy'")
|
||||
->toContain("'disabled' => blank(\$publicPort)")
|
||||
->toContain('wire:key="public-access-{{ $publicPort ?: \'unset\' }}"')
|
||||
->not->toContain('x-data="{ publicPort:')
|
||||
->not->toContain('x-effect="options[1].disabled')
|
||||
->not->toContain('id="publicPort" x-on:input=')
|
||||
->not->toContain('id="publicPort" live');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
test('database restore output opens in the centered process dialog', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/database/import-form.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-process-dialog @databaserestore.window="processDialogOpen = true" closeWithX size="xl">')
|
||||
->toContain('<livewire:activity-monitor wire:key="database-restore-{{ $resourceUuid }}" header="Logs" fullHeight />')
|
||||
->not->toContain('<x-slide-over @databaserestore.window="slideOverOpen = true"');
|
||||
});
|
||||
@@ -42,6 +42,38 @@ it('insets empty states nested inside a flush settings section wrapper', functio
|
||||
expect($css)->toContain('.application-settings-section-body.is-flush > div:has(> .empty-state:only-child)');
|
||||
});
|
||||
|
||||
it('insets the backup executions empty state from the table header', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/database/backup-executions.blade.php'));
|
||||
|
||||
expect($view)->toMatch('/@empty\s*<div class="p-4">\s*<x-empty size="sm" title="No backup executions"/');
|
||||
});
|
||||
|
||||
it('keeps backup execution actions compact', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
$view = file_get_contents(resource_path('views/livewire/project/database/backup-executions.blade.php'));
|
||||
|
||||
expect($css)
|
||||
->toContain('grid-template-columns: 6.5rem minmax(7rem, 1fr) 7rem 5rem 4rem minmax(8rem, 1fr) 5rem;')
|
||||
->and($view)
|
||||
->toContain('title="Download backup" aria-label="Download backup"')
|
||||
->toContain('<x-reicon name="upload" class="size-3.5 rotate-180" />')
|
||||
->toContain('title="Delete backup" aria-label="Delete backup"')
|
||||
->toContain('<x-reicon name="trash" class="size-3.5" />');
|
||||
});
|
||||
|
||||
it('uses the compact resource table styling for backup executions', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/database/backup-executions.blade.php'));
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($view)
|
||||
->toContain('class="data-table deployment-table-scroll"')
|
||||
->toContain('data-table-header backup-executions-table-grid h-auto rounded-none px-4 py-2.5 text-[11px]')
|
||||
->toContain('data-table-row backup-executions-table-grid min-h-14 px-4 py-2.5')
|
||||
->toContain('flex min-h-11 items-center justify-between border-t')
|
||||
->and($css)
|
||||
->toMatch('/\.backup-executions-table-grid\s*\{[^}]*gap:\s*0\.75rem;[^}]*min-width:\s*49rem;/');
|
||||
});
|
||||
|
||||
it('renders compact size without the full-page min height class', function () {
|
||||
$html = $this->blade(
|
||||
'<x-empty title="No cloud tokens" description="Add a provider token." icon-name="keys" size="sm" />'
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<?php
|
||||
|
||||
it('uses the themed warning color for resource action icons', function () {
|
||||
foreach ([
|
||||
'application',
|
||||
'database',
|
||||
'service',
|
||||
] as $resource) {
|
||||
$heading = file_get_contents(resource_path("views/livewire/project/{$resource}/heading.blade.php"));
|
||||
|
||||
expect(substr_count($heading, 'name="play-circle" class="size-3.5 text-warning"'))
|
||||
->toBeGreaterThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses native mobile menus for databases and services', function () {
|
||||
$applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
|
||||
|
||||
@@ -39,3 +39,42 @@ test('process dialog body styles support a full-height log surface', function ()
|
||||
->toContain('.process-dialog-body')
|
||||
->toContain('min-height: min(70dvh, 28rem)');
|
||||
});
|
||||
|
||||
test('runtime log terminal fills the available dialog width', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php'));
|
||||
|
||||
expect($view)->toContain("<div @class(['w-full min-w-0', 'runtime-log-shell' => \$collapsible])>");
|
||||
});
|
||||
|
||||
test('process and log viewers use centered dialogs', function () {
|
||||
$paths = [
|
||||
resource_path('views/livewire/project/service/index.blade.php'),
|
||||
resource_path('views/livewire/project/database/keydb/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/redis/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/postgresql/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/clickhouse/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/mongodb/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/dragonfly/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/mariadb/general.blade.php'),
|
||||
resource_path('views/livewire/project/database/mysql/general.blade.php'),
|
||||
resource_path('views/livewire/server/security/patches.blade.php'),
|
||||
resource_path('views/livewire/server/cloudflare-tunnel.blade.php'),
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
expect(file_get_contents($path))
|
||||
->toContain('<x-process-dialog')
|
||||
->not->toContain('<x-slide-over');
|
||||
}
|
||||
|
||||
expect(file_get_contents(resource_path('views/livewire/server/show.blade.php')))
|
||||
->not->toContain('<x-slide-over');
|
||||
});
|
||||
|
||||
test('process dialog can start open for an in-progress operation', function () {
|
||||
$component = file_get_contents(resource_path('views/components/process-dialog.blade.php'));
|
||||
|
||||
expect($component)
|
||||
->toContain("'open' => false")
|
||||
->toContain('processDialogOpen: @js($open)');
|
||||
});
|
||||
@@ -11,7 +11,7 @@ use Livewire\Livewire;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0]);
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
});
|
||||
|
||||
function setupProxyUser(string $role): array
|
||||
@@ -72,6 +72,25 @@ test('admin can see proxy restart and stop buttons', function () {
|
||||
->assertSee('Stop Proxy');
|
||||
});
|
||||
|
||||
test('admin can stop a proxy while it is starting', function () {
|
||||
[$user, $team, $server] = setupProxyUser('admin');
|
||||
|
||||
$server->proxy->status = 'starting';
|
||||
$server->proxy->type = ProxyTypes::TRAEFIK->value;
|
||||
$server->save();
|
||||
$server->refresh();
|
||||
|
||||
$mock = Mockery::mock($server)->makePartial();
|
||||
$mock->shouldReceive('proxySet')->andReturn(true);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
Livewire::test('server.navbar', ['server' => $mock])
|
||||
->assertSee('Stop Proxy')
|
||||
->assertDontSee('Start Proxy');
|
||||
});
|
||||
|
||||
test('member cannot see start proxy button', function () {
|
||||
[$user, $team, $server] = setupProxyUser('member');
|
||||
|
||||
@@ -89,3 +108,23 @@ test('member cannot see start proxy button', function () {
|
||||
Livewire::test('server.navbar', ['server' => $mock])
|
||||
->assertDontSee('Start Proxy');
|
||||
});
|
||||
|
||||
test('start proxy button shows a loading state while proxy startup actions run', function () {
|
||||
[$user, $team, $server] = setupProxyUser('admin');
|
||||
|
||||
$server->proxy->status = 'exited';
|
||||
$server->proxy->type = ProxyTypes::TRAEFIK->value;
|
||||
$server->save();
|
||||
$server->refresh();
|
||||
|
||||
$mock = Mockery::mock($server)->makePartial();
|
||||
$mock->shouldReceive('proxySet')->andReturn(true);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
Livewire::test('server.navbar', ['server' => $mock])
|
||||
->assertSeeHtml('wire:loading.attr="disabled"')
|
||||
->assertSeeHtml('wire:loading.class="is-loading"')
|
||||
->assertSeeHtml('wire:target="checkProxy,startProxy"');
|
||||
});
|
||||
@@ -4,9 +4,10 @@ test('server revalidation opens in the centered process dialog', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/show.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-process-dialog closeWithX size="xl">')
|
||||
->toContain('<x-process-dialog closeWithX size="xl" :open="$isValidating">')
|
||||
->toContain(':isHighlighted="! $server->isFunctional()"')
|
||||
->toContain('@click="processDialogOpen = true" wire:click.prevent="validateServer"');
|
||||
->toContain('@click="processDialogOpen = true" wire:click.prevent="validateServer"')
|
||||
->not->toContain('<x-slide-over');
|
||||
});
|
||||
|
||||
test('completed server validation shows a close action instead of empty logs', function () {
|
||||
|
||||
@@ -18,6 +18,12 @@ it('moves service and database page navigation into their sidebars', function ()
|
||||
->toContain("['label' => 'Terminal'");
|
||||
});
|
||||
|
||||
it('uses a full page load for database backup imports', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
|
||||
|
||||
expect($sidebar)->toContain("['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'navigate' => false");
|
||||
});
|
||||
|
||||
it('matches application action bar behavior for services and databases', function () {
|
||||
$service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||
$database = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
|
||||
@@ -40,7 +46,7 @@ it('keeps database and service sidebar sections in the application sequence', fu
|
||||
$service = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
|
||||
|
||||
expect($database)
|
||||
->toContain("'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Servers', 'Import Backup']")
|
||||
->toContain("'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Import Backup', 'Servers']")
|
||||
->toContain("'Automation' => ['Webhooks', 'Healthcheck']")
|
||||
->toContain("'Logs' => ['Runtime']")
|
||||
->toContain("'Operations' => ['Terminal', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone']");
|
||||
|
||||
@@ -44,3 +44,31 @@ test('configuration sidebar subitems trigger section highlight on scroll', funct
|
||||
->toContain("addEventListener('scrollend'")
|
||||
->toContain('stableFrames');
|
||||
});
|
||||
|
||||
test('postgresql general navigation lists each in-page settings section', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
|
||||
$general = file_get_contents(resource_path('views/livewire/project/database/postgresql/general.blade.php'));
|
||||
|
||||
$sections = [
|
||||
'database-details-section' => 'Database details',
|
||||
'credentials-section' => 'Credentials',
|
||||
'initialization-section' => 'Initialization',
|
||||
'runtime-network-section' => 'Runtime and network',
|
||||
'public-access-section' => 'Public access',
|
||||
'configuration-section' => 'Configuration',
|
||||
'log-delivery-section' => 'Log delivery',
|
||||
'initialization-scripts-section' => 'Initialization scripts',
|
||||
];
|
||||
|
||||
foreach ($sections as $id => $label) {
|
||||
expect($sidebar)
|
||||
->toContain("['id' => '{$id}', 'label' => '{$label}']")
|
||||
->and($general)->toContain("id=\"{$id}\"");
|
||||
}
|
||||
|
||||
expect($sidebar)
|
||||
->toContain("\$database->type() === 'standalone-postgresql'")
|
||||
->toContain('window.scrollToSettingsSection?.(id)')
|
||||
->toContain("activeSection === '{{ \$section['id'] }}'")
|
||||
->toContain("scrollToSection('{{ \$section['id'] }}')");
|
||||
});
|
||||
Reference in new issue
Block a user