mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-19 14:23:25 +00:00
feat(ui): polish email modal, search loading, and volume tables
Move profile email change into a modal with Alpine focus/close handling, show a loading overlay during command-palette navigation, and adjust volumes/config-diff grids plus related service/storage UI. Cover with feature tests for search, profile, storage, and domains.
This commit is contained in:
@@ -167,6 +167,7 @@ class Index extends Component
|
|||||||
$this->show_verification = false;
|
$this->show_verification = false;
|
||||||
|
|
||||||
$this->dispatch('success', 'Email address updated successfully.');
|
$this->dispatch('success', 'Email address updated successfully.');
|
||||||
|
$this->dispatch('close-email-change-modal');
|
||||||
} else {
|
} else {
|
||||||
$this->dispatch('error', 'Failed to update email address.');
|
$this->dispatch('error', 'Failed to update email address.');
|
||||||
}
|
}
|
||||||
@@ -231,12 +232,6 @@ class Index extends Component
|
|||||||
$this->dispatch('success', 'Email change request cancelled.');
|
$this->dispatch('success', 'Email change request cancelled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showEmailChangeForm()
|
|
||||||
{
|
|
||||||
$this->show_email_change = true;
|
|
||||||
$this->new_email = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resetPassword()
|
public function resetPassword()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
+12
-7
@@ -1881,17 +1881,17 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem minmax(8rem, 1fr) 5rem;
|
grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem minmax(8rem, 1fr) 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Persistent storage volumes: Name | Source | Destination | [PR suffix] | [Actions] */
|
/* Persistent storage volumes: Name | Source | Destination | [PR suffix] | Backup | [Actions] */
|
||||||
.volumes-table-grid-readonly {
|
.volumes-table-grid-readonly {
|
||||||
grid-template-columns: minmax(12rem, 1.5fr) minmax(8rem, 1fr) minmax(8rem, 1fr);
|
grid-template-columns: minmax(12rem, 1.5fr) minmax(8rem, 1fr) minmax(8rem, 1fr) 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid {
|
.volumes-table-grid {
|
||||||
grid-template-columns: minmax(10rem, 1.4fr) minmax(6rem, 1fr) minmax(6rem, 1fr) minmax(10.5rem, auto);
|
grid-template-columns: minmax(10rem, 1.4fr) minmax(6rem, 1fr) minmax(6rem, 1fr) 5rem 17.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid-with-pr {
|
.volumes-table-grid-with-pr {
|
||||||
grid-template-columns: minmax(9rem, 1.2fr) minmax(5.5rem, 0.85fr) minmax(5.5rem, 0.85fr) 9.25rem minmax(10.5rem, auto);
|
grid-template-columns: minmax(9rem, 1.2fr) minmax(5.5rem, 0.85fr) minmax(5.5rem, 0.85fr) 9.25rem 5rem 17.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-mobile-label {
|
.volumes-mobile-label {
|
||||||
@@ -1927,7 +1927,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.volumes-table-grid {
|
.volumes-table-grid {
|
||||||
grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) minmax(9rem, auto);
|
grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) 5rem 17.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid > .volumes-col-source,
|
.volumes-table-grid > .volumes-col-source,
|
||||||
@@ -1936,7 +1936,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid-with-pr {
|
.volumes-table-grid-with-pr {
|
||||||
grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem minmax(9rem, auto);
|
grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem 5rem 17.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid-with-pr > .volumes-col-source,
|
.volumes-table-grid-with-pr > .volumes-col-source,
|
||||||
@@ -1945,7 +1945,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid-readonly {
|
.volumes-table-grid-readonly {
|
||||||
grid-template-columns: minmax(10rem, 1.4fr) minmax(8rem, 1fr);
|
grid-template-columns: minmax(10rem, 1.4fr) minmax(8rem, 1fr) 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volumes-table-grid-readonly > .volumes-col-source,
|
.volumes-table-grid-readonly > .volumes-col-source,
|
||||||
@@ -2001,6 +2001,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.volumes-cell-name,
|
.volumes-cell-name,
|
||||||
|
.volumes-col-backup,
|
||||||
.volumes-cell-dest,
|
.volumes-cell-dest,
|
||||||
.volumes-cell-actions {
|
.volumes-cell-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2010,6 +2011,10 @@ input[type="search"]::-webkit-search-results-decoration {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.volumes-col-backup {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.volumes-cell-actions {
|
.volumes-cell-actions {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="overflow-x-auto rounded-lg ring-1 ring-neutral-200 dark:ring-white/[0.08]">
|
<div class="overflow-x-auto rounded-lg ring-1 ring-neutral-200 dark:ring-white/[0.08]">
|
||||||
<div class="min-w-[640px]">
|
<div class="min-w-[640px]">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[minmax(12rem,1.5fr)_minmax(8rem,1fr)_1.5rem_minmax(8rem,1fr)_1.75rem] items-center gap-3 bg-neutral-100 px-3 py-2 text-[11px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint">
|
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-center gap-3 bg-neutral-100 px-3 py-2 text-[11px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint">
|
||||||
<div>Field</div>
|
<div>Field</div>
|
||||||
<div>Current</div>
|
<div>Current</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[minmax(12rem,1.5fr)_minmax(8rem,1fr)_1.5rem_minmax(8rem,1fr)_1.75rem] items-start gap-3 px-3 py-2.5 text-sm transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]">
|
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-start gap-3 px-3 py-2.5 text-sm transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]">
|
||||||
<div class="min-w-0 font-medium text-neutral-900 dark:text-fg">
|
<div class="min-w-0 font-medium text-neutral-900 dark:text-fg">
|
||||||
@if ($rowExpandable)
|
@if ($rowExpandable)
|
||||||
<div class="break-words"
|
<div class="break-words"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
selectedIndex: -1,
|
selectedIndex: -1,
|
||||||
isSearching: false,
|
isSearching: false,
|
||||||
isLoadingInitialData: false,
|
isLoadingInitialData: false,
|
||||||
|
isPaletteTransitioning: false,
|
||||||
allSearchableItems: [],
|
allSearchableItems: [],
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
creatableItems: [],
|
creatableItems: [],
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
this.isLoadingInitialData = false;
|
this.isLoadingInitialData = false;
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
this.allSearchableItems = [];
|
this.allSearchableItems = [];
|
||||||
|
this.isPaletteTransitioning = false;
|
||||||
// Ensure scroll is restored
|
// Ensure scroll is restored
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
// Use $wire instead of @this for SPA navigation compatibility
|
// Use $wire instead of @this for SPA navigation compatibility
|
||||||
@@ -104,6 +106,12 @@
|
|||||||
$wire.closeSearchModal();
|
$wire.closeSearchModal();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
runPaletteTransition(callback) {
|
||||||
|
this.isPaletteTransitioning = true;
|
||||||
|
return Promise.resolve(callback()).finally(() => {
|
||||||
|
this.isPaletteTransitioning = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
navigateResults(direction) {
|
navigateResults(direction) {
|
||||||
const results = document.querySelectorAll('.search-result-item');
|
const results = document.querySelectorAll('.search-result-item');
|
||||||
if (results.length === 0) return;
|
if (results.length === 0) return;
|
||||||
@@ -162,7 +170,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchingItem && typeof $wire !== 'undefined' && $wire) {
|
if (matchingItem && typeof $wire !== 'undefined' && $wire) {
|
||||||
$wire.navigateToResource(matchingItem.type);
|
this.runPaletteTransition(() => $wire.navigateToResource(matchingItem.type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -313,14 +321,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search results -->
|
<!-- Search results -->
|
||||||
<div x-show="searchQuery.length >= 1" x-cloak class="command-palette-body">
|
<div x-show="searchQuery.length >= 1" x-cloak class="command-palette-body relative">
|
||||||
|
<div x-show="isPaletteTransitioning" x-cloak
|
||||||
|
class="absolute inset-0 z-30 flex items-center justify-center bg-white/50 backdrop-blur-[2px] dark:bg-black/40">
|
||||||
|
<x-loading text="Loading…" />
|
||||||
|
</div>
|
||||||
@if ($isSelectingResource)
|
@if ($isSelectingResource)
|
||||||
<!-- Resource selection flow -->
|
<!-- Resource selection flow -->
|
||||||
<div class="command-palette-section">
|
<div class="relative min-h-32">
|
||||||
|
<div class="command-palette-section transition-all"
|
||||||
|
wire:loading.class="pointer-events-none opacity-40 blur-[2px]"
|
||||||
|
wire:target="selectServer,selectDestination,selectProject,selectEnvironment">
|
||||||
@if ($selectedServerId === null)
|
@if ($selectedServerId === null)
|
||||||
<div x-init="selectedIndex = -1">
|
<div x-init="selectedIndex = -1">
|
||||||
<div class="command-palette-step-header">
|
<div class="command-palette-step-header">
|
||||||
<button type="button" @click="$wire.goBack()" class="command-palette-step-back"
|
<button type="button" @click="runPaletteTransition(() => $wire.goBack())" class="command-palette-step-back"
|
||||||
title="Back">
|
title="Back">
|
||||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
@@ -367,7 +382,7 @@
|
|||||||
@if ($selectedServerId !== null && $selectedDestinationUuid === null)
|
@if ($selectedServerId !== null && $selectedDestinationUuid === null)
|
||||||
<div x-init="selectedIndex = -1">
|
<div x-init="selectedIndex = -1">
|
||||||
<div class="command-palette-step-header">
|
<div class="command-palette-step-header">
|
||||||
<button type="button" @click="$wire.goBack()" class="command-palette-step-back"
|
<button type="button" @click="runPaletteTransition(() => $wire.goBack())" class="command-palette-step-back"
|
||||||
title="Back">
|
title="Back">
|
||||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
@@ -415,7 +430,7 @@
|
|||||||
@if ($selectedDestinationUuid !== null && $selectedProjectUuid === null)
|
@if ($selectedDestinationUuid !== null && $selectedProjectUuid === null)
|
||||||
<div x-init="selectedIndex = -1">
|
<div x-init="selectedIndex = -1">
|
||||||
<div class="command-palette-step-header">
|
<div class="command-palette-step-header">
|
||||||
<button type="button" @click="$wire.goBack()" class="command-palette-step-back"
|
<button type="button" @click="runPaletteTransition(() => $wire.goBack())" class="command-palette-step-back"
|
||||||
title="Back">
|
title="Back">
|
||||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
@@ -463,7 +478,7 @@
|
|||||||
@if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null)
|
@if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null)
|
||||||
<div x-init="selectedIndex = -1">
|
<div x-init="selectedIndex = -1">
|
||||||
<div class="command-palette-step-header">
|
<div class="command-palette-step-header">
|
||||||
<button type="button" @click="$wire.goBack()" class="command-palette-step-back"
|
<button type="button" @click="runPaletteTransition(() => $wire.goBack())" class="command-palette-step-back"
|
||||||
title="Back">
|
title="Back">
|
||||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
@@ -509,6 +524,12 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
<div wire:loading.flex
|
||||||
|
wire:target="selectServer,selectDestination,selectProject,selectEnvironment"
|
||||||
|
class="absolute inset-0 z-10 hidden items-center justify-center bg-white/40 backdrop-blur-[1px] dark:bg-black/30">
|
||||||
|
<x-loading text="Loading selection…" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource)
|
@elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource)
|
||||||
<!-- Create mode (server-rendered path) -->
|
<!-- Create mode (server-rendered path) -->
|
||||||
@@ -569,7 +590,7 @@
|
|||||||
<div class="command-palette-section">
|
<div class="command-palette-section">
|
||||||
<div class="command-palette-group-label">{{ $category }}</div>
|
<div class="command-palette-group-label">{{ $category }}</div>
|
||||||
@foreach ($items as $item)
|
@foreach ($items as $item)
|
||||||
<button type="button" wire:click="navigateToResource('{{ $item['type'] }}')"
|
<button type="button" @click="runPaletteTransition(() => $wire.navigateToResource('{{ $item['type'] }}'))"
|
||||||
class="search-result-item command-palette-item">
|
class="search-result-item command-palette-item">
|
||||||
@if (!empty($item['logo']))
|
@if (!empty($item['logo']))
|
||||||
<div class="command-palette-item-icon">
|
<div class="command-palette-item-icon">
|
||||||
@@ -646,7 +667,7 @@
|
|||||||
<div class="command-palette-section">
|
<div class="command-palette-section">
|
||||||
<div class="command-palette-group-label" x-text="categoryName"></div>
|
<div class="command-palette-group-label" x-text="categoryName"></div>
|
||||||
<template x-for="item in items" :key="item.type">
|
<template x-for="item in items" :key="item.type">
|
||||||
<button type="button" @click="$wire.navigateToResource(item.type)"
|
<button type="button" @click="runPaletteTransition(() => $wire.navigateToResource(item.type))"
|
||||||
class="search-result-item command-palette-item">
|
class="search-result-item command-palette-item">
|
||||||
<template x-if="item.logo">
|
<template x-if="item.logo">
|
||||||
<div class="command-palette-item-icon">
|
<div class="command-palette-item-icon">
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<div>
|
<div x-data="{
|
||||||
|
emailModalOpen: @js($show_verification),
|
||||||
|
openEmailModal() {
|
||||||
|
this.emailModalOpen = true;
|
||||||
|
this.$nextTick(() => this.$refs.newEmailInput?.focus());
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
@close-email-change-modal.window="emailModalOpen = false">
|
||||||
<x-slot:title>Profile | Coolify</x-slot>
|
<x-slot:title>Profile | Coolify</x-slot>
|
||||||
<x-profile.navbar />
|
<x-profile.navbar />
|
||||||
|
|
||||||
@@ -16,8 +23,8 @@
|
|||||||
<x-forms.input id="name" label="Name" required />
|
<x-forms.input id="name" label="Name" required />
|
||||||
<div class="flex items-end gap-2">
|
<div class="flex items-end gap-2">
|
||||||
<x-forms.input id="email" label="Email" readonly />
|
<x-forms.input id="email" label="Email" readonly />
|
||||||
<x-forms.button wire:click="showEmailChangeForm" type="button"
|
<x-forms.button @click="openEmailModal()" type="button"
|
||||||
:disabled="$show_email_change || $show_verification">
|
x-bind:disabled="emailModalOpen">
|
||||||
Change
|
Change
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,53 +32,56 @@
|
|||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if ($show_email_change)
|
<template x-teleport="body">
|
||||||
<form wire:submit="requestEmailChange">
|
<div x-show="emailModalOpen" x-cloak
|
||||||
<section class="application-settings-section">
|
class="fixed inset-0 z-99 flex h-screen w-screen items-center justify-center p-4">
|
||||||
<div class="application-settings-section-header">
|
<div class="absolute inset-0 h-full w-full bg-black/55 backdrop-blur-[3px]"></div>
|
||||||
|
<div x-show="emailModalOpen" x-trap.inert.noscroll="emailModalOpen"
|
||||||
|
class="application-settings-form application-settings-section relative w-full max-w-xl overflow-hidden"
|
||||||
|
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||||
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<h2>Change email</h2>
|
<h3>{{ $show_verification ? 'Verify new email' : 'Change email' }}</h3>
|
||||||
<p>A six-digit verification code will be sent to the new address.</p>
|
<p class="mt-1 text-xs text-neutral-500 dark:text-fg-dim">
|
||||||
</div>
|
@if ($show_verification)
|
||||||
</div>
|
Code sent to {{ $new_email ?: auth()->user()->pending_email }}.
|
||||||
<div class="application-settings-section-body">
|
@else
|
||||||
<div class="flex max-w-xl items-end gap-2">
|
A six-digit verification code will be sent to the new address.
|
||||||
<x-forms.input id="new_email" label="New email address" required type="email" />
|
@endif
|
||||||
<x-forms.button type="submit">Send code</x-forms.button>
|
|
||||||
<x-forms.button wire:click="$set('show_email_change', false)" type="button">
|
|
||||||
Cancel
|
|
||||||
</x-forms.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if ($show_verification)
|
|
||||||
<form wire:submit="verifyEmailChange">
|
|
||||||
<section class="application-settings-section">
|
|
||||||
<div class="application-settings-section-header">
|
|
||||||
<div>
|
|
||||||
<h2>Verify new email</h2>
|
|
||||||
<p>
|
|
||||||
Code sent to {{ $new_email ?? auth()->user()->pending_email }}.
|
|
||||||
It expires after {{ config('constants.email_change.verification_code_expiry_minutes', 10) }}
|
|
||||||
minutes.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button type="button"
|
||||||
<div class="application-settings-section-body">
|
@click="@if ($show_verification) $wire.cancelEmailChange().then(() => emailModalOpen = false) @else emailModalOpen = false @endif"
|
||||||
<div class="flex max-w-xl items-end gap-2">
|
class="icon-button shrink-0" aria-label="Close">
|
||||||
|
<x-reicon name="x" class="size-4" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if ($show_verification)
|
||||||
|
<form wire:submit="verifyEmailChange" class="application-settings-section-body space-y-4">
|
||||||
<x-forms.input id="email_verification_code" label="Verification code" required
|
<x-forms.input id="email_verification_code" label="Verification code" required
|
||||||
inputmode="numeric" maxlength="6" />
|
inputmode="numeric" maxlength="6" />
|
||||||
<x-forms.button type="submit">Verify email</x-forms.button>
|
<p class="text-xs text-neutral-500 dark:text-fg-dim">
|
||||||
<x-forms.button wire:click="resendVerificationCode" type="button">Resend</x-forms.button>
|
The code expires after
|
||||||
<x-forms.button wire:click="cancelEmailChange" type="button">Cancel</x-forms.button>
|
{{ config('constants.email_change.verification_code_expiry_minutes', 10) }} minutes.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div class="flex justify-end gap-2">
|
||||||
</section>
|
<x-forms.button wire:click="resendVerificationCode" type="button">Resend code</x-forms.button>
|
||||||
</form>
|
<x-forms.button type="submit" isHighlighted>Verify email</x-forms.button>
|
||||||
@endif
|
</div>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<form wire:submit="requestEmailChange" class="application-settings-section-body space-y-4">
|
||||||
|
<x-forms.input id="new_email" label="New email address" required type="email"
|
||||||
|
x-ref="newEmailInput" />
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<x-forms.button type="submit" isHighlighted>Send code</x-forms.button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<form wire:submit="resetPassword">
|
<form wire:submit="resetPassword">
|
||||||
<section class="application-settings-section">
|
<section class="application-settings-section">
|
||||||
|
|||||||
@@ -202,7 +202,7 @@
|
|||||||
{{ $backup->targetName() }}
|
{{ $backup->targetName() }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ $backup->targetType() }}</span>
|
<span>{{ $backup->targetType() }}</span>
|
||||||
<span class="font-mono text-xs">{{ $backup->frequency }}</span>
|
<span>{{ $backup->frequency }}</span>
|
||||||
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
||||||
<span>
|
<span>
|
||||||
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
||||||
|
|||||||
@@ -38,6 +38,13 @@
|
|||||||
$groupedItems = collect($menuGroups)
|
$groupedItems = collect($menuGroups)
|
||||||
->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values())
|
->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values())
|
||||||
->filter(fn ($items) => $items->isNotEmpty());
|
->filter(fn ($items) => $items->isNotEmpty());
|
||||||
|
|
||||||
|
$storageSections = $applications
|
||||||
|
->concat($databases)
|
||||||
|
->map(fn ($resource): array => [
|
||||||
|
'id' => 'storage-service-'.$resource->uuid,
|
||||||
|
'label' => Str::headline($resource->name),
|
||||||
|
]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
||||||
@@ -61,6 +68,19 @@
|
|||||||
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
|
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
|
||||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||||
</a>
|
</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"
|
||||||
|
x-data="{ activeSection: '' }">
|
||||||
|
@foreach ($storageSections as $section)
|
||||||
|
<button type="button" class="menu-subitem"
|
||||||
|
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
|
||||||
|
@click="activeSection = '{{ $section['id'] }}'; window.scrollToSettingsSection?.('{{ $section['id'] }}')">
|
||||||
|
<span class="menu-item-label truncate text-left"
|
||||||
|
title="{{ $section['label'] }}">{{ $section['label'] }}</span>
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
@endforeach
|
@endforeach
|
||||||
</nav>
|
</nav>
|
||||||
@@ -112,6 +132,10 @@
|
|||||||
<livewire:project.shared.environment-variable.all :resource="$service" />
|
<livewire:project.shared.environment-variable.all :resource="$service" />
|
||||||
@elseif ($currentRoute === 'project.service.storages')
|
@elseif ($currentRoute === 'project.service.storages')
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-[13px] leading-5 text-amber-800 dark:border-warning/15 dark:bg-warning/[0.07] dark:text-amber-300/90">
|
||||||
|
Service volume mounts are read-only here. Edit the Docker Compose file and reload it to change volumes.
|
||||||
|
</div>
|
||||||
@foreach ($applications as $application)
|
@foreach ($applications as $application)
|
||||||
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
|
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
|
||||||
:resource="$application" />
|
:resource="$application" />
|
||||||
|
|||||||
@@ -67,9 +67,6 @@
|
|||||||
@if ($isSuggested && ! empty($row['suggestion_label']))
|
@if ($isSuggested && ! empty($row['suggestion_label']))
|
||||||
<span class="table-badge table-badge-warning shrink-0">{{ $row['suggestion_label'] }}</span>
|
<span class="table-badge table-badge-warning shrink-0">{{ $row['suggestion_label'] }}</span>
|
||||||
@endif
|
@endif
|
||||||
@if ($showServiceColumn)
|
|
||||||
<span class="domains-service-mobile table-badge shrink-0">{{ $serviceLabel }}</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@if ($isSuggested && filled($row['dns_message']))
|
@if ($isSuggested && filled($row['dns_message']))
|
||||||
<p class="text-[12px] leading-4 text-amber-700 sm:truncate dark:text-amber-400/90"
|
<p class="text-[12px] leading-4 text-amber-700 sm:truncate dark:text-amber-400/90"
|
||||||
|
|||||||
@@ -416,7 +416,7 @@
|
|||||||
</x-application.settings-section>
|
</x-application.settings-section>
|
||||||
@else
|
@else
|
||||||
{{-- Service stack resources: one settings card + table per service --}}
|
{{-- Service stack resources: one settings card + table per service --}}
|
||||||
<x-application.settings-section :id="'storage-service-'.$resource->id"
|
<x-application.settings-section :id="'storage-service-'.$resource->uuid"
|
||||||
:title="Str::headline($resource->name)" :flush="true"
|
:title="Str::headline($resource->name)" :flush="true"
|
||||||
helper="Volume mounts for this compose service. Compose-managed mounts are read-only in the dashboard.">
|
helper="Volume mounts for this compose service. Compose-managed mounts are read-only in the dashboard.">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
x-transition:leave="ease-in duration-100"
|
x-transition:leave="ease-in duration-100"
|
||||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
class="application-settings-form application-settings-section relative max-h-[calc(100dvh-2rem)] w-full max-w-3xl overflow-hidden"
|
class="application-settings-form application-settings-section relative max-h-[calc(100dvh-2rem)] w-full max-w-6xl overflow-hidden"
|
||||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||||
<header>
|
<header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -7,14 +7,10 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
@if ($isComposeOrService)
|
@if (data_get($resource, 'build_pack') === 'dockercompose')
|
||||||
<div
|
<div
|
||||||
class="border-b border-neutral-200 px-4 py-3 text-[13px] leading-5 text-amber-800 dark:border-white/[0.08] dark:text-amber-300/90">
|
class="border-b border-neutral-200 px-4 py-3 text-[13px] leading-5 text-amber-800 dark:border-white/[0.08] dark:text-amber-300/90">
|
||||||
@if ($resource->type() === 'service')
|
Docker Compose volume mounts are read-only here. Edit the compose file and reload it to change volumes.
|
||||||
Service volume mounts are read-only here. Edit the Docker Compose file and reload it to change volumes.
|
|
||||||
@else
|
|
||||||
Docker Compose volume mounts are read-only here. Edit the compose file and reload it to change volumes.
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@@ -30,6 +26,7 @@
|
|||||||
PR suffix
|
PR suffix
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
<span class="volumes-col-backup text-center">Backup</span>
|
||||||
@if ($showActionsColumn)
|
@if ($showActionsColumn)
|
||||||
<span class="volumes-col-actions text-right">Actions</span>
|
<span class="volumes-col-actions text-right">Actions</span>
|
||||||
@endif
|
@endif
|
||||||
@@ -58,16 +55,6 @@
|
|||||||
<span
|
<span
|
||||||
class="min-w-0 truncate font-mono text-[13px] font-medium text-neutral-950 dark:text-fg"
|
class="min-w-0 truncate font-mono text-[13px] font-medium text-neutral-950 dark:text-fg"
|
||||||
title="{{ $form['name'] }}">{{ $form['name'] }}</span>
|
title="{{ $form['name'] }}">{{ $form['name'] }}</span>
|
||||||
@if ($hasEnabledBackup)
|
|
||||||
@if ($backupUrl)
|
|
||||||
<a href="{{ $backupUrl }}"
|
|
||||||
class="table-badge table-badge-success shrink-0 underline-offset-2 hover:underline"
|
|
||||||
title="Volume backup is enabled">Backup</a>
|
|
||||||
@else
|
|
||||||
<span class="table-badge table-badge-success shrink-0"
|
|
||||||
title="Volume backup is enabled">Backup</span>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,6 +78,20 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<div class="volumes-col-backup data-table-cell-check">
|
||||||
|
<span class="volumes-mobile-label volumes-field-label">Backup</span>
|
||||||
|
@if ($hasEnabledBackup)
|
||||||
|
<a @if ($backupUrl) href="{{ $backupUrl }}" @endif title="Volume backup is enabled">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor" class="size-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span class="data-table-cell-dash">-</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($showActionsColumn)
|
@if ($showActionsColumn)
|
||||||
<div
|
<div
|
||||||
class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
|
class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
|
||||||
@@ -115,16 +116,6 @@
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<x-forms.input id="forms.{{ $id }}.name" required />
|
<x-forms.input id="forms.{{ $id }}.name" required />
|
||||||
</div>
|
</div>
|
||||||
@if ($hasEnabledBackup)
|
|
||||||
@if ($backupUrl)
|
|
||||||
<a href="{{ $backupUrl }}"
|
|
||||||
class="table-badge table-badge-success shrink-0 underline-offset-2 hover:underline"
|
|
||||||
title="Volume backup is enabled">Backup</a>
|
|
||||||
@else
|
|
||||||
<span class="table-badge table-badge-success shrink-0"
|
|
||||||
title="Volume backup is enabled">Backup</span>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -149,6 +140,20 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<div class="volumes-col-backup data-table-cell-check">
|
||||||
|
<span class="volumes-mobile-label volumes-field-label">Backup</span>
|
||||||
|
@if ($hasEnabledBackup)
|
||||||
|
<a @if ($backupUrl) href="{{ $backupUrl }}" @endif title="Volume backup is enabled">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor" class="size-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span class="data-table-cell-dash">-</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
|
class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
|
||||||
<x-forms.button type="submit" class="!px-2.5 !text-xs">
|
<x-forms.button type="submit" class="!px-2.5 !text-xs">
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ it('renders frontend-only application backup search data for volume names and fr
|
|||||||
->assertSee('Target A–Z')
|
->assertSee('Target A–Z')
|
||||||
->assertSee('filterOpen')
|
->assertSee('filterOpen')
|
||||||
->assertSee('sortOpen')
|
->assertSee('sortOpen')
|
||||||
|
->assertDontSee('class="font-mono text-xs">daily', false)
|
||||||
->assertDontSee('backup-type-filter-trigger', false)
|
->assertDontSee('backup-type-filter-trigger', false)
|
||||||
->assertDontSee('backup-sort-trigger', false);
|
->assertDontSee('backup-sort-trigger', false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('shows a loading overlay while resource selection steps load', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
|
||||||
|
|
||||||
|
expect($view)
|
||||||
|
->toContain('wire:loading.class="pointer-events-none opacity-40 blur-[2px]"')
|
||||||
|
->toContain('wire:loading.flex')
|
||||||
|
->toContain('selectServer,selectDestination,selectProject,selectEnvironment')
|
||||||
|
->toContain('Loading selection…')
|
||||||
|
->toContain('isPaletteTransitioning')
|
||||||
|
->toContain('runPaletteTransition')
|
||||||
|
->toContain('Loading…');
|
||||||
|
});
|
||||||
@@ -131,6 +131,12 @@ it('uses the same eye-off2 icon in configuration changes expand toggle', functio
|
|||||||
->not->toContain('M22.2954 6.31083');
|
->not->toContain('M22.2954 6.31083');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a wide configuration changes dialog', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/project/shared/configuration-checker.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->toContain('max-w-6xl');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders env var password input before visibility toggle in tab order', function () {
|
it('renders env var password input before visibility toggle in tab order', function () {
|
||||||
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
|
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ it('batches volume backup meta and exposes forms for every volume', function ()
|
|||||||
$component = Livewire::test(All::class, ['resource' => $application]);
|
$component = Livewire::test(All::class, ['resource' => $application]);
|
||||||
|
|
||||||
expect($component->get('volumeBackupMeta'))->toHaveCount(5)
|
expect($component->get('volumeBackupMeta'))->toHaveCount(5)
|
||||||
->and($component->get('forms'))->toHaveCount(5);
|
->and($component->get('forms'))->toHaveCount(5)
|
||||||
|
->and($component->html())->toContain('volumes-col-backup')
|
||||||
|
->not->toContain('table-badge table-badge-success');
|
||||||
|
|
||||||
foreach ($component->get('volumeBackupMeta') as $meta) {
|
foreach ($component->get('volumeBackupMeta') as $meta) {
|
||||||
expect($meta['enabled'])->toBeTrue()
|
expect($meta['enabled'])->toBeTrue()
|
||||||
|
|||||||
@@ -101,13 +101,15 @@ it('renders volumes as a data table with shared column headers', function () {
|
|||||||
->toContain('Volume Name')
|
->toContain('Volume Name')
|
||||||
->toContain('Source Path')
|
->toContain('Source Path')
|
||||||
->toContain('Destination Path')
|
->toContain('Destination Path')
|
||||||
|
->toContain('volumes-col-backup')
|
||||||
->toContain('supportsPreviewSuffix')
|
->toContain('supportsPreviewSuffix')
|
||||||
->toContain('openBackupModal')
|
->toContain('openBackupModal')
|
||||||
->toContain('data-table-row')
|
->toContain('data-table-row')
|
||||||
->toContain('volumes-mobile-label')
|
->toContain('volumes-mobile-label')
|
||||||
->toContain('table-badge-success')
|
->not->toContain('table-badge table-badge-success')
|
||||||
->not->toContain('livewire:project.shared.storages.show')
|
->not->toContain('livewire:project.shared.storages.show')
|
||||||
->not->toContain('x-status-badge');
|
->not->toContain('x-status-badge')
|
||||||
|
->not->toContain('Service volume mounts are read-only here.');
|
||||||
|
|
||||||
// Show remains available for isolated embeds/tests but is no longer nested from All.
|
// Show remains available for isolated embeds/tests but is no longer nested from All.
|
||||||
expect($showView)
|
expect($showView)
|
||||||
@@ -118,7 +120,16 @@ it('renders volumes as a data table with shared column headers', function () {
|
|||||||
expect($storageView)
|
expect($storageView)
|
||||||
->toContain('Str::headline($resource->name)')
|
->toContain('Str::headline($resource->name)')
|
||||||
->toContain(':flush="true"')
|
->toContain(':flush="true"')
|
||||||
->toContain('storage-service-');
|
->toContain("'storage-service-'.\$resource->uuid");
|
||||||
|
|
||||||
|
$serviceConfigurationView = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
|
||||||
|
|
||||||
|
expect($serviceConfigurationView)
|
||||||
|
->toContain('storageSections')
|
||||||
|
->toContain('menu-subitem')
|
||||||
|
->toContain('scrollToSettingsSection')
|
||||||
|
->toContain('Service volume mounts are read-only here.')
|
||||||
|
->toContain("'storage-service-'.\$resource->uuid");
|
||||||
|
|
||||||
$css = file_get_contents(resource_path('css/app.css'));
|
$css = file_get_contents(resource_path('css/app.css'));
|
||||||
|
|
||||||
@@ -131,6 +142,8 @@ it('renders volumes as a data table with shared column headers', function () {
|
|||||||
->toContain('@media (max-width: 768px)')
|
->toContain('@media (max-width: 768px)')
|
||||||
->toContain('.table-badge-success');
|
->toContain('.table-badge-success');
|
||||||
|
|
||||||
|
expect($css)->toContain('17.5rem');
|
||||||
|
|
||||||
// Settings form labels are 13px (not Tailwind text-sm 14px).
|
// Settings form labels are 13px (not Tailwind text-sm 14px).
|
||||||
expect($css)
|
expect($css)
|
||||||
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
|
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ it('adds profile navigation with an appearance tab and route', function () {
|
|||||||
->not->toContain('<h1>Profile</h1>\n <div class="subtitle -mt-2">');
|
->not->toContain('<h1>Profile</h1>\n <div class="subtitle -mt-2">');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the email change form without a Livewire request', function () {
|
||||||
|
$profileView = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
|
||||||
|
|
||||||
|
expect($profileView)
|
||||||
|
->toContain('@click="openEmailModal()"')
|
||||||
|
->toContain('this.$refs.newEmailInput?.focus()')
|
||||||
|
->toContain('x-ref="newEmailInput"')
|
||||||
|
->toContain('x-show="emailModalOpen"')
|
||||||
|
->toContain('x-teleport="body"')
|
||||||
|
->toContain('Change email')
|
||||||
|
->toContain('Send code')
|
||||||
|
->toContain('Verify email')
|
||||||
|
->not->toContain('>Cancel</x-forms.button>')
|
||||||
|
->not->toContain('wire:click="showEmailChangeForm"');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps color theme preferences on the profile appearance view without page width or density controls', function () {
|
it('keeps color theme preferences on the profile appearance view without page width or density controls', function () {
|
||||||
$appearanceView = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
|
$appearanceView = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,8 @@ it('auto-adds the suggested www pair when adding a service domain with both dire
|
|||||||
->set('newDomain', 'https://web.example.com')
|
->set('newDomain', 'https://web.example.com')
|
||||||
->call('addDomain')
|
->call('addDomain')
|
||||||
->assertHasNoErrors()
|
->assertHasNoErrors()
|
||||||
->assertDispatched('success');
|
->assertDispatched('success')
|
||||||
|
->assertSee('DNS skipped');
|
||||||
|
|
||||||
expect(explode(',', (string) $this->webApp->fresh()->fqdn))
|
expect(explode(',', (string) $this->webApp->fresh()->fqdn))
|
||||||
->toContain('https://web.example.com')
|
->toContain('https://web.example.com')
|
||||||
@@ -203,6 +204,22 @@ it('adds a domain to a selected service application', function () {
|
|||||||
$this->webApp->refresh();
|
$this->webApp->refresh();
|
||||||
expect(explode(',', (string) $this->webApp->fqdn))
|
expect(explode(',', (string) $this->webApp->fqdn))
|
||||||
->toBe(['https://web.example.com', 'https://www.web.example.com']);
|
->toBe(['https://web.example.com', 'https://www.web.example.com']);
|
||||||
|
|
||||||
|
$dnsStatuses = $this->webApp->domain_dns_statuses;
|
||||||
|
|
||||||
|
expect($dnsStatuses)
|
||||||
|
->toHaveKey('https://web.example.com')
|
||||||
|
->toHaveKey('https://www.web.example.com')
|
||||||
|
->and($dnsStatuses['https://web.example.com']['status'])
|
||||||
|
->toBe('skipped')
|
||||||
|
->and($dnsStatuses['https://web.example.com']['checked_at'])
|
||||||
|
->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate the service name as a badge in the domain cell', function () {
|
||||||
|
$view = file_get_contents(resource_path('views/livewire/project/service/partials/domain-table.blade.php'));
|
||||||
|
|
||||||
|
expect($view)->not->toContain('domains-service-mobile table-badge');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rolls back a domain change when compose regeneration fails', function () {
|
it('rolls back a domain change when compose regeneration fails', function () {
|
||||||
@@ -273,7 +290,9 @@ it('prunes the previous dns status when a service domain is renamed', function (
|
|||||||
$this->apiApp->refresh();
|
$this->apiApp->refresh();
|
||||||
|
|
||||||
expect($this->apiApp->fqdn)->toBe('https://renamed.example.com')
|
expect($this->apiApp->fqdn)->toBe('https://renamed.example.com')
|
||||||
->and($this->apiApp->domain_dns_statuses)->toBeNull();
|
->and($this->apiApp->domain_dns_statuses)->not->toHaveKey('https://api.example.com')
|
||||||
|
->and($this->apiApp->domain_dns_statuses)->toHaveKey('https://renamed.example.com')
|
||||||
|
->and($this->apiApp->domain_dns_statuses['https://renamed.example.com']['status'])->toBe('skipped');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not restore stale dns status when a removed service domain is re-added', function () {
|
it('does not restore stale dns status when a removed service domain is re-added', function () {
|
||||||
@@ -392,8 +411,7 @@ it('hides dns message text when service domain dns status is ok', function () {
|
|||||||
|
|
||||||
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
|
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
|
||||||
->assertSee('DNS OK')
|
->assertSee('DNS OK')
|
||||||
->assertDontSee('DNS points to 203.0.113.10')
|
->assertDontSee('DNS points to 203.0.113.10');
|
||||||
->assertDontSee('Last checked');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('forbids read-only users from checking service domain dns', function (string $action, array $parameters) {
|
it('forbids read-only users from checking service domain dns', function (string $action, array $parameters) {
|
||||||
|
|||||||
Reference in New Issue
Block a user