feat(ui): add volume backup storage controls and grouped navigation

This commit is contained in:
Andras Bacsai committed 2026-08-10 10:35:15 +02:00
1 parent 4ba6aa3500
commit 62b2fe2330
20 files changed
+373 -112

No files matched your search

@@ -10,7 +10,7 @@ test('highlighted buttons use the shared coollabs style in every color scheme',
expect($utilities)
->toContain('@utility button-highlighted')
->toContain('@apply border-2 text-coollabs-200 dark:text-white bg-coollabs-50 dark:bg-coollabs/20 border-coollabs dark:border-coollabs-100 hover:bg-coollabs hover:text-white dark:hover:bg-coollabs-100 dark:hover:text-white;')
->toContain('@apply border-coollabs-200 bg-linear-to-b from-coollabs-100 to-coollabs-200 text-white! hover:from-coollabs-100 hover:to-coollabs hover:text-white!;')
->and($appStyles)
->toContain('button[isHighlighted]:not(:disabled)')
->toContain('@apply button-highlighted;')
@@ -177,19 +177,20 @@ it('moves application terminal and logs from the top tabs into the settings side
->not->toContain("'label' => 'Runtime'")
->and($sidebar)
->toContain("'label' => 'Terminal'")
->toContain("'label' => 'Deployment'")
->toContain("'label' => 'Runtime'")
->toContain("'Logs' => ['Deployment', 'Runtime']")
->toContain("'Operations' => ['Terminal', 'Rollback', 'Resource Limits'");
->toContain("'label' => 'Deployment Logs'")
->toContain("'label' => 'Runtime Logs'")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback'");
});
it('groups application automation pages separately from build and deploy', function () {
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($sidebar)
->toContain("'Build & deploy' => ['Git Source', 'Servers', 'Healthcheck']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Preview Deployments']")
->toContain("'Operations' => ['Terminal', 'Rollback', 'Resource Limits'");
->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck']")
->toContain("'Deploy' => ['Git Source', 'Servers', 'Preview Deployments']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback'");
});
it('centers the rollback image loading state across the card', function () {
@@ -291,7 +292,7 @@ it('removes desktop top spacing from the deployment log viewer', function () {
it('uses a distinct runtime log icon in the sidebar', function () {
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($sidebar)->toContain("'Runtime' => 'unordered-list'");
expect($sidebar)->toContain("'Runtime Logs' => 'unordered-list'");
});
it('shows deployment history above the selected deployment logs', function () {
@@ -8,17 +8,21 @@ test('view switchers use the shared coollabs selected state on every page', func
resource_path('views/livewire/project/resource/index.blade.php'),
resource_path('views/livewire/project/service/configuration.blade.php'),
resource_path('views/livewire/tags/show.blade.php'),
resource_path('views/livewire/shared/list-search-controls.blade.php'),
resource_path('views/components/shared-variables/view-controls.blade.php'),
])->map(fn (string $path): string => file_get_contents($path));
$utilities = file_get_contents(resource_path('css/utilities.css'));
expect($views->every(fn (string $view): bool => str_contains($view, "viewMode === 'table'")
expect($views->every(fn (string $view): bool => (str_contains($view, "viewMode === 'table'") || str_contains($view, "viewMode === 'list'"))
&& str_contains($view, "viewMode === 'grid'")
&& str_contains($view, 'control-selected')))
->toBeTrue()
->and($views->every(fn (string $view): bool => str_contains($view, 'flex h-9')
&& str_contains($view, 'size-7.5')))->toBeTrue()
->and($views->implode("\n"))->not->toContain('dark:bg-warning/15 dark:text-warning')
->and($utilities)
->toContain('@utility control-selected')
->toContain('@apply bg-coollabs text-white dark:bg-coollabs dark:text-white;');
->toContain('@apply bg-linear-to-b from-coollabs-100 to-coollabs-200 text-white;');
});
test('server index does not expose server IP addresses', function () {
@@ -10,11 +10,11 @@ it('moves service and database page navigation into their sidebars', function ()
->and($databaseHeading)->not->toContain('<x-resource-heading-tabs')
->and($serviceConfiguration)
->toContain("['label' => 'Backups'")
->toContain("['label' => 'Runtime'")
->toContain("['label' => 'Runtime Logs'")
->toContain("['label' => 'Terminal'")
->and($databaseSidebar)
->toContain("['label' => 'Backups'")
->toContain("['label' => 'Runtime'")
->toContain("['label' => 'Runtime Logs'")
->toContain("['label' => 'Terminal'");
});
@@ -41,21 +41,38 @@ it('matches application action bar behavior for services and databases', functio
->and($database)->toContain('id="database-desktop-actions"');
});
it('keeps database and service sidebar sections in the application sequence', function () {
it('groups database and service navigation by user workflow', function () {
$database = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php'));
$service = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
$serviceSidebars = [
file_get_contents(resource_path('views/components/service/configuration-sidebar.blade.php')),
file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php')),
];
expect($database)
->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']");
->toContain("'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Healthcheck']")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal', 'Metrics']")
->toContain("'Deploy' => ['Servers']")
->toContain("'Automation' => ['Webhooks', 'Backups', 'Import Backup']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Tags', 'Danger Zone']");
expect($service)
->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Backups']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks']")
->toContain("'Logs' => ['Runtime']")
->toContain("'Operations' => ['Terminal', 'Resource Operations', 'Tags', 'Danger Zone']");
foreach ($serviceSidebars as $serviceSidebar) {
expect($serviceSidebar)
->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage']")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']")
->toContain("'Operations' => ['Resource Operations', 'Tags', 'Danger Zone']");
}
});
it('groups application navigation by user workflow', function () {
$application = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($application)
->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck']")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics']")
->toContain("'Deploy' => ['Git Source', 'Servers', 'Preview Deployments']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback', 'Tags', 'Danger Zone']");
});
it('shows the database sidebar on backup pages', function () {
@@ -155,6 +172,6 @@ it('shows the service sidebar on runtime logs and terminal pages', function () {
->toContain("in_array(\$type, ['application', 'database', 'service', 'server'], true)")
->toContain('<x-service.configuration-sidebar :service="$resource" current-route="project.service.command"')
->and($sidebar)
->toContain("'Logs' => ['Runtime']")
->toContain("'Operations' => ['Terminal', 'Resource Operations', 'Tags', 'Danger Zone']");
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal']")
->toContain("'Operations' => ['Resource Operations', 'Tags', 'Danger Zone']");
});
@@ -2,8 +2,10 @@
use App\Livewire\Storage\Resources as StorageResources;
use App\Models\InstanceSettings;
use App\Models\LocalPersistentVolume;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledVolumeBackup;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -75,6 +77,57 @@ beforeEach(function () {
});
describe('Storage/Resources team-scoped backup access', function () {
test('lists and manages volume backup schedules using this storage', function () {
$volume = LocalPersistentVolume::create([
'name' => 'minio-volume-data',
'mount_path' => '/data',
'resource_id' => 999,
'resource_type' => 'App\\Models\\Application',
]);
$backup = ScheduledVolumeBackup::create([
'uuid' => fake()->uuid(),
'backupable_type' => $volume->getMorphClass(),
'backupable_id' => $volume->id,
'team_id' => $this->teamA->id,
's3_storage_id' => $this->storageA->id,
'frequency' => 'daily',
'enabled' => true,
'save_s3' => true,
]);
$destination = S3Storage::unguarded(fn () => S3Storage::create([
'uuid' => fake()->uuid(),
'name' => 'volume-backup-destination',
'region' => 'us-east-1',
'key' => 'key-c',
'secret' => 'secret-c',
'bucket' => 'bucket-c',
'endpoint' => 'https://s3.example.com',
'team_id' => $this->teamA->id,
'is_usable' => true,
]));
Livewire::test(StorageResources::class, ['storage' => $this->storageA])
->assertSee('minio-volume-data')
->assertSee('Volume')
->assertSeeHtml('class="listbox-trigger"')
->assertSeeHtml('x-teleport="body"')
->assertSeeHtml('requestAnimationFrame')
->assertSeeHtml('-panel"')
->assertSeeHtml("visibility: positioned ? 'visible' : 'hidden'")
->assertSeeHtml('positionPanel($el)')
->assertSeeHtml('class="flex items-center"')
->set("selectedVolumeStorages.{$backup->id}", $destination->id)
->call('moveVolumeBackup', $backup->id);
expect($backup->refresh()->s3_storage_id)->toBe($destination->id);
Livewire::test(StorageResources::class, ['storage' => $destination])
->call('disableVolumeS3', $backup->id);
expect($backup->refresh()->save_s3)->toBeFalse()
->and($backup->s3_storage_id)->toBeNull();
});
test('disableS3 on other team backup throws and leaves row unchanged', function () {
expect(fn () => Livewire::test(StorageResources::class, ['storage' => $this->storageA])
->call('disableS3', $this->backupB->id))