mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(ui): show resource menus in the sidebar and mobile drawer, not in the page
The main sidebar shows the resource menu from lg, where it appears, and the mobile drawer shows it too, so the page grid menu is gone. The drawer opens on the menu of the current page again after it closes. Below xl, the resource actions are a compact button in line with the title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a261d89988
commit
649e90ab3b
@@ -4628,7 +4628,7 @@ function wireNavigate(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a resource menu that the main sidebar shows in place of its main menu on wide screens
|
||||
* Registers a resource menu that the main sidebar and the mobile drawer show in place of their main menu
|
||||
* (see components/navbar.blade.php). The menu teleports itself into #main-nav-context-{depth}.
|
||||
* Back on a menu shows the menu one depth lower, and Back at depth 0 shows the main menu.
|
||||
*/
|
||||
|
||||
+13
-12
@@ -2167,6 +2167,16 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
|
||||
}
|
||||
}
|
||||
|
||||
/* A resource menu that the main sidebar (and the mobile drawer) shows instead
|
||||
(navbar #main-nav-context-{depth}). */
|
||||
.application-settings-navigation.in-main-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.application-settings-navigation.in-main-nav ~ * {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
/* Fixed like the main sidebar (3rem → bottom); left tracks its width (--sidebar-w).
|
||||
Fixed leaves the grid, so the content sibling is pinned to column 2 below. */
|
||||
@@ -2193,15 +2203,6 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
/* A resource menu that the main sidebar shows instead (navbar #main-nav-context-{depth}). */
|
||||
.application-settings-navigation.in-main-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.application-settings-navigation.in-main-nav ~ * {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
html:not(.dark) .application-settings-navigation {
|
||||
--rail-fill: var(--coollabs-base);
|
||||
}
|
||||
@@ -6788,12 +6789,12 @@ html.dark .project-card-stats-dot {
|
||||
}
|
||||
}
|
||||
|
||||
/* Resource menu in the collapsed main sidebar: icons only. */
|
||||
.sidebar-collapsed .nav-context-menu :is(.menu-item-label, .nav-children, .menu-item > :not(.menu-item-icon)) {
|
||||
/* Resource menu in the collapsed desktop sidebar: icons only (the mobile drawer never collapses). */
|
||||
.app-sidebar .sidebar-collapsed .nav-context-menu :is(.menu-item-label, .nav-children, .menu-item > :not(.menu-item-icon)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .nav-context-menu .menu-item {
|
||||
.app-sidebar .sidebar-collapsed .nav-context-menu .menu-item {
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
@@ -227,11 +227,11 @@
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// The main sidebar shows this menu on wide screens (see components/navbar.blade.php).
|
||||
// The main sidebar and the mobile drawer show this menu (see components/navbar.blade.php).
|
||||
registerMainNavContext(0, 'Dashboard', 'Back to the main menu');
|
||||
@endphp
|
||||
|
||||
{{-- Rendered twice: in the page (below xl), and teleported into the main sidebar (xl and up).
|
||||
{{-- Rendered twice: in the page (CSS hides it; it keeps the command palette items), and teleported into the main sidebar.
|
||||
navOnly renders only the sidebar copy, as the parent menu of a deeper page (e.g. a backup). --}}
|
||||
@foreach ($navOnly ? ['main-nav'] : ['page', 'main-nav'] as $placement)
|
||||
@if ($placement === 'main-nav')
|
||||
@@ -239,19 +239,15 @@
|
||||
<div class="nav-context-menu">
|
||||
@else
|
||||
<aside @class([
|
||||
'application-settings-navigation in-main-nav min-w-0 xl:self-start',
|
||||
'application-settings-navigation in-main-nav min-w-0',
|
||||
'is-flush' => $flush,
|
||||
]) data-settings-search-items="{{ json_encode($settingsSearchItems) }}">
|
||||
@endif
|
||||
<nav aria-label="Configuration sections"
|
||||
@class([
|
||||
'grid gap-0.5',
|
||||
'grid-cols-2 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]' => $placement === 'page',
|
||||
'grid-cols-1' => $placement === 'main-nav',
|
||||
])>
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($groupedMenuItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]"
|
||||
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]"
|
||||
aria-hidden="true"></div>
|
||||
@endunless
|
||||
@foreach ($groupItems as $menuItem)
|
||||
@@ -280,7 +276,7 @@
|
||||
{{-- Sub-sections belong to the current page only; collapse them for
|
||||
every other item so the sidebar stays short. --}}
|
||||
@if ($menuItem['active'] && filled($sections))
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
|
||||
<div class="nav-children flex flex-col gap-0.5 py-1"
|
||||
x-data="{ activeSection: '' }">
|
||||
@foreach ($sections as $section)
|
||||
<button type="button" class="menu-subitem"
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<x-main-nav-menu :depth="1" :back-label="$parentMenu" :back-title="'Back to the '.strtolower($parentMenu).' menu'"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems(['Backup' => collect($items)->map(fn (array $item): array => [...$item, 'route' => $routes[$item['key']]])], $parameters)) }}">
|
||||
<nav aria-label="Backup settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
<a class="menu-item" data-page-only {{ wireNavigate() }} href="{{ route($routes['back'], $backParameters) }}">
|
||||
<x-reicon name="logout" class="menu-item-icon rotate-180" />
|
||||
<span class="menu-item-label">{{ $backLabel }}</span>
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
@foreach ($items as $item)
|
||||
@if ($item['key'] === 'danger')
|
||||
<div class="col-span-full my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="col-span-full my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endif
|
||||
<a @class([
|
||||
'menu-item',
|
||||
|
||||
@@ -59,29 +59,25 @@
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// The main sidebar shows this menu on wide screens (see components/navbar.blade.php).
|
||||
// The main sidebar and the mobile drawer show this menu (see components/navbar.blade.php).
|
||||
registerMainNavContext(0, 'Dashboard', 'Back to the main menu');
|
||||
@endphp
|
||||
|
||||
{{-- Rendered twice: in the page (below xl), and teleported into the main sidebar (xl and up).
|
||||
{{-- Rendered twice: in the page (CSS hides it; it keeps the command palette items), and teleported into the main sidebar.
|
||||
navOnly renders only the sidebar copy, as the parent menu of a deeper page (e.g. a backup). --}}
|
||||
@foreach ($navOnly ? ['main-nav'] : ['page', 'main-nav'] as $placement)
|
||||
@if ($placement === 'main-nav')
|
||||
@teleport('#main-nav-context-0')
|
||||
<div class="nav-context-menu">
|
||||
@else
|
||||
<aside class="application-settings-navigation in-main-nav min-w-0 xl:self-start"
|
||||
<aside class="application-settings-navigation in-main-nav min-w-0"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems($groupedItems, $databaseRouteParameters, ['project.database.configuration' => $pageSections])) }}">
|
||||
@endif
|
||||
<nav aria-label="Database settings"
|
||||
@class([
|
||||
'grid gap-0.5',
|
||||
'grid-cols-2 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]' => $placement === 'page',
|
||||
'grid-cols-1' => $placement === 'main-nav',
|
||||
])>
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($groupedItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endunless
|
||||
@foreach ($groupItems as $menuItem)
|
||||
<a @class(['menu-item', 'menu-item-active' => $menuItem['active']])
|
||||
@@ -94,7 +90,7 @@
|
||||
@endif
|
||||
</a>
|
||||
@if ($menuItem['active'] && $menuItem['route'] === 'project.database.configuration' && $pageSections !== [])
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
|
||||
<div class="nav-children flex flex-col gap-0.5 py-1"
|
||||
x-data="{
|
||||
activeSection: '',
|
||||
scrollToSection(id) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@props(['backLabel', 'backTitle' => 'Back to the main menu', 'depth' => 0, 'navOnly' => false])
|
||||
@php
|
||||
// The main sidebar replaces its menu with this one on wide screens (see components/navbar.blade.php).
|
||||
// The main sidebar and the mobile drawer replace their menu with this one (see components/navbar.blade.php).
|
||||
registerMainNavContext($depth, $backLabel, $backTitle);
|
||||
@endphp
|
||||
{{-- A page menu, rendered twice: in the page (below xl, CSS hides it on xl), and teleported into the
|
||||
main sidebar (xl and up). The slot must not contain Livewire components (they would render twice).
|
||||
{{-- A page menu, rendered twice: in the page (CSS hides it), and teleported into the
|
||||
main sidebar. The slot must not contain Livewire components (they would render twice).
|
||||
navOnly renders only the sidebar copy: a deeper page uses it to keep its parent menu one Back away. --}}
|
||||
@unless ($navOnly)
|
||||
<aside {{ $attributes->class('application-settings-navigation in-main-nav min-w-0 xl:self-start') }}>
|
||||
<aside {{ $attributes->class('application-settings-navigation in-main-nav min-w-0') }}>
|
||||
{{ $slot }}
|
||||
</aside>
|
||||
@endunless
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
// Label of the open submenu ('' = main menu). It starts on the submenu of the current page
|
||||
// ([data-active-submenu], read in init). Back opens the main menu.
|
||||
submenu: '',
|
||||
// Depth of the open resource menu on wide screens (-1 = main menu or submenu). It starts on the
|
||||
// Depth of the open resource menu (-1 = main menu or submenu). It starts on the
|
||||
// deepest menu of the current page; Back opens the menu one depth lower.
|
||||
navLevel: -1,
|
||||
// Direction of the last menu swap ('forward' or 'back'); it selects the enter animation of every menu.
|
||||
@@ -35,25 +35,53 @@
|
||||
const ua = navigator.userAgent || '';
|
||||
return /Mac|iPhone|iPad|iPod/i.test(platform) || /Mac OS X|Macintosh/i.test(ua) ? '⌘' : 'Ctrl+';
|
||||
})(),
|
||||
// Shows the menu of the current page: its deepest resource menu, or the submenu that holds it.
|
||||
showCurrentPageMenu() {
|
||||
this.submenu = this.$root.querySelector('[data-active-submenu]')?.dataset.activeSubmenu || '';
|
||||
this.navLevel = Number([...this.$root.querySelectorAll('[data-nav-level]')].pop()?.dataset.navLevel ?? -1);
|
||||
},
|
||||
init() {
|
||||
this.submenu = this.$root.querySelector('[data-active-submenu]')?.dataset.activeSubmenu || '';
|
||||
this.navLevel = Number([...this.$root.querySelectorAll('[data-nav-level]')].pop()?.dataset.navLevel ?? -1);
|
||||
this.showCurrentPageMenu();
|
||||
if (@js($desktop)) {
|
||||
// The open resource menu depth is kept across page loads: a page that opens a deeper menu
|
||||
// (e.g. a backup) plays the forward swap, and a page that opens a lower one plays the back swap.
|
||||
const previousLevel = sessionStorage.getItem('navLevel');
|
||||
if (previousLevel !== null && window.matchMedia('(min-width: 80rem)').matches) {
|
||||
if (previousLevel !== null && window.matchMedia('(min-width: 64rem)').matches) {
|
||||
const levelChange = this.navLevel - Number(previousLevel);
|
||||
this.navSwap = levelChange > 0 ? 'forward' : (levelChange < 0 ? 'back' : '');
|
||||
}
|
||||
sessionStorage.setItem('navLevel', this.navLevel);
|
||||
this.$watch('navLevel', level => sessionStorage.setItem('navLevel', level));
|
||||
} else {
|
||||
// The resource menus teleport into the desktop sidebar. The mobile drawer moves them into
|
||||
// its own slots when it opens, and gives them back when the desktop sidebar shows.
|
||||
const moveResourceMenus = toDrawer => this.$root.querySelectorAll('[data-mobile-nav-context]').forEach(drawerSlot => {
|
||||
const desktopSlot = document.getElementById('main-nav-context-' + drawerSlot.dataset.mobileNavContext);
|
||||
if (desktopSlot) {
|
||||
toDrawer ? drawerSlot.append(...desktopSlot.childNodes) : desktopSlot.append(...drawerSlot.childNodes);
|
||||
}
|
||||
});
|
||||
this.desktopQuery = window.matchMedia('(min-width: 64rem)');
|
||||
this.onDesktopChange = event => event.matches && moveResourceMenus(false);
|
||||
this.desktopQuery.addEventListener('change', this.onDesktopChange);
|
||||
// A closed drawer forgets where Back went: it opens again on the menu of the current page.
|
||||
this.$watch('open', isOpen => {
|
||||
if (isOpen) {
|
||||
moveResourceMenus(true);
|
||||
} else {
|
||||
this.navSwap = '';
|
||||
this.showCurrentPageMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (localStorage.getItem('theme') === 'system') { window.applyStoredTheme(); }
|
||||
});
|
||||
window.applyStoredTheme();
|
||||
},
|
||||
destroy() {
|
||||
this.desktopQuery?.removeEventListener('change', this.onDesktopChange);
|
||||
},
|
||||
}">
|
||||
{{-- Search is only useful when workspace resources are available --}}
|
||||
@if (isSubscribed() || ! isCloud())
|
||||
@@ -153,18 +181,18 @@
|
||||
->all());
|
||||
$activeSubmenu = $navSections->flatten(1)->first(fn (array $item): bool => isset($item['children']) && $item['active']);
|
||||
// Resource menus by depth, registered by registerMainNavContext() (e.g. from
|
||||
// components/main-nav-menu.blade.php). Each one teleports itself into #main-nav-context-{depth};
|
||||
// only the desktop sidebar has the slots.
|
||||
$navContext = $desktop ? request()->attributes->get('mainNavContext', []) : [];
|
||||
// components/main-nav-menu.blade.php). Each one teleports itself into #main-nav-context-{depth}
|
||||
// (the desktop sidebar); the mobile drawer moves it into its own slot while it shows (see init).
|
||||
$navContext = request()->attributes->get('mainNavContext', []);
|
||||
@endphp
|
||||
{{-- Wide screens on a resource page: the deepest resource menu replaces the main menu. Back shows the
|
||||
{{-- On a resource page the deepest resource menu replaces the main menu. Back shows the
|
||||
menu one depth lower, and then the main menu (or its submenu), without leaving the page. --}}
|
||||
@foreach ($navContext as $depth => $navMenu)
|
||||
@php
|
||||
$previousDepth = collect($navContext)->keys()->filter(fn (int $key): bool => $key < $depth)->last() ?? -1;
|
||||
@endphp
|
||||
<li x-show="navLevel === {{ $depth }}" data-nav-level="{{ $depth }}" @unless ($loop->last) x-cloak @endunless
|
||||
class="hidden xl:contents">
|
||||
class="contents">
|
||||
<ul role="list" class="flex flex-col gap-y-0.5" :class="navSwap && 'nav-swap-' + navSwap">
|
||||
<li class="mb-1">
|
||||
<button type="button" @click="navLevel = {{ $previousDepth }}; navSwap = 'back'"
|
||||
@@ -177,7 +205,8 @@
|
||||
:class="collapsed && 'lg:hidden'">{{ $navMenu['label'] }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li id="main-nav-context-{{ $depth }}" class="nav-context min-w-0"></li>
|
||||
<li @if ($desktop) id="main-nav-context-{{ $depth }}" @else data-mobile-nav-context="{{ $depth }}" @endif
|
||||
class="nav-context min-w-0"></li>
|
||||
</ul>
|
||||
</li>
|
||||
@endforeach
|
||||
@@ -187,9 +216,9 @@
|
||||
$renderedSubmenus = $navSections->flatten(1)->filter(fn (array $item): bool => isset($item['children']));
|
||||
@endphp
|
||||
@foreach ($renderedSubmenus as $submenuItem)
|
||||
<li x-show="submenu === @js($submenuItem['label'])"
|
||||
@if ($submenuItem['active']) data-active-submenu="{{ $submenuItem['label'] }}" @else x-cloak @endif
|
||||
@class(['contents', 'xl:hidden' => $navContext]) :class="{ 'xl:hidden': navLevel !== -1 }">
|
||||
<li x-show="navLevel === -1 && submenu === @js($submenuItem['label'])"
|
||||
@if ($submenuItem['active']) data-active-submenu="{{ $submenuItem['label'] }}" @endif
|
||||
@if (! $submenuItem['active'] || $navContext) x-cloak @endif class="contents">
|
||||
<ul role="list" class="flex flex-col gap-y-0.5" :class="navSwap && 'nav-swap-' + navSwap">
|
||||
<li class="mb-1">
|
||||
<button type="button" @click="submenu = ''; navSwap = 'back'" title="Back to the main menu" aria-label="Back to the main menu"
|
||||
@@ -231,7 +260,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
@endforeach
|
||||
<li x-show="!submenu" @if ($activeSubmenu) x-cloak @endif @class(['contents', 'xl:hidden' => $navContext]) :class="{ 'xl:hidden': navLevel !== -1 }">
|
||||
<li x-show="navLevel === -1 && !submenu" @if ($activeSubmenu || $navContext) x-cloak @endif class="contents">
|
||||
<ul role="list" class="flex flex-col gap-y-0.5" :class="navSwap && 'nav-swap-' + navSwap">
|
||||
@foreach ($navSections as $sectionLabel => $sectionItems)
|
||||
{{-- The first section needs no title. --}}
|
||||
@@ -269,8 +298,7 @@
|
||||
@endif
|
||||
@if (auth()->id() === 0 && (isCloud() || isDev()))
|
||||
{{-- Part of the main menu: hidden while a submenu or a resource menu shows. --}}
|
||||
<li x-show="!submenu" @if ($activeSubmenu ?? null) x-cloak @endif @class(['xl:hidden' => $navContext ?? []])
|
||||
:class="{ 'xl:hidden': navLevel !== -1 }">
|
||||
<li x-show="navLevel === -1 && !submenu" @if (($activeSubmenu ?? null) || ($navContext ?? [])) x-cloak @endif>
|
||||
<a title="Admin" {{ wireNavigate() }}
|
||||
class="nav-admin {{ request()->is('admin') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
:class="collapsed && 'lg:justify-center lg:px-0'" href="{{ route('admin.index') }}">
|
||||
|
||||
@@ -227,11 +227,11 @@
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// The main sidebar shows this menu on wide screens (see components/navbar.blade.php).
|
||||
// The main sidebar and the mobile drawer show this menu (see components/navbar.blade.php).
|
||||
registerMainNavContext(0, 'Servers', 'Back to the main menu');
|
||||
@endphp
|
||||
|
||||
{{-- Rendered twice: in the page (below xl), and teleported into the main sidebar (xl and up).
|
||||
{{-- Rendered twice: in the page (CSS hides it; it keeps the command palette items), and teleported into the main sidebar.
|
||||
Both copies keep their own warning state (proxy, Sentinel). --}}
|
||||
@foreach (['page', 'main-nav'] as $placement)
|
||||
@if ($placement === 'main-nav')
|
||||
@@ -261,7 +261,7 @@
|
||||
scheduleSentinelExpiry($event.detail.expiresInMilliseconds);
|
||||
">
|
||||
@else
|
||||
<aside class="application-settings-navigation in-main-nav min-w-0 xl:self-start"
|
||||
<aside class="application-settings-navigation in-main-nav min-w-0"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems($groupedServerMenuItems, $serverRouteParameters)) }}"
|
||||
x-data="{
|
||||
proxyConfigurationPending: @js($server->hasPendingProxyConfiguration()),
|
||||
@@ -288,14 +288,10 @@
|
||||
">
|
||||
@endif
|
||||
<nav aria-label="Server configuration sections"
|
||||
@class([
|
||||
'grid gap-0.5',
|
||||
'grid-cols-2 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]' => $placement === 'page',
|
||||
'grid-cols-1' => $placement === 'main-nav',
|
||||
])>
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($groupedServerMenuItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]"
|
||||
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]"
|
||||
aria-hidden="true"></div>
|
||||
@endunless
|
||||
@foreach ($groupItems as $menuItem)
|
||||
@@ -332,7 +328,7 @@
|
||||
@endif
|
||||
</a>
|
||||
@if ($menuItem['active'] && isset($menuItem['children']))
|
||||
<div class="col-span-full grid grid-cols-2 gap-0.5 border-l border-neutral-200 pl-2 sm:grid-cols-3 xl:grid-cols-1 dark:border-white/[0.08]">
|
||||
<div class="col-span-full grid grid-cols-1 gap-0.5 border-l border-neutral-200 pl-2 dark:border-white/[0.08]">
|
||||
@foreach (collect($menuItem['children'])->filter(fn (array $child): bool => $child['visible'] ?? true) as $child)
|
||||
<a wire:key="{{ $placement }}-server-settings-child-{{ str($menuItem['label'].'-'.$child['label'])->slug() }}"
|
||||
@class(['menu-item', 'menu-item-active' => $child['active']])
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<x-main-nav-menu :depth="1" back-label="Service" back-title="Back to the service menu">
|
||||
<nav aria-label="Compose resource settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
<a class="menu-item" data-page-only {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<x-reicon name="logout" class="menu-item-icon rotate-180" />
|
||||
|
||||
@@ -54,10 +54,10 @@
|
||||
<x-main-nav-menu back-label="Dashboard" :nav-only="$navOnly"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems($groupedItems, $serviceRouteParameters)) }}">
|
||||
<nav aria-label="Service settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($groupedItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endunless
|
||||
@foreach ($groupItems as $menuItem)
|
||||
<a @class(['menu-item', 'menu-item-active' => $menuItem['active']])
|
||||
@@ -73,7 +73,7 @@
|
||||
@endif
|
||||
</a>
|
||||
@if ($menuItem['active'] && $menuItem['route'] === 'project.service.storages' && $storageSections->isNotEmpty())
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
|
||||
<div class="nav-children flex flex-col gap-0.5 py-1"
|
||||
x-data="{ activeSection: '' }">
|
||||
@foreach ($storageSections as $section)
|
||||
<button type="button" class="menu-subitem"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</span>
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top
|
||||
class="listbox-panel right-0! xl:left-auto! xl:min-w-60! xl:max-w-96!" role="menu">
|
||||
class="listbox-panel right-0! left-auto! xl:min-w-60! xl:max-w-96!" role="menu">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
<x-main-nav-menu back-label="Destinations" back-title="Back to Integrations"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems(['Destination' => $destinationMenuItems], $destinationRouteParameters)) }}">
|
||||
<nav aria-label="Destination settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($destinationMenuItems as $menuItem)
|
||||
@if ($menuItem['label'] === 'Danger Zone')
|
||||
<div class="col-span-full my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="col-span-full my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endif
|
||||
<a wire:key="destination-settings-{{ str($menuItem['label'])->slug() }}"
|
||||
@class(['menu-item', 'menu-item-active' => $menuItem['active']])
|
||||
|
||||
@@ -9,116 +9,119 @@
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $application->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$application->status"
|
||||
:healthcheck-url="route('project.application.healthcheck', $parameters)" />
|
||||
<x-applications.links :application="$application" compact />
|
||||
@if ($this->runningDeploymentUrl)
|
||||
<x-deploying-indicator :href="$this->runningDeploymentUrl" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
<x-application.restart-limit-warning :application="$application" />
|
||||
{{-- Below xl: the title and the resource actions share one row. --}}
|
||||
<div class="mb-3 flex w-full min-w-0 items-start gap-3 xl:hidden">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $application->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$application->status"
|
||||
:healthcheck-url="route('project.application.healthcheck', $parameters)" />
|
||||
<x-applications.links :application="$application" compact />
|
||||
@if ($this->runningDeploymentUrl)
|
||||
<x-deploying-indicator :href="$this->runningDeploymentUrl" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
<x-application.restart-limit-warning :application="$application" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
|
||||
@can('deploy', $application)
|
||||
<x-split-action id="application-mobile-actions" class="mb-3 flex w-full">
|
||||
@if (str($application->status)->startsWith('exited'))
|
||||
<x-slot:main wire:click="deploy">
|
||||
<x-reicon name="play-circle" class="size-3.5" />
|
||||
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy' }}
|
||||
</x-slot:main>
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="deploy(true)"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)' }}
|
||||
</button>
|
||||
@endif
|
||||
@if ($application->container_present !== false)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Remove container
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
@if ($application->destination->server->isSwarm())
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<x-slot:main wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Update Service
|
||||
</x-slot:main>
|
||||
@else
|
||||
<x-slot:main @click="document.getElementById('application-mobile-stop-trigger')?.click()">
|
||||
<x-reicon name="stop-circle" class="size-3.5" />
|
||||
Stop
|
||||
</x-slot:main>
|
||||
@endif
|
||||
@else
|
||||
<div class="shrink-0">
|
||||
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
|
||||
@can('deploy', $application)
|
||||
<x-split-action id="application-mobile-actions">
|
||||
@if (str($application->status)->startsWith('exited'))
|
||||
<x-slot:main wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Redeploy
|
||||
<x-reicon name="play-circle" class="size-3.5" />
|
||||
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy' }}
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="{{ str($application->status)->startsWith('running') ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy (without cache)
|
||||
</button>
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="deploy(true)"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)' }}
|
||||
</button>
|
||||
@endif
|
||||
@if ($application->container_present !== false)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Remove container
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
@if ($application->destination->server->isSwarm())
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<x-slot:main wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Update Service
|
||||
</x-slot:main>
|
||||
@else
|
||||
<x-slot:main @click="document.getElementById('application-mobile-stop-trigger')?.click()">
|
||||
<x-reicon name="stop-circle" class="size-3.5" />
|
||||
Stop
|
||||
</x-slot:main>
|
||||
@endif
|
||||
@else
|
||||
<x-slot:main wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Redeploy
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="{{ str($application->status)->startsWith('running') ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy (without cache)
|
||||
</button>
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
@unless ($application->destination->server->isSwarm() && $application->build_pack === 'dockercompose')
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@endunless
|
||||
@endif
|
||||
@unless ($application->destination->server->isSwarm() && $application->build_pack === 'dockercompose')
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@endunless
|
||||
@endif
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@endif
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation
|
||||
canGate="deploy" :canResource="$application"
|
||||
title="{{ str($application->status)->startsWith('exited') ? 'Confirm Container Removal?' : 'Confirm Application Stopping?' }}"
|
||||
buttonTitle="{{ str($application->status)->startsWith('exited') ? 'Remove container' : 'Stop' }}"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
str($application->status)->startsWith('exited') ? 'The exited application container will be removed.' : 'This application will be stopped.',
|
||||
str($application->status)->startsWith('exited') ? 'Anonymous volumes may become eligible for Docker cleanup.' : 'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-stop-trigger" type="button">Stop</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||
submitAction="restart" :actions="[
|
||||
'This application will be restarted without rebuilding.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-restart-trigger" type="button">Restart</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@endif
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation
|
||||
canGate="deploy" :canResource="$application"
|
||||
title="{{ str($application->status)->startsWith('exited') ? 'Confirm Container Removal?' : 'Confirm Application Stopping?' }}"
|
||||
buttonTitle="{{ str($application->status)->startsWith('exited') ? 'Remove container' : 'Stop' }}"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
str($application->status)->startsWith('exited') ? 'The exited application container will be removed.' : 'This application will be stopped.',
|
||||
str($application->status)->startsWith('exited') ? 'Anonymous volumes may become eligible for Docker cleanup.' : 'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-stop-trigger" type="button">Stop</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||
submitAction="restart" :actions="[
|
||||
'This application will be restarted without rebuilding.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-restart-trigger" type="button">Restart</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -59,51 +59,54 @@
|
||||
|
||||
<div x-data="{ busy: false }" @database-busy.window="busy = true"
|
||||
@database-action-finished.window="busy = false">
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $database->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$database->status" title="Database status"
|
||||
:healthcheck-url="route('project.database.healthcheck', $parameters)" />
|
||||
@if ($isDeploymentProgress)
|
||||
<x-deploying-indicator label="Working" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
<x-application.restart-limit-warning :application="$database" />
|
||||
{{-- Below xl: the title and the resource actions share one row. --}}
|
||||
<div class="mb-3 flex w-full min-w-0 items-start gap-3 xl:hidden">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $database->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$database->status" title="Database status"
|
||||
:healthcheck-url="route('project.database.healthcheck', $parameters)" />
|
||||
@if ($isDeploymentProgress)
|
||||
<x-deploying-indicator label="Working" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
<x-application.restart-limit-warning :application="$database" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
@can('manage', $database)
|
||||
<x-split-action id="database-mobile-actions" class="mb-3 flex w-full">
|
||||
@if (! $databaseStatus->startsWith('exited'))
|
||||
<x-slot:main x-bind:disabled="busy"
|
||||
@click="document.getElementById('database-restart-trigger')?.click()">
|
||||
<x-loading-on-button x-show="busy" x-cloak />
|
||||
<x-reicon name="restart" class="size-3.5" x-show="!busy" />
|
||||
<span x-text="busy ? 'Restarting…' : 'Restart'">Restart</span>
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('database-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@else
|
||||
<x-slot:main x-bind:disabled="busy" @click="busy = true; $wire.dispatch('startEvent')">
|
||||
<x-loading-on-button x-show="busy" x-cloak />
|
||||
<x-reicon name="play-circle" class="size-3.5" x-show="!busy" />
|
||||
<span x-text="busy ? 'Starting…' : 'Start'">Start</span>
|
||||
</x-slot:main>
|
||||
@endif
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@endif
|
||||
<div class="shrink-0">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
@can('manage', $database)
|
||||
<x-split-action id="database-mobile-actions">
|
||||
@if (! $databaseStatus->startsWith('exited'))
|
||||
<x-slot:main x-bind:disabled="busy"
|
||||
@click="document.getElementById('database-restart-trigger')?.click()">
|
||||
<x-loading-on-button x-show="busy" x-cloak />
|
||||
<x-reicon name="restart" class="size-3.5" x-show="!busy" />
|
||||
<span x-text="busy ? 'Restarting…' : 'Restart'">Restart</span>
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('database-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@else
|
||||
<x-slot:main x-bind:disabled="busy" @click="busy = true; $wire.dispatch('startEvent')">
|
||||
<x-loading-on-button x-show="busy" x-cloak />
|
||||
<x-reicon name="play-circle" class="size-3.5" x-show="!busy" />
|
||||
<span x-text="busy ? 'Starting…' : 'Start'">Start</span>
|
||||
</x-slot:main>
|
||||
@endif
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@teleport('#resource-action-hud-slot')
|
||||
|
||||
@@ -26,109 +26,112 @@
|
||||
|
||||
<div x-data="{ deploying: false }" @service-restarting.window="deploying = true"
|
||||
@service-deploy-finished.window="deploying = false">
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $service->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$displayStatus" :title="$selectedResource ? 'Resource status' : 'Service status'"
|
||||
:container-name="$selectedResource ? 'Container' : 'Containers'" />
|
||||
<x-services.links :service="$service" compact />
|
||||
@if ($isDeploymentProgress)
|
||||
<x-deploying-indicator />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
@if ($selectedResource)
|
||||
<x-application.restart-limit-warning :application="$selectedResource" />
|
||||
@endif
|
||||
{{-- Below xl: the title and the resource actions share one row. --}}
|
||||
<div class="mb-3 flex w-full min-w-0 items-start gap-3 xl:hidden">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 flex-col items-start gap-2">
|
||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $service->name }}
|
||||
</h1>
|
||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||
<x-status-summary :status="$displayStatus" :title="$selectedResource ? 'Resource status' : 'Service status'"
|
||||
:container-name="$selectedResource ? 'Container' : 'Containers'" />
|
||||
<x-services.links :service="$service" compact />
|
||||
@if ($isDeploymentProgress)
|
||||
<x-deploying-indicator />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-1">
|
||||
@if ($selectedResource)
|
||||
<x-application.restart-limit-warning :application="$selectedResource" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($service->isDeployable)
|
||||
@can('deploy', $service)
|
||||
<x-split-action id="service-mobile-actions" class="mb-3 flex w-full">
|
||||
@if ($selectedResource && $selectedResource->container_present !== false && $selectedResourceStatus->startsWith('exited'))
|
||||
<x-slot:main @click="document.getElementById('selected-resource-remove-trigger')?.click()">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
Remove container
|
||||
</x-slot:main>
|
||||
@elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||
<x-slot:main x-bind:disabled="deploying"
|
||||
@click="document.getElementById('service-restart-trigger')?.click()">
|
||||
<x-loading-on-button x-show="deploying" x-cloak />
|
||||
<x-reicon name="restart" class="size-3.5" x-show="!deploying" />
|
||||
<span x-text="deploying ? 'Restarting…' : 'Restart'">Restart</span>
|
||||
</x-slot:main>
|
||||
@if ($serviceStatus->contains('running'))
|
||||
<div class="shrink-0">
|
||||
@if ($service->isDeployable)
|
||||
@can('deploy', $service)
|
||||
<x-split-action id="service-mobile-actions">
|
||||
@if ($selectedResource && $selectedResource->container_present !== false && $selectedResourceStatus->startsWith('exited'))
|
||||
<x-slot:main @click="document.getElementById('selected-resource-remove-trigger')?.click()">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
Remove container
|
||||
</x-slot:main>
|
||||
@elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||
<x-slot:main x-bind:disabled="deploying"
|
||||
@click="document.getElementById('service-restart-trigger')?.click()">
|
||||
<x-loading-on-button x-show="deploying" x-cloak />
|
||||
<x-reicon name="restart" class="size-3.5" x-show="!deploying" />
|
||||
<span x-text="deploying ? 'Restarting…' : 'Restart'">Restart</span>
|
||||
</x-slot:main>
|
||||
@if ($serviceStatus->contains('running'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Restart (pull latest)
|
||||
</button>
|
||||
@endif
|
||||
@if ($serviceStatus->contains('degraded'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Restart (pull latest)
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="open = false; document.getElementById('service-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@endif
|
||||
@if ($serviceStatus->contains('degraded'))
|
||||
@else
|
||||
<x-slot:main x-bind:disabled="deploying" @click="deploying = true; $wire.dispatch('startEvent')">
|
||||
<x-loading-on-button x-show="deploying" x-cloak />
|
||||
<x-reicon name="play-circle" class="size-3.5" x-show="!deploying" />
|
||||
<span x-text="deploying ? 'Deploying…' : 'Deploy'">Deploy</span>
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
Force Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="$wire.dispatch('cleanupEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="trash" class="size-3.5 opacity-70" />
|
||||
Force Cleanup Containers
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="open = false; document.getElementById('service-stop-trigger')?.click()" role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@else
|
||||
@can('deploy', $service)
|
||||
<div id="service-mobile-actions" class="relative"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
<button type="button" class="button gap-1.5" @click="open = !open"
|
||||
:aria-expanded="open" aria-haspopup="menu">
|
||||
<span>Actions</span>
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
@else
|
||||
<x-slot:main x-bind:disabled="deploying" @click="deploying = true; $wire.dispatch('startEvent')">
|
||||
<x-loading-on-button x-show="deploying" x-cloak />
|
||||
<x-reicon name="play-circle" class="size-3.5" x-show="!deploying" />
|
||||
<span x-text="deploying ? 'Deploying…' : 'Deploy'">Deploy</span>
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="$wire.dispatch('cleanupEvent'); open = false" role="menuitem">
|
||||
<x-reicon name="trash" class="size-3.5 opacity-70" />
|
||||
Force Cleanup Containers
|
||||
</button>
|
||||
@endif
|
||||
</x-split-action>
|
||||
@endcan
|
||||
@else
|
||||
@can('deploy', $service)
|
||||
<div id="service-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
<button type="button" class="button w-full justify-between" @click="open = !open"
|
||||
:aria-expanded="open" aria-haspopup="menu">
|
||||
<span>Actions</span>
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.left
|
||||
class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="menu">
|
||||
<div class="listbox-option cursor-default! justify-start! gap-2.5! text-neutral-400! dark:text-fg-faint!"
|
||||
role="menuitem" aria-disabled="true">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
<span>Deploy (<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="cursor-pointer underline underline-offset-2">missing required env vars</a>)</span>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.left
|
||||
class="listbox-panel right-0! left-auto!" role="menu">
|
||||
<div class="listbox-option cursor-default! justify-start! gap-2.5! text-neutral-400! dark:text-fg-faint!"
|
||||
role="menuitem" aria-disabled="true">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
<span>Deploy (<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="cursor-pointer underline underline-offset-2">missing required env vars</a>)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@teleport('#resource-action-hud-slot')
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@elseif (! $embedded)
|
||||
<x-main-nav-menu :depth="1" back-label="Service" back-title="Back to the service menu">
|
||||
<nav aria-label="Compose resource settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
<a class="menu-item" data-page-only {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<x-reicon name="logout" class="menu-item-icon rotate-180" />
|
||||
|
||||
@@ -155,103 +155,106 @@
|
||||
@endteleport
|
||||
|
||||
<div>
|
||||
{{-- Name lives in the desktop topbar (teleport) from lg up. Below that the
|
||||
mobile topbar has no resource context, so show the H1 through lg. --}}
|
||||
<div class="mb-3 w-full lg:hidden">
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<h1 data-testid="server-subtitle"
|
||||
class="min-w-0 truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $server->name }}
|
||||
</h1>
|
||||
<div class="flex w-full min-w-0 items-center gap-2">
|
||||
<x-server.status-summary :server="$server" :proxy-status="$proxyStatus"
|
||||
:show-sentinel-status="$showSentinelStatus" />
|
||||
@if ($traefikDashboardAvailable)
|
||||
<a target="_blank" href="http://{{ formatHostForUrl((string) $serverIp) }}:8080" title="Open Traefik dashboard"
|
||||
class="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2 text-xs font-medium leading-none text-neutral-700 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-white">
|
||||
<x-reicon name="external-link" class="size-3" />
|
||||
Traefik
|
||||
</a>
|
||||
@endif
|
||||
{{-- Below xl: the title and the resource actions share one row. --}}
|
||||
<div class="mb-3 flex w-full min-w-0 items-start gap-3 xl:hidden">
|
||||
{{-- Name lives in the desktop topbar (teleport) from lg up. Below that the
|
||||
mobile topbar has no resource context, so show the H1 through lg. --}}
|
||||
<div class="min-w-0 flex-1 lg:hidden">
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<h1 data-testid="server-subtitle"
|
||||
class="min-w-0 truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $server->name }}
|
||||
</h1>
|
||||
<div class="flex w-full min-w-0 items-center gap-2">
|
||||
<x-server.status-summary :server="$server" :proxy-status="$proxyStatus"
|
||||
:show-sentinel-status="$showSentinelStatus" />
|
||||
@if ($traefikDashboardAvailable)
|
||||
<a target="_blank" href="http://{{ formatHostForUrl((string) $serverIp) }}:8080" title="Open Traefik dashboard"
|
||||
class="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2 text-xs font-medium leading-none text-neutral-700 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-white">
|
||||
<x-reicon name="external-link" class="size-3" />
|
||||
Traefik
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Resource actions stay available below the desktop action HUD. Navigation lives in the sidebar. --}}
|
||||
<div class="w-full xl:hidden">
|
||||
@if ($server->proxySet())
|
||||
@can('manageProxy', $server)
|
||||
<x-split-action id="server-mobile-actions" class="mb-3 flex w-full">
|
||||
@if ($proxyCanBeStopped)
|
||||
<x-slot:main wire:loading.attr="disabled" wire:target="checkProxy,startProxy"
|
||||
@click="document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5" />
|
||||
Restart Proxy
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop Proxy
|
||||
</button>
|
||||
@else
|
||||
<x-slot:main wire:loading.attr="disabled" wire:target="checkProxy,startProxy" @click="$wire.dispatch('checkProxyEvent')">
|
||||
<x-reicon name="play-circle" class="size-3.5" />
|
||||
Start Proxy
|
||||
</x-slot:main>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="checkProxyStatus"
|
||||
wire:loading.attr="disabled" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Refresh Proxy Status
|
||||
</button>
|
||||
</x-split-action>
|
||||
|
||||
{{-- Programmatic open only (clicked from the Actions menu). Keep fully
|
||||
display:none so the modal shells never reserve a layout row. --}}
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy"
|
||||
submitAction="restart" :actions="[
|
||||
'This proxy will be stopped and started again.',
|
||||
'All resources hosted on Coolify will be unavailable during the restart.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Proxy"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:trigger>
|
||||
<button id="server-mobile-restart-proxy-trigger" type="button">
|
||||
{{-- Resource actions stay available below the desktop action HUD. Navigation lives in the sidebar. --}}
|
||||
<div class="ml-auto shrink-0">
|
||||
@if ($server->proxySet())
|
||||
@can('manageProxy', $server)
|
||||
<x-split-action id="server-mobile-actions">
|
||||
@if ($proxyCanBeStopped)
|
||||
<x-slot:main wire:loading.attr="disabled" wire:target="checkProxy,startProxy"
|
||||
@click="document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5" />
|
||||
Restart Proxy
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy"
|
||||
submitAction="stop(true)" :actions="[
|
||||
'The Coolify proxy will be stopped.',
|
||||
'All resources hosted on Coolify will be unavailable.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Stop Proxy"
|
||||
:dispatchEvent="true" dispatchEventType="stopEvent">
|
||||
<x-slot:trigger>
|
||||
<button id="server-mobile-stop-proxy-trigger" type="button">
|
||||
</x-slot:main>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop Proxy
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
@else
|
||||
<x-slot:main wire:loading.attr="disabled" wire:target="checkProxy,startProxy" @click="$wire.dispatch('checkProxyEvent')">
|
||||
<x-reicon name="play-circle" class="size-3.5" />
|
||||
Start Proxy
|
||||
</x-slot:main>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="checkProxyStatus"
|
||||
wire:loading.attr="disabled" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Refresh Proxy Status
|
||||
</button>
|
||||
</x-split-action>
|
||||
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-resource-heading-tabs class="min-w-0 flex-1">
|
||||
@foreach ($serverMenuItems as $menuItem)
|
||||
<a @class([
|
||||
'app-tab shrink-0',
|
||||
'app-tab-active' => $menuItem['active'],
|
||||
])
|
||||
@if ($menuItem['active']) aria-current="page" @endif
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $serverRouteParameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</x-resource-heading-tabs>
|
||||
{{-- Programmatic open only (clicked from the Actions menu). Keep fully
|
||||
display:none so the modal shells never reserve a layout row. --}}
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy"
|
||||
submitAction="restart" :actions="[
|
||||
'This proxy will be stopped and started again.',
|
||||
'All resources hosted on Coolify will be unavailable during the restart.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Proxy"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:trigger>
|
||||
<button id="server-mobile-restart-proxy-trigger" type="button">
|
||||
Restart Proxy
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy"
|
||||
submitAction="stop(true)" :actions="[
|
||||
'The Coolify proxy will be stopped.',
|
||||
'All resources hosted on Coolify will be unavailable.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Stop Proxy"
|
||||
:dispatchEvent="true" dispatchEventType="stopEvent">
|
||||
<x-slot:trigger>
|
||||
<button id="server-mobile-stop-proxy-trigger" type="button">
|
||||
Stop Proxy
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-resource-heading-tabs class="min-w-0 flex-1">
|
||||
@foreach ($serverMenuItems as $menuItem)
|
||||
<a @class([
|
||||
'app-tab shrink-0',
|
||||
'app-tab-active' => $menuItem['active'],
|
||||
])
|
||||
@if ($menuItem['active']) aria-current="page" @endif
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $serverRouteParameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</x-resource-heading-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
<x-main-nav-menu back-label="Sources" back-title="Back to Integrations"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems(['GitHub App' => $settingsMenuItems], $githubAppRouteParameters)) }}">
|
||||
<nav aria-label="GitHub App settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($settingsMenuItems as $menuItem)
|
||||
@if ($menuItem['label'] === 'Danger Zone')
|
||||
<div class="col-span-full my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="col-span-full my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endif
|
||||
<a wire:key="github-app-settings-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
<x-main-nav-menu back-label="S3 Storage" back-title="Back to Integrations"
|
||||
data-settings-search-items="{{ json_encode(settingsSearchItems(['S3 storage' => $settingsMenuItems], $storageRouteParameters)) }}">
|
||||
<nav aria-label="S3 storage settings"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
class="grid grid-cols-1 gap-0.5">
|
||||
@foreach ($settingsMenuItems as $menuItem)
|
||||
@if ($menuItem['label'] === 'Danger Zone')
|
||||
<div class="col-span-full my-2 hidden border-t border-neutral-200 xl:block dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
<div class="col-span-full my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endif
|
||||
<a wire:key="storage-settings-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
|
||||
@@ -55,14 +55,13 @@ it('hides the breadcrumb trail on mobile while keeping the current status visibl
|
||||
$response->assertSee('title="Switch resource"', false);
|
||||
|
||||
// Desktop resource actions dock into the top bar; the slot is hidden below xl.
|
||||
$response->assertSee('<div id="resource-action-hud-slot" class="hidden shrink-0 items-center xl:flex"></div>', false);
|
||||
$response->assertSee('<div id="resource-action-hud-slot" x-ref="resourceActions" class="hidden shrink-0 items-center xl:flex"></div>', false);
|
||||
|
||||
// Below xl, the heading keeps the resource name, current status, and a split action visible.
|
||||
// Below xl, the heading keeps the resource name, current status, and a split action in one row.
|
||||
expect(preg_match(
|
||||
'/<div class="mb-3 w-full xl:hidden">.*?<h1[^>]*>\s*Pure Dockerfile Example\s*<\/h1>.*?Application status/s',
|
||||
'/<div class="mb-3 flex w-full min-w-0 items-start gap-3 xl:hidden">.*?<h1[^>]*>\s*Pure Dockerfile Example\s*<\/h1>.*?Application status.*?id="application-mobile-actions"/s',
|
||||
$response->getContent()
|
||||
))->toBe(1);
|
||||
$response->assertSee('id="application-mobile-actions"', false);
|
||||
$response->assertSee('Running');
|
||||
|
||||
expect($response->getContent())->not->toContain('hidden pt-2 pb-10 md:flex');
|
||||
|
||||
Reference in New Issue
Block a user