feat(ui): optimistic updates for notifications and resource actions

- Notification channels: flip enable/disable label + highlight instantly, roll back on error (all 6 channels via shared channel-actions)
- Database heading: Start/Restart show instant Starting/Restarting state + spinner, disable during the request
- Service heading: Restart now reuses the deploying state (spinner + Restarting label), matching the existing Deploy pattern
This commit is contained in:
Aditya Tripathi
2026-09-11 06:38:48 +00:00
parent 01337c0cd6
commit c046b186aa
3 changed files with 55 additions and 26 deletions
@@ -13,12 +13,17 @@
toggleMethod: @js($toggleMethod),
testMethod: @js($testMethod),
}">
<x-forms.button type="button" :disabled="!$canUpdate" :isHighlighted="!$enabled"
<x-forms.button type="button" :disabled="!$canUpdate"
x-bind:class="{ 'button-highlighted': !enabled }"
x-on:click="
if (!enabled && !$el.closest('form').reportValidity()) return;
$wire.$set(enabledProperty, !enabled).then(() => $wire.$call(toggleMethod));
const next = !enabled;
enabled = next;
$wire.$set(enabledProperty, next)
.then(() => $wire.$call(toggleMethod))
.catch(() => { enabled = !next; });
">
{{ $enabled ? 'Disable' : 'Enable' }}
<span x-text="enabled ? 'Disable' : 'Enable'">{{ $enabled ? 'Disable' : 'Enable' }}</span>
</x-forms.button>
<x-forms.button type="button" :disabled="!$enabled"
x-on:click="if ($el.closest('form').reportValidity()) $wire.$call(testMethod)">
@@ -57,7 +57,8 @@
</x-slot:content>
</x-process-dialog>
<div x-data>
<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">
@@ -77,9 +78,11 @@
@can('manage', $database)
<x-split-action id="database-mobile-actions" class="mb-3 flex w-full">
@if (! $databaseStatus->startsWith('exited'))
<x-slot:main @click="document.getElementById('database-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5" />
Restart
<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">
@@ -87,9 +90,10 @@
Stop
</button>
@else
<x-slot:main @click="$wire.dispatch('startEvent')">
<x-reicon name="play-circle" class="size-3.5" />
Start
<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>
@@ -107,9 +111,11 @@
@can('manage', $database)
<x-split-action id="database-desktop-actions">
@if (! $databaseStatus->startsWith('exited'))
<x-slot:main @click="document.getElementById('database-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5" />
Restart
<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">
@@ -117,9 +123,10 @@
Stop
</button>
@else
<x-slot:main @click="$wire.dispatch('startEvent')">
<x-reicon name="play-circle" class="size-3.5" />
Start
<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>
@@ -162,14 +169,23 @@
@script
<script>
$wire.$on('startEvent', () => {
$wire.$on('startEvent', async () => {
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('start');
try {
await $wire.$call('start');
} finally {
window.dispatchEvent(new CustomEvent('database-action-finished'));
}
});
$wire.$on('restartEvent', () => {
$wire.$on('restartEvent', async () => {
window.dispatchEvent(new CustomEvent('database-busy'));
$wire.$dispatch('info', 'Restarting database.');
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('restart');
try {
await $wire.$call('restart');
} finally {
window.dispatchEvent(new CustomEvent('database-action-finished'));
}
});
</script>
@endscript
@@ -65,7 +65,8 @@
</x-slot:content>
</x-process-dialog>
<div x-data="{ deploying: false }" @service-deploy-finished.window="deploying = false">
<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">
@@ -95,8 +96,9 @@
@elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
<x-slot:main x-bind:disabled="deploying"
@click="document.getElementById('service-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5" />
Restart
<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!"
@@ -185,8 +187,9 @@
@elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
<x-slot:main x-bind:disabled="deploying"
@click="document.getElementById('service-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5" />
Restart
<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!"
@@ -319,9 +322,14 @@
return;
}
window.dispatchEvent(new CustomEvent('service-restarting'));
$wire.$dispatch('info',
'Gracefully stopping service.<br/><br/>It could take a while depending on the service.');
$wire.$call('restart');
try {
await $wire.$call('restart');
} finally {
window.dispatchEvent(new CustomEvent('service-deploy-finished'));
}
});
$wire.$on('forceDeployEvent', () => $wire.$call('forceDeploy'));
$wire.$on('pullAndRestartEvent', () => {