mirror of
https://github.com/tiennm99/coolify.git
synced 2026-09-02 20:24:43 +00:00
fix(ui): replace the project layer-2 bar with a page header
The project navbar rendered a single Resources tab that was only ever active on project.resource.index, a page that never rendered the bar, and rendered nothing at all without an environment, leaving project and environment settings with no title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
decf6c7a9a
commit
a052b4b9e5
@@ -162,6 +162,14 @@ primary action in the page header instead. When tabs are useful, their left edge
|
||||
uses the same compact `pl-2` alignment as application navigation rather than
|
||||
the content container's wide horizontal padding.
|
||||
|
||||
A layer-2 tab must be active on the page that renders it. A bar whose only tab
|
||||
points at a different route reads as broken navigation, so project and
|
||||
environment pages (`project.show`, `project.edit`, `project.environment.edit`,
|
||||
`project.clone-me`) carry a plain page header with a 24px title and a 13px
|
||||
muted summary instead of a bar. The environment identity and the way back to
|
||||
its resources already live in `x-top-breadcrumb`; do not restate them in a
|
||||
sub-header.
|
||||
|
||||
The dashboard is a compact overview, not a metrics wall. Use two full-width
|
||||
sections that follow the projects-page grid pattern: projects first, then
|
||||
servers. Keep one `New` action in the page header and let its modal choose the
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
@props([
|
||||
'project',
|
||||
'environment' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$projectParameters = ['project_uuid' => $project->uuid];
|
||||
$environmentParameters = $environment
|
||||
? [...$projectParameters, 'environment_uuid' => $environment->uuid]
|
||||
: [];
|
||||
|
||||
$items = $environment
|
||||
? [
|
||||
['label' => 'Resources', 'route' => 'project.resource.index', 'active' => request()->routeIs('project.resource.index'), 'icon' => 'grid'],
|
||||
]
|
||||
: [];
|
||||
|
||||
$routeParameters = $environment ? $environmentParameters : $projectParameters;
|
||||
@endphp
|
||||
|
||||
@if ($environment)
|
||||
<nav class="mb-6 w-full lg:mb-0">
|
||||
<div class="flex w-full items-center lg:fixed lg:top-12 lg:right-0 lg:z-30 lg:h-12 lg:w-auto lg:border-b lg:border-neutral-200 lg:bg-white/95 lg:pr-4 lg:pl-2 lg:backdrop-blur lg:transition-[left] lg:duration-200 lg:dark:border-white/[0.06] lg:dark:bg-panel/95"
|
||||
:class="[typeof collapsed !== 'undefined' && collapsed ? 'lg:left-16' : 'lg:left-56']">
|
||||
<div
|
||||
class="resource-heading-navbar application-heading-actions flex w-full min-w-0 items-center justify-between gap-2 overflow-visible rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
|
||||
<x-resource-heading-tabs class="min-w-0">
|
||||
@foreach ($items as $item)
|
||||
<a @class([
|
||||
'app-tab shrink-0',
|
||||
'app-tab-active' => $item['active'],
|
||||
])
|
||||
@if ($item['active']) aria-current="page" @endif
|
||||
{{ wireNavigate() }} href="{{ route($item['route'], $routeParameters) }}">
|
||||
<x-reicon :name="$item['icon']" class="size-3.5" />
|
||||
{{ $item['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</x-resource-heading-tabs>
|
||||
|
||||
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5 border-l border-neutral-200 pl-1 dark:border-white/[0.08]">
|
||||
@isset($actions)
|
||||
{{ $actions }}
|
||||
@endisset
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('project.resource.create', $environmentParameters) }}" {{ wireNavigate() }}
|
||||
class="button button-highlighted">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
New resource
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
|
||||
</nav>
|
||||
@endif
|
||||
@@ -1,8 +1,14 @@
|
||||
<div>
|
||||
<x-slot:title>{{ data_get_str($project, 'name')->limit(10) }} > Clone | Coolify</x-slot>
|
||||
<x-project.navbar :project="$project" :environment="$environment" />
|
||||
<div class="w-full max-w-[1180px]">
|
||||
<header class="mb-5">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $environment->name }}</h1>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
Clone this environment inside {{ $project->name }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="mt-8 flex w-full max-w-[1180px] flex-col gap-6 lg:mt-3">
|
||||
<div class="flex flex-col gap-6">
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
@@ -143,5 +149,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<div>
|
||||
<x-slot:title>{{ data_get_str($project, 'name')->limit(10) }} > Edit | Coolify</x-slot>
|
||||
<x-project.navbar :project="$project" />
|
||||
<div class="w-full max-w-[1180px]">
|
||||
<header class="mb-5">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $project->name }}</h1>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">Project settings</p>
|
||||
</header>
|
||||
|
||||
<div class="mt-8 flex w-full max-w-[1180px] flex-col gap-6 lg:mt-3">
|
||||
<div class="flex flex-col gap-6">
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<section class="application-settings-section">
|
||||
@@ -31,5 +35,6 @@
|
||||
<livewire:project.delete-project :disabled="! $project->isEmpty()" :project_id="$project->id" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
<div>
|
||||
<x-slot:title>{{ data_get_str($environment, 'name')->limit(10) }} > Edit | Coolify</x-slot>
|
||||
<x-project.navbar :project="$project" :environment="$environment" />
|
||||
<div class="w-full max-w-[1180px]">
|
||||
<header class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $environment->name }}</h1>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
Environment settings in {{ $project->name }}
|
||||
</p>
|
||||
</div>
|
||||
@can('createAnyResource')
|
||||
<div class="flex w-fit shrink-0 items-center gap-2">
|
||||
<a class="button whitespace-nowrap" {{ wireNavigate() }}
|
||||
href="{{ route('project.clone-me', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
|
||||
<x-reicon name="layers" class="size-3.5 opacity-70" />
|
||||
Clone environment
|
||||
</a>
|
||||
</div>
|
||||
@endcan
|
||||
</header>
|
||||
|
||||
<div class="mt-8 flex w-full max-w-[1180px] flex-col gap-6 lg:mt-3">
|
||||
<div class="flex flex-col gap-6">
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<section class="application-settings-section">
|
||||
@@ -11,13 +28,6 @@
|
||||
<h2>Environment details</h2>
|
||||
<p>Name and describe this environment inside {{ $project->name }}.</p>
|
||||
</div>
|
||||
@can('createAnyResource')
|
||||
<a class="button" {{ wireNavigate() }}
|
||||
href="{{ route('project.clone-me', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
|
||||
<x-reicon name="layers" class="size-3.5 opacity-70" />
|
||||
Clone environment
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$environment" />
|
||||
@@ -44,5 +54,6 @@
|
||||
</div>
|
||||
</section>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<x-slot:title>
|
||||
{{ data_get_str($project, 'name')->limit(10) }} > Environments | Coolify
|
||||
</x-slot>
|
||||
<x-project.navbar :project="$project" />
|
||||
|
||||
<div x-data="projectEnvironments()" class="w-full">
|
||||
<header class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
|
||||
@@ -16,7 +16,6 @@ test('dashboard navbar hides family titles at lg to match the desktop shell', fu
|
||||
test('fixed layer-2 spacers match the fixed bar height', function () {
|
||||
$paths = [
|
||||
resource_path('views/components/dashboard/navbar.blade.php'),
|
||||
resource_path('views/components/project/navbar.blade.php'),
|
||||
resource_path('views/livewire/server/navbar.blade.php'),
|
||||
resource_path('views/livewire/project/application/heading.blade.php'),
|
||||
resource_path('views/livewire/project/database/heading.blade.php'),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Project and environment pages carry their own page header instead of a
|
||||
* layer-2 bar. The former `x-project.navbar` rendered a single "Resources"
|
||||
* tab that was only ever active on `project.resource.index` (a page that did
|
||||
* not render the bar), and rendered nothing at all without an environment.
|
||||
*/
|
||||
$projectPages = [
|
||||
'views/livewire/project/show.blade.php',
|
||||
'views/livewire/project/edit.blade.php',
|
||||
'views/livewire/project/environment-edit.blade.php',
|
||||
'views/livewire/project/clone-me.blade.php',
|
||||
];
|
||||
|
||||
it('drops the project layer-2 bar that never matched the current route', function () use ($projectPages) {
|
||||
expect(file_exists(resource_path('views/components/project/navbar.blade.php')))->toBeFalse();
|
||||
|
||||
foreach ($projectPages as $page) {
|
||||
expect(file_get_contents(resource_path($page)))
|
||||
->not->toContain('<x-project.navbar')
|
||||
->not->toContain('resource-heading-navbar');
|
||||
}
|
||||
});
|
||||
|
||||
it('gives every project and environment page an in-flow title', function () use ($projectPages) {
|
||||
foreach ($projectPages as $page) {
|
||||
expect(file_get_contents(resource_path($page)))
|
||||
->toContain('<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">')
|
||||
->toContain('class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim"');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the environment clone action in the page header', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/environment-edit.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('{{ $environment->name }}</h1>')
|
||||
->toContain('Environment settings in {{ $project->name }}')
|
||||
->toContain('Clone environment')
|
||||
// The action moved out of the card header into the page header.
|
||||
->toMatch('/<header[^>]*>.*Clone environment.*<\/header>/s');
|
||||
});
|
||||
|
||||
it('does not repeat the resources action outside the resources page', function () {
|
||||
foreach (['views/livewire/project/environment-edit.blade.php', 'views/livewire/project/clone-me.blade.php'] as $page) {
|
||||
expect(file_get_contents(resource_path($page)))->not->toContain('New resource');
|
||||
}
|
||||
|
||||
expect(file_get_contents(resource_path('views/livewire/project/resource/index.blade.php')))
|
||||
->toContain('New resource');
|
||||
});
|
||||
@@ -470,7 +470,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/server/navbar.blade.php'),
|
||||
resource_path('views/components/project/navbar.blade.php'),
|
||||
];
|
||||
|
||||
foreach ($files as $path) {
|
||||
|
||||
Reference in New Issue
Block a user