mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 22:17:19 +00:00
feat(ui): unify resource nav sidebars and polish project layouts
Extract application/database/service configuration sidebars and unified headings; improve clone-to-environment, embedded deployments, storage actions, OAuth icons, terminal, and shared form components.
This commit is contained in:
1 parent
2291c3fa1c
commit
5de6f97256
85 files changed
+2087
-981
No files matched your search
@@ -14,30 +14,205 @@ it('uses a single unified navbar for application, service, database, and server
|
||||
foreach ($files as $path) {
|
||||
$contents = file_get_contents($path);
|
||||
|
||||
expect($contents)
|
||||
->toContain('resource-heading-navbar')
|
||||
->toContain('justify-between')
|
||||
->toContain('border-l border-neutral-200 pl-1');
|
||||
expect($contents)->toContain('resource-heading-navbar');
|
||||
}
|
||||
|
||||
$application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
expect($application)
|
||||
->toContain('justify-start')
|
||||
->toContain('xl:justify-end')
|
||||
->toContain('xl:w-auto')
|
||||
->toContain('xl:bg-transparent')
|
||||
->toContain('xl:fixed')
|
||||
->toContain('xl:top-14')
|
||||
->toContain('xl:hidden')
|
||||
->not->toContain('application-primary-tabs')
|
||||
->not->toContain("typeof collapsed !== 'undefined'")
|
||||
->not->toContain('Spacer: in-flow stand-in')
|
||||
->not->toContain('hidden lg:block lg:h-12')
|
||||
->not->toContain('border-l border-neutral-200 pl-1');
|
||||
});
|
||||
|
||||
it('keeps links with the menu group and actions on the right for applications and services', function () {
|
||||
it('keeps application links next to advanced actions on the right', function () {
|
||||
$application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||
$links = file_get_contents(resource_path('views/components/applications/links.blade.php'));
|
||||
|
||||
$linksBeforeActionsDivider = function (string $contents, string $linksComponent): bool {
|
||||
$linksPos = strpos($contents, $linksComponent);
|
||||
$dividerPos = strpos($contents, 'border-l border-neutral-200 pl-1');
|
||||
$desktop = str($application)->after('resource-heading-actions flex')->toString();
|
||||
$advancedPosition = strpos($desktop, '<x-applications.advanced');
|
||||
$linksPosition = strpos($desktop, '<x-applications.links');
|
||||
|
||||
return $linksPos !== false && $dividerPos !== false && $linksPos < $dividerPos;
|
||||
};
|
||||
|
||||
expect($linksBeforeActionsDivider($application, '<x-applications.links'))->toBeTrue();
|
||||
expect($linksBeforeActionsDivider($service, '<x-services.links'))->toBeTrue();
|
||||
expect($advancedPosition)->not->toBeFalse()
|
||||
->and($linksPosition)->not->toBeFalse()
|
||||
->and($linksPosition)->toBeGreaterThan($advancedPosition)
|
||||
->and($links)->toContain('listbox-panel top-full! right-0! left-auto!')
|
||||
->and($links)->toContain('listbox-option justify-start! gap-2.5!')
|
||||
->and($links)->not->toContain('md:left-0 md:right-auto');
|
||||
|
||||
// Desktop layer uses exactly one unified pill bar (mobile section may still use its own pill).
|
||||
expect(substr_count($application, 'resource-heading-navbar'))->toBe(1);
|
||||
expect(substr_count($service, 'resource-heading-navbar'))->toBe(1);
|
||||
});
|
||||
|
||||
it('groups application lifecycle controls in an actions dropdown', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$desktop = str($heading)->after('resource-heading-actions flex')->toString();
|
||||
|
||||
expect($desktop)
|
||||
->toContain('application-desktop-actions')
|
||||
->toContain('Actions')
|
||||
->toContain('listbox-panel top-full! right-0! left-auto!')
|
||||
->toContain('Redeploy')
|
||||
->toContain('Restart')
|
||||
->toContain('Stop')
|
||||
->toContain('Deploy');
|
||||
});
|
||||
|
||||
it('shows deploy directly when it is the only available lifecycle action', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$desktop = str($heading)->after('resource-heading-actions flex')->toString();
|
||||
|
||||
expect($desktop)
|
||||
->toContain("@if (str(\$application->status)->startsWith('exited'))")
|
||||
->toContain('id="application-desktop-deploy"')
|
||||
->toContain('@else')
|
||||
->toContain('id="application-desktop-actions"');
|
||||
});
|
||||
|
||||
it('moves application backups from the top tabs into the settings sidebar', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php'));
|
||||
$backup = file_get_contents(resource_path('views/livewire/project/application/backup/index.blade.php'));
|
||||
|
||||
expect($heading)->not->toContain("'label' => 'Backups'")
|
||||
->and($configuration)->toContain('<x-application.configuration-sidebar')
|
||||
->and($backup)->toContain('<x-application.configuration-sidebar');
|
||||
});
|
||||
|
||||
it('moves application terminal and logs from the top tabs into the settings sidebar', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
|
||||
|
||||
expect($heading)
|
||||
->not->toContain("'label' => 'Terminal'")
|
||||
->not->toContain("'label' => 'Deployment'")
|
||||
->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'");
|
||||
});
|
||||
|
||||
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'");
|
||||
});
|
||||
|
||||
it('centers the rollback image loading state across the card', function () {
|
||||
$rollback = file_get_contents(resource_path('views/livewire/project/application/rollback.blade.php'));
|
||||
|
||||
expect($rollback)
|
||||
->toContain('class="w-full" wire:target="loadImages" wire:loading')
|
||||
->toContain('flex items-center justify-center');
|
||||
});
|
||||
|
||||
it('shows pull request loading feedback inside its settings card', function () {
|
||||
$previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
|
||||
|
||||
expect($previews)
|
||||
->toContain('class="w-full" wire:loading wire:target="load_prs"')
|
||||
->toContain('min-h-32 items-center justify-center')
|
||||
->toContain('<x-loading text="Loading pull requests…"');
|
||||
});
|
||||
|
||||
it('allows the shared empty state to control the storage backups background', function () {
|
||||
$backups = file_get_contents(resource_path('views/livewire/project/application/backup/index.blade.php'));
|
||||
|
||||
expect($backups)
|
||||
->toContain("'application-settings-section-body w-full'")
|
||||
->toContain("'is-flush' => \$backups->isNotEmpty()")
|
||||
->not->toContain('application-settings-section-body is-flush w-full bg-transparent!');
|
||||
});
|
||||
|
||||
it('keeps the application settings sidebar below the fixed header while scrolling', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($sidebar)->toContain('application-settings-navigation')
|
||||
->and($css)->toContain('.application-settings-workspace > .application-settings-navigation')
|
||||
->and($css)->toContain('top: 4rem;')
|
||||
->and($css)->toContain('max-height: calc(100dvh - 5rem);');
|
||||
});
|
||||
|
||||
it('builds application sidebar routes independently of the current request route', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
|
||||
|
||||
expect($sidebar)
|
||||
->not->toContain('get_route_parameters()')
|
||||
->toContain("'project_uuid' => \$application->environment->project->uuid")
|
||||
->toContain("'environment_uuid' => \$application->environment->uuid")
|
||||
->toContain("'application_uuid' => \$application->uuid");
|
||||
});
|
||||
|
||||
it('keeps the deployment log sidebar fixed in the layout without a top gap', function () {
|
||||
$deployment = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($deployment)->toContain(':flush="true"')
|
||||
->and($css)->toContain('.application-settings-navigation.is-flush')
|
||||
->and($css)->toContain('position: static;')
|
||||
->and($css)->toContain('overflow: visible;');
|
||||
});
|
||||
|
||||
it('removes desktop top spacing from the deployment log viewer', function () {
|
||||
$deployment = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
|
||||
expect($deployment)->toContain("'mt-2 flex flex-1 min-h-0 flex-col overflow-hidden lg:mt-0'");
|
||||
});
|
||||
|
||||
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'");
|
||||
});
|
||||
|
||||
it('shows deployment history above the selected deployment logs', function () {
|
||||
$indexClass = file_get_contents(app_path('Livewire/Project/Application/Deployment/Index.php'));
|
||||
$indexView = file_get_contents(resource_path('views/livewire/project/application/deployment/index.blade.php'));
|
||||
$showView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
|
||||
expect($indexClass)
|
||||
->toContain('public bool $embedded = false;')
|
||||
->toContain('public ?string $selectedDeploymentUuid = null;')
|
||||
->and($indexView)
|
||||
->toContain("'data-table-row-active' => \$selectedDeploymentUuid")
|
||||
->and($showView)
|
||||
->toContain('<livewire:project.application.deployment.index :embedded="true"')
|
||||
->toContain(':selected-deployment-uuid="$deployment_uuid"');
|
||||
});
|
||||
|
||||
it('fits the combined deployment history and logs within the desktop viewport', function () {
|
||||
$indexClass = file_get_contents(app_path('Livewire/Project/Application/Deployment/Index.php'));
|
||||
$showView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
|
||||
expect($indexClass)->toContain('$this->defaultTake = 3;')
|
||||
->and($showView)
|
||||
->toContain('xl:h-[calc(100dvh-7.5rem)]')
|
||||
->toContain('xl:overflow-hidden')
|
||||
->toContain('xl:flex-1');
|
||||
});
|
||||
|
||||
it('uses only the healthcheck toggle action to communicate enabled state', function () {
|
||||
$healthcheck = file_get_contents(resource_path('views/livewire/project/shared/health-checks.blade.php'));
|
||||
|
||||
expect($healthcheck)
|
||||
->not->toContain('<x-status-badge')
|
||||
->toContain('buttonTitle="Enable"')
|
||||
->toContain('Disable');
|
||||
});
|
||||
|
||||
it('keeps Links dropdowns outside the scrollable tabs strip', function () {
|
||||
@@ -46,40 +221,16 @@ it('keeps Links dropdowns outside the scrollable tabs strip', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
$tabsComponent = file_get_contents(resource_path('views/components/resource-heading-tabs.blade.php'));
|
||||
|
||||
// Links must not sit inside the overflow-x-auto tabs scroller (causes scrollbar on open).
|
||||
// Application Links are standalone on mobile and next to actions on desktop.
|
||||
expect($application)
|
||||
->toContain('<x-resource-heading-tabs')
|
||||
->not->toContain('<x-resource-heading-tabs')
|
||||
->toContain('resource-heading-menus')
|
||||
->toContain('<x-applications.links');
|
||||
|
||||
// Mobile and desktop each have a tabs + menus pair; verify every pair.
|
||||
preg_match_all('/<x-resource-heading-tabs/', $application, $applicationTabsMatches, PREG_OFFSET_CAPTURE);
|
||||
preg_match_all('/resource-heading-menus/', $application, $applicationMenusMatches, PREG_OFFSET_CAPTURE);
|
||||
expect($applicationTabsMatches[0])->toHaveCount(2);
|
||||
expect($applicationMenusMatches[0])->toHaveCount(2);
|
||||
|
||||
foreach ($applicationTabsMatches[0] as $index => [$match, $tabsOffset]) {
|
||||
$menusOffset = $applicationMenusMatches[0][$index][1];
|
||||
$overflowInTabs = substr($application, $tabsOffset, $menusOffset - $tabsOffset);
|
||||
|
||||
expect($overflowInTabs)->not->toContain('<x-applications.links');
|
||||
expect(strpos($application, '<x-applications.links', $tabsOffset))
|
||||
->toBeGreaterThan($menusOffset);
|
||||
}
|
||||
|
||||
preg_match_all('/<x-resource-heading-tabs/', $service, $serviceTabsMatches, PREG_OFFSET_CAPTURE);
|
||||
preg_match_all('/resource-heading-menus/', $service, $serviceMenusMatches, PREG_OFFSET_CAPTURE);
|
||||
expect($serviceTabsMatches[0])->toHaveCount(2);
|
||||
expect($serviceMenusMatches[0])->toHaveCount(2);
|
||||
|
||||
foreach ($serviceTabsMatches[0] as $index => [$match, $tabsOffset]) {
|
||||
$menusOffset = $serviceMenusMatches[0][$index][1];
|
||||
$overflowInTabs = substr($service, $tabsOffset, $menusOffset - $tabsOffset);
|
||||
|
||||
expect($overflowInTabs)->not->toContain('<x-services.links');
|
||||
expect(strpos($service, '<x-services.links', $tabsOffset))
|
||||
->toBeGreaterThan($menusOffset);
|
||||
}
|
||||
expect($service)
|
||||
->not->toContain('<x-resource-heading-tabs')
|
||||
->toContain('resource-heading-menus')
|
||||
->toContain('<x-services.links');
|
||||
|
||||
expect($css)
|
||||
->toContain('.resource-heading-tabs::-webkit-scrollbar')
|
||||
@@ -101,22 +252,40 @@ it('keeps Links dropdowns outside the scrollable tabs strip', function () {
|
||||
->toContain('livewire:navigated');
|
||||
});
|
||||
|
||||
it('shows an icon in application and service Links controls', function () {
|
||||
$applicationLinks = file_get_contents(resource_path('views/components/applications/links.blade.php'));
|
||||
$serviceLinks = file_get_contents(resource_path('views/components/services/links.blade.php'));
|
||||
|
||||
expect($applicationLinks)->toContain("@props(['application', 'fullWidth' => false])")
|
||||
->toContain('<x-reicon name="external-link"')
|
||||
->and($serviceLinks)->toContain('<x-reicon name="external-link"')
|
||||
->and($applicationLinks)->not->toContain('<x-external-link')
|
||||
->and($serviceLinks)->not->toContain('<x-external-link');
|
||||
});
|
||||
|
||||
it('shows application and service Links on mobile headings', function () {
|
||||
$application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||
|
||||
$mobileApplicationSection = str($application)->between('class="w-full md:hidden"', 'class="hidden w-full items-center md:flex')->toString();
|
||||
$mobileApplicationSection = str($application)->between('class="w-full xl:hidden"', 'class="hidden w-full items-center xl:fixed')->toString();
|
||||
$mobileServiceSection = str($service)->between('class="w-full md:hidden"', 'class="hidden w-full items-center md:flex')->toString();
|
||||
|
||||
expect($mobileApplicationSection)
|
||||
->toContain('<x-applications.links')
|
||||
->toContain('full-width')
|
||||
->toContain('resource-heading-menus')
|
||||
->toContain('<x-resource-heading-tabs');
|
||||
->not->toContain('<x-resource-heading-tabs')
|
||||
->not->toContain("'label' => 'Settings'");
|
||||
|
||||
$applicationLinks = file_get_contents(resource_path('views/components/applications/links.blade.php'));
|
||||
expect($applicationLinks)
|
||||
->toContain("'button w-full justify-between' => \$fullWidth")
|
||||
->toContain('<x-reicon name="chevron-down"');
|
||||
|
||||
expect($mobileServiceSection)
|
||||
->toContain('<x-services.links')
|
||||
->toContain('resource-heading-menus')
|
||||
->toContain('<x-resource-heading-tabs');
|
||||
->not->toContain('<x-resource-heading-tabs');
|
||||
|
||||
// One mobile + one desktop instance.
|
||||
expect(substr_count($application, '<x-applications.links'))->toBe(2);
|
||||
@@ -125,9 +294,6 @@ it('shows application and service Links on mobile headings', function () {
|
||||
|
||||
it('uses overflow scroll arrows on resource heading navbars', function () {
|
||||
$files = [
|
||||
resource_path('views/livewire/project/application/heading.blade.php'),
|
||||
resource_path('views/livewire/project/service/heading.blade.php'),
|
||||
resource_path('views/livewire/project/database/heading.blade.php'),
|
||||
resource_path('views/livewire/server/navbar.blade.php'),
|
||||
resource_path('views/components/project/navbar.blade.php'),
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user