mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(ui): consolidate table row actions into one menu pattern
Rows with two or more actions use a shared "..." menu (x-table.row-actions); rows with one action keep a single icon button. Every actions column has an "Actions" header. Removing a member and revoking an invitation now ask for confirmation. Row and card icons are neutral in dark mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bacfd1966
commit
8fcff6ebd1
+23
-16
@@ -2806,6 +2806,12 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Destructive items of a table row action menu (x-table.row-actions). */
|
||||
.row-actions-danger .listbox-option,
|
||||
.dark .row-actions-danger .listbox-option {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Searchable listbox: sticky search field + scrollable options */
|
||||
.searchable-listbox-panel {
|
||||
max-height: 18rem;
|
||||
@@ -3150,7 +3156,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.env-table-grid {
|
||||
grid-template-columns: minmax(14rem, 2.5fr) 4.8rem 6rem 4rem 4.5rem 4.8rem 4.2rem 3rem;
|
||||
grid-template-columns: minmax(14rem, 2.5fr) 4.8rem 6rem 4rem 4.5rem 4.8rem 4.2rem 4.5rem;
|
||||
}
|
||||
|
||||
/* File browser: icon, name, perms, size, modified, actions.
|
||||
@@ -3198,11 +3204,11 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.environment-table-scroll .env-table-grid {
|
||||
min-width: 53rem;
|
||||
min-width: 54.5rem;
|
||||
}
|
||||
|
||||
.env-table-grid.env-table-grid-no-type {
|
||||
grid-template-columns: minmax(14rem, 2.5fr) 4.8rem 4rem 4.5rem 4.8rem 4.2rem 3rem;
|
||||
grid-template-columns: minmax(14rem, 2.5fr) 4.8rem 4rem 4.5rem 4.8rem 4.2rem 4.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -3225,20 +3231,20 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.data-table-row.env-table-grid > :last-child {
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared variables only store value shape (multiline), not per-resource flags. */
|
||||
.env-table-grid-shared {
|
||||
grid-template-columns: minmax(0, 1.6fr) 6rem minmax(0, 1fr) 4.5rem 3rem;
|
||||
grid-template-columns: minmax(0, 1.6fr) 6rem minmax(0, 1fr) 4.5rem 4.5rem;
|
||||
}
|
||||
|
||||
/* Shared env vars collapse secondary columns on tablet and use cards on phone. */
|
||||
@media (max-width: 1100px) {
|
||||
.env-table-grid-shared {
|
||||
grid-template-columns: minmax(0, 1.4fr) 6rem minmax(0, 1fr) 3rem;
|
||||
grid-template-columns: minmax(0, 1.4fr) 6rem minmax(0, 1fr) 4.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -3250,7 +3256,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.env-table-grid-shared {
|
||||
grid-template-columns: minmax(0, 1fr) 6rem 3rem;
|
||||
grid-template-columns: minmax(0, 1fr) 6rem 4.5rem;
|
||||
}
|
||||
|
||||
.env-table-grid-shared > :nth-child(3) {
|
||||
@@ -3505,7 +3511,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.service-backup-table-grid {
|
||||
grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem 6.5rem minmax(8rem, 1fr) 12.5rem;
|
||||
grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem 6.5rem minmax(8rem, 1fr) 4.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3533,11 +3539,11 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.volumes-table-grid {
|
||||
grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) 5rem 15rem;
|
||||
grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) 5rem 4.5rem;
|
||||
}
|
||||
|
||||
.volumes-table-grid-with-pr {
|
||||
grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem 5rem 15rem;
|
||||
grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem 5rem 4.5rem;
|
||||
}
|
||||
|
||||
.volumes-table-grid-readonly.has-source {
|
||||
@@ -3545,11 +3551,11 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.volumes-table-grid.has-source {
|
||||
grid-template-columns: minmax(9rem, 1.1fr) minmax(14rem, 1.5fr) minmax(6rem, 1fr) 5rem 15rem;
|
||||
grid-template-columns: minmax(9rem, 1.1fr) minmax(14rem, 1.5fr) minmax(6rem, 1fr) 5rem 4.5rem;
|
||||
}
|
||||
|
||||
.volumes-table-grid-with-pr.has-source {
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(14rem, 1.4fr) minmax(6rem, 1fr) 8.5rem 5rem 15rem;
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(14rem, 1.4fr) minmax(6rem, 1fr) 8.5rem 5rem 4.5rem;
|
||||
}
|
||||
|
||||
.volumes-mobile-label {
|
||||
@@ -3795,7 +3801,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.server-resources-unmanaged-table-grid {
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(12rem, 1.5fr) 7rem minmax(9rem, auto);
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(12rem, 1.5fr) 7rem 4.5rem;
|
||||
}
|
||||
|
||||
.server-images-table-grid {
|
||||
@@ -6821,13 +6827,14 @@ html.dark .resource-glyph {
|
||||
color: var(--color-brand-light);
|
||||
}
|
||||
|
||||
/* Row and card actions stay neutral: the brand color is for the primary action of an area. */
|
||||
html.dark .card-icon-button {
|
||||
color: var(--color-brand-light);
|
||||
color: var(--color-fg-faint);
|
||||
}
|
||||
|
||||
html.dark .card-icon-button:hover {
|
||||
background-color: color-mix(in srgb, var(--color-brand) 16%, transparent);
|
||||
color: #fff;
|
||||
background-color: rgb(255 255 255 / 0.06);
|
||||
color: var(--color-fg);
|
||||
}
|
||||
|
||||
html.dark .app-main :is(article.group, a.group).rounded-xl {
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<span>Scope</span>
|
||||
<span>{{ count($readOnlyKeys) ? 'Value / comment' : 'Comment' }}</span>
|
||||
<span class="text-center">Multiline</span>
|
||||
<span></span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($variables as $env)
|
||||
@if (in_array($env->key, $readOnlyKeys))
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{{-- One item of <x-table.row-actions>. Renders a link (with wire:navigate when enabled) when href is set. --}}
|
||||
@props([
|
||||
'href' => null,
|
||||
])
|
||||
|
||||
@if ($href)
|
||||
<a href="{{ $href }}" {{ wireNavigate() }} role="menuitem" {{ $attributes->class('listbox-option justify-start!') }}>{{ $slot }}</a>
|
||||
@else
|
||||
<button type="button" role="menuitem" {{ $attributes->class('listbox-option justify-start!') }}>{{ $slot }}</button>
|
||||
@endif
|
||||
@@ -0,0 +1,27 @@
|
||||
{{-- Row action menu for tables: a "..." icon button at the far right of the row.
|
||||
Put <x-table.row-action> items in the default slot. Put destructive items in
|
||||
the danger slot: they show in red, below a divider. A click on any item
|
||||
closes the menu, also when the item opens a modal. --}}
|
||||
@props([
|
||||
'label' => 'Actions',
|
||||
])
|
||||
|
||||
<x-table.dropdown panel-class="w-44!" role="menu">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="icon-button" title="{{ $label }}" aria-label="{{ $label }}"
|
||||
aria-haspopup="menu" :aria-expanded="open">
|
||||
<x-reicon name="dots-horizontal" class="size-4" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<div class="contents" @click="if ($event.target.closest('[role=menuitem]')) close()">
|
||||
{{ $slot }}
|
||||
@if (isset($danger) && $danger->isNotEmpty())
|
||||
@if ($slot->isNotEmpty())
|
||||
<div class="my-1 border-t border-neutral-200 dark:border-white/[0.08]"></div>
|
||||
@endif
|
||||
<div class="row-actions-danger contents">
|
||||
{{ $danger }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
@@ -64,7 +64,7 @@
|
||||
<span>Name</span>
|
||||
<span>Email</span>
|
||||
<span>Subscription</span>
|
||||
<span class="text-right">Action</span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($foundUsers as $user)
|
||||
@php
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
<div x-show="viewMode === 'table'" class="{{ $tableShell }} transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
|
||||
wire:target="setProjectSort">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_4rem] gap-4 @2xl:min-w-[560px] @2xl:grid-cols-[minmax(0,1.2fr)_7rem_minmax(0,1.5fr)_4rem] {{ $tableHead }}">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_4.5rem] gap-4 @2xl:min-w-[560px] @2xl:grid-cols-[minmax(0,1.2fr)_7rem_minmax(0,1.5fr)_4.5rem] {{ $tableHead }}">
|
||||
<x-table.sort-header label="Project" asc="name-asc" desc="name-desc"
|
||||
action="setProjectSort" :current="$projectSort" />
|
||||
<x-table.sort-header label="Contents" asc="resources-asc" desc="resources-desc" first="desc"
|
||||
@@ -171,14 +171,14 @@
|
||||
<x-table.sort-header label="Description" asc="description-asc" desc="description-desc"
|
||||
action="setProjectSort" :current="$projectSort" />
|
||||
</div>
|
||||
<div></div>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($dashboardProjects as $project)
|
||||
@php
|
||||
$firstEnvironment = $project->environments->first();
|
||||
@endphp
|
||||
<div wire:key="dashboard-project-row-{{ $project->uuid }}"
|
||||
class="relative grid grid-cols-[minmax(0,1fr)_7rem_4rem] gap-4 @2xl:min-w-[560px] @2xl:grid-cols-[minmax(0,1.2fr)_7rem_minmax(0,1.5fr)_4rem] {{ $tableRow }}">
|
||||
class="relative grid grid-cols-[minmax(0,1fr)_7rem_4.5rem] gap-4 @2xl:min-w-[560px] @2xl:grid-cols-[minmax(0,1.2fr)_7rem_minmax(0,1.5fr)_4.5rem] {{ $tableRow }}">
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }}
|
||||
class="absolute inset-0" aria-label="Open {{ $project->name }}"></a>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
|
||||
@@ -131,14 +131,14 @@
|
||||
|
||||
<div class="service-domain-actions flex items-center justify-end gap-1">
|
||||
@can('update', $application)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns"
|
||||
class="icon-button shrink-0" title="Check DNS" aria-label="Check DNS">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
@unless ($labelsAreWritable)
|
||||
@if ($isSuggested)
|
||||
@if ($labelsAreWritable || $isSuggested)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns"
|
||||
class="icon-button shrink-0" title="Check DNS" aria-label="Check DNS">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
@if ($isSuggested && ! $labelsAreWritable)
|
||||
@if ($row['needs_force_add'] ?? false)
|
||||
<x-forms.button wire:click="addSuggestedDomain({{ $index }})" isError class="h-7! px-2! text-[12px]!">
|
||||
Continue
|
||||
@@ -148,29 +148,34 @@
|
||||
Add domain
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
<button type="button"
|
||||
@click="openEditDomain(@js($index), @js($row['url']), @js($editingParts), @js($row['service']), @js($isNoindexed ? 'noindex' : 'index'), @js($rowDirection), @js($row['inherited_port'] ?? null))"
|
||||
class="icon-button shrink-0"
|
||||
title="Domain settings" aria-label="Settings for {{ $publicUrl }}">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<x-modal-confirmation class="!w-auto shrink-0" title="Remove domain?" buttonTitle="Remove"
|
||||
isErrorButton canGate="update" :canResource="$application"
|
||||
submitAction="removeDomainByKey({{ $domainKey }})" :actions="[
|
||||
'This domain will be removed from the application.',
|
||||
'Redeploy or restart may be required for proxy changes.',
|
||||
]" :checkboxes="[['id' => 'deleteManagedDns', 'label' => 'Also delete the DNS record created by Coolify, if present.']]"
|
||||
:confirmWithPassword="false" :confirmWithText="false" step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Remove domain" aria-label="Remove domain">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
@endunless
|
||||
@else
|
||||
<x-table.row-actions label="Domain actions">
|
||||
<x-table.row-action wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns">
|
||||
Check DNS
|
||||
</x-table.row-action>
|
||||
<x-table.row-action
|
||||
x-on:click="openEditDomain({{ \Illuminate\Support\Js::from($index) }}, {{ \Illuminate\Support\Js::from($row['url']) }}, {{ \Illuminate\Support\Js::from($editingParts) }}, {{ \Illuminate\Support\Js::from($row['service']) }}, {{ \Illuminate\Support\Js::from($isNoindexed ? 'noindex' : 'index') }}, {{ \Illuminate\Support\Js::from($rowDirection) }}, {{ \Illuminate\Support\Js::from($row['inherited_port'] ?? null) }})"
|
||||
aria-label="Edit {{ $publicUrl }}">
|
||||
Edit
|
||||
</x-table.row-action>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation buttonFullWidth title="Remove domain?" buttonTitle="Remove"
|
||||
isErrorButton canGate="update" :canResource="$application"
|
||||
submitAction="removeDomainByKey({{ $domainKey }})" :actions="[
|
||||
'This domain will be removed from the application.',
|
||||
'Redeploy or restart may be required for proxy changes.',
|
||||
]" :checkboxes="[['id' => 'deleteManagedDns', 'label' => 'Also delete the DNS record created by Coolify, if present.']]"
|
||||
:confirmWithPassword="false" :confirmWithText="false" step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,34 +177,33 @@
|
||||
</div>
|
||||
<div class="service-domain-actions flex items-center justify-end gap-1">
|
||||
@can('update', $preview->application)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns"
|
||||
class="icon-button shrink-0" title="Check DNS" aria-label="Check DNS">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="openEditDomain(@js($index), @js($row['url']), @js($editingParts), @js($row['service']))"
|
||||
class="icon-button shrink-0" title="Domain settings" aria-label="Settings for {{ getFqdnWithoutPort($row['url']) }}">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<x-modal-confirmation class="!w-auto shrink-0" title="Remove domain?"
|
||||
buttonTitle="Remove" isErrorButton
|
||||
submitAction="removeDomainByKey({{ $domainKey }})"
|
||||
:actions="[
|
||||
'This domain will be removed from the preview deployment.',
|
||||
'Redeploy the preview to apply proxy changes.',
|
||||
]"
|
||||
:confirmWithPassword="false" :confirmWithText="false"
|
||||
step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Remove domain" aria-label="Remove domain">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-table.row-actions label="Domain actions">
|
||||
<x-table.row-action wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns">
|
||||
Check DNS
|
||||
</x-table.row-action>
|
||||
<x-table.row-action
|
||||
x-on:click="openEditDomain({{ \Illuminate\Support\Js::from($index) }}, {{ \Illuminate\Support\Js::from($row['url']) }}, {{ \Illuminate\Support\Js::from($editingParts) }}, {{ \Illuminate\Support\Js::from($row['service']) }})"
|
||||
aria-label="Edit {{ getFqdnWithoutPort($row['url']) }}">
|
||||
Edit
|
||||
</x-table.row-action>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation buttonFullWidth title="Remove domain?"
|
||||
buttonTitle="Remove" isErrorButton
|
||||
submitAction="removeDomainByKey({{ $domainKey }})"
|
||||
:actions="[
|
||||
'This domain will be removed from the preview deployment.',
|
||||
'Redeploy the preview to apply proxy changes.',
|
||||
]"
|
||||
:confirmWithPassword="false" :confirmWithText="false"
|
||||
step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<span>Latest run</span>
|
||||
<span>S3 storage</span>
|
||||
<span>Executions</span>
|
||||
<span class="text-right">Action</span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($database->scheduledBackups as $backup)
|
||||
@php
|
||||
@@ -87,9 +87,10 @@
|
||||
|| @js(strtolower($s3StorageNames)).includes(search.toLowerCase())"
|
||||
class="data-table-row scheduled-backups-table-grid border-b border-neutral-200 last:border-b-0 dark:border-white/[0.06]">
|
||||
<div class="min-w-0">
|
||||
<span class="block truncate text-[12px] font-semibold text-black dark:text-fg">
|
||||
<a {{ wireNavigate() }} href="{{ $backupRoute }}"
|
||||
class="block truncate text-[12px] font-semibold text-black hover:underline dark:text-fg">
|
||||
{{ $backup->frequency }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
||||
@@ -107,7 +108,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a class="button" {{ wireNavigate() }} href="{{ $backupRoute }}">Manage</a>
|
||||
<a class="icon-button" {{ wireNavigate() }} href="{{ $backupRoute }}"
|
||||
title="Manage backup schedule" aria-label="Manage backup schedule">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<x-table.sort-header label="Created" asc="created-asc" desc="created-desc" first="desc" then="page = 1" />
|
||||
<x-table.sort-header label="Description" asc="description-asc" desc="description-desc" then="page = 1"
|
||||
class="project-description" />
|
||||
<div></div>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
|
||||
<template x-for="project in paginatedProjects" :key="project.uuid">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
@else
|
||||
<div class="data-table relative w-full overflow-x-auto">
|
||||
<x-table.loading target="previousPage,nextPage,setPage,perPage" text="Loading executions..." />
|
||||
<div class="data-table-header grid min-w-[900px] grid-cols-[minmax(150px,1.4fr)_100px_100px_110px_190px_90px_48px]">
|
||||
<div class="data-table-header grid min-w-[900px] grid-cols-[minmax(150px,1.4fr)_100px_100px_110px_190px_90px_4.5rem]">
|
||||
<span>Target</span><span>Type</span><span>Schedule</span><span>Status</span><span>Started</span><span>Size</span><span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($executions as $execution)
|
||||
@@ -56,7 +56,7 @@
|
||||
<div wire:key="service-backup-execution-{{ $execution['id'] }}"
|
||||
wire:click="openExecution('{{ $execution['uuid'] }}')"
|
||||
wire:keydown.enter="openExecution('{{ $execution['uuid'] }}')" role="button" tabindex="0"
|
||||
class="data-table-row grid min-w-[900px] cursor-pointer grid-cols-[minmax(150px,1.4fr)_100px_100px_110px_190px_90px_48px] text-left text-[13px] text-neutral-700 dark:text-fg-dim">
|
||||
class="data-table-row grid min-w-[900px] cursor-pointer grid-cols-[minmax(150px,1.4fr)_100px_100px_110px_190px_90px_4.5rem] text-left text-[13px] text-neutral-700 dark:text-fg-dim">
|
||||
<span class="flex min-w-0 items-center gap-2 font-medium text-neutral-950 dark:text-fg">
|
||||
<span class="truncate" title="{{ $execution['target'] }}">{{ $execution['target'] }}</span>
|
||||
<span tabindex="0" data-tooltip="{{ $execution['s3_tooltip'] }}"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<div>Resource</div>
|
||||
<div>Image</div>
|
||||
<div class="justify-self-start">Status</div>
|
||||
<div></div>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -165,13 +165,13 @@
|
||||
|
||||
<div class="service-domain-actions flex items-center justify-end gap-1">
|
||||
@can('update', $service)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns"
|
||||
class="icon-button shrink-0" title="Check DNS" aria-label="Check DNS">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
@if ($isSuggested)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns"
|
||||
class="icon-button shrink-0" title="Check DNS" aria-label="Check DNS">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
@if ($row['needs_force_add'] ?? false)
|
||||
<x-forms.button canGate="update" :canResource="$service"
|
||||
wire:click="addSuggestedDomain({{ $index }})" isError
|
||||
@@ -186,27 +186,32 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
<button type="button" class="icon-button shrink-0" title="Domain settings" aria-label="Settings for {{ $publicUrl }}"
|
||||
@click="openEditDomain(@js($index), @js($row['url']), @js($editingParts), @js((int) $row['service_application_id']), @js($serviceLabel), @js($isNoindexed ? 'noindex' : 'index'), @js($rowDirection))">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<x-modal-confirmation class="!w-auto shrink-0" title="Remove domain?"
|
||||
buttonTitle="Remove" isErrorButton
|
||||
canGate="update" :canResource="$service"
|
||||
submitAction="removeDomainByKey({{ $domainKey }})" :actions="[
|
||||
'This domain will be removed from the service application.',
|
||||
'Redeploy or restart may be required for proxy changes.',
|
||||
]" :checkboxes="[['id' => 'deleteManagedDns', 'label' => 'Also delete the DNS record created by Coolify, if present.']]"
|
||||
:confirmWithPassword="false" :confirmWithText="false"
|
||||
step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Remove domain" aria-label="Remove domain">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-table.row-actions label="Domain actions">
|
||||
<x-table.row-action wire:click="checkDomainDns({{ $index }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="checkDomainDns({{ $index }}),checkAllDns">
|
||||
Check DNS
|
||||
</x-table.row-action>
|
||||
<x-table.row-action aria-label="Edit {{ $publicUrl }}"
|
||||
x-on:click="openEditDomain({{ \Illuminate\Support\Js::from($index) }}, {{ \Illuminate\Support\Js::from($row['url']) }}, {{ \Illuminate\Support\Js::from($editingParts) }}, {{ \Illuminate\Support\Js::from((int) $row['service_application_id']) }}, {{ \Illuminate\Support\Js::from($serviceLabel) }}, {{ \Illuminate\Support\Js::from($isNoindexed ? 'noindex' : 'index') }}, {{ \Illuminate\Support\Js::from($rowDirection) }})">
|
||||
Edit
|
||||
</x-table.row-action>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation buttonFullWidth title="Remove domain?"
|
||||
buttonTitle="Remove" isErrorButton
|
||||
canGate="update" :canResource="$service"
|
||||
submitAction="removeDomainByKey({{ $domainKey }})" :actions="[
|
||||
'This domain will be removed from the service application.',
|
||||
'Redeploy or restart may be required for proxy changes.',
|
||||
]" :checkboxes="[['id' => 'deleteManagedDns', 'label' => 'Also delete the DNS record created by Coolify, if present.']]"
|
||||
:confirmWithPassword="false" :confirmWithText="false"
|
||||
step2ButtonText="Remove domain">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<x-slot:content>
|
||||
<div x-data="{
|
||||
openSettings(event) {
|
||||
if (event.target.closest('a, button')) {
|
||||
if (event.target.closest('a, button, [role=menu]')) {
|
||||
return;
|
||||
}
|
||||
modalOpen = true;
|
||||
@@ -108,25 +108,27 @@
|
||||
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
||||
<x-application.restart-limit-warning :application="$resource" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
||||
<a class="icon-button" title="Service backups" aria-label="Service backups" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.volume-backups.index', $parameters) }}">
|
||||
<x-reicon name="database" class="size-4" />
|
||||
</a>
|
||||
@endif
|
||||
@if ($isApplication && $resource->fqdn)
|
||||
@can('update', $service)
|
||||
<a class="icon-button" title="Manage domains" aria-label="Manage domains" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.domains', $parameters) }}">
|
||||
<x-reicon name="globe" class="size-4" />
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
@php
|
||||
$hasBackupsAction = $isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated);
|
||||
$hasDomainsAction = $isApplication && $resource->fqdn && auth()->user()?->can('update', $service);
|
||||
@endphp
|
||||
<div class="flex items-center justify-end">
|
||||
@if ($hasBackupsAction || $hasDomainsAction)
|
||||
<x-table.row-actions label="Resource actions">
|
||||
@if ($hasBackupsAction)
|
||||
<x-table.row-action :href="route('project.service.volume-backups.index', $parameters)">Backups</x-table.row-action>
|
||||
@endif
|
||||
@if ($hasDomainsAction)
|
||||
<x-table.row-action :href="route('project.service.domains', $parameters)">Domains</x-table.row-action>
|
||||
@endif
|
||||
<x-table.row-action @click="modalOpen = true">Settings</x-table.row-action>
|
||||
</x-table.row-actions>
|
||||
@else
|
||||
<button type="button" class="icon-button" title="Resource settings" aria-label="Resource settings"
|
||||
@click="modalOpen = true">
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,18 +290,17 @@
|
||||
:data-tooltip="$databaseS3Tooltip" :aria-label="$databaseS3Tooltip" tabindex="0" />
|
||||
</span>
|
||||
<span>{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}</span>
|
||||
<span class="flex justify-end gap-2" x-on:keydown.enter.stop>
|
||||
<x-forms.button type="button" canGate="update" :canResource="$service"
|
||||
:disabled="! str($databaseBackup->database->status)->startsWith('running')"
|
||||
:tooltip="! str($databaseBackup->database->status)->startsWith('running') ? 'The database must be running to start a backup.' : null"
|
||||
wire:click.stop="backupNow('database', '{{ $databaseBackup->uuid }}')"
|
||||
wire:target="backupNow('database', '{{ $databaseBackup->uuid }}')">Back up now</x-forms.button>
|
||||
<x-forms.button type="button" canGate="update" :canResource="$service"
|
||||
defaultClass="icon-button shrink-0" :showLoadingIndicator="false"
|
||||
title="Edit backup schedule" aria-label="Edit backup schedule"
|
||||
wire:click.stop="openSchedule('{{ $databaseBackup->uuid }}')">
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</x-forms.button>
|
||||
<span class="flex justify-end" x-on:click.stop x-on:keydown.enter.stop>
|
||||
@can('update', $service)
|
||||
<x-table.row-actions label="Backup schedule actions">
|
||||
<x-table.row-action
|
||||
:disabled="! str($databaseBackup->database->status)->startsWith('running')"
|
||||
:title="! str($databaseBackup->database->status)->startsWith('running') ? 'The database must be running to start a backup.' : null"
|
||||
wire:click="backupNow('database', '{{ $databaseBackup->uuid }}')">Back up now</x-table.row-action>
|
||||
<x-table.row-action
|
||||
wire:click="openSchedule('{{ $databaseBackup->uuid }}')">Edit</x-table.row-action>
|
||||
</x-table.row-actions>
|
||||
@endcan
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -351,16 +350,15 @@
|
||||
<span>
|
||||
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
||||
</span>
|
||||
<span class="flex justify-end gap-2" x-on:keydown.enter.stop>
|
||||
<x-forms.button type="button" canGate="update" :canResource="$service"
|
||||
wire:click.stop="backupNow('storage', '{{ $backup->uuid }}')"
|
||||
wire:target="backupNow('storage', '{{ $backup->uuid }}')">Back up now</x-forms.button>
|
||||
<x-forms.button type="button" canGate="update" :canResource="$service"
|
||||
defaultClass="icon-button shrink-0" :showLoadingIndicator="false"
|
||||
title="Edit backup schedule" aria-label="Edit backup schedule"
|
||||
wire:click.stop="openSchedule('{{ $backup->uuid }}')">
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</x-forms.button>
|
||||
<span class="flex justify-end" x-on:click.stop x-on:keydown.enter.stop>
|
||||
@can('update', $service)
|
||||
<x-table.row-actions label="Backup schedule actions">
|
||||
<x-table.row-action
|
||||
wire:click="backupNow('storage', '{{ $backup->uuid }}')">Back up now</x-table.row-action>
|
||||
<x-table.row-action
|
||||
wire:click="openSchedule('{{ $backup->uuid }}')">Edit</x-table.row-action>
|
||||
</x-table.row-actions>
|
||||
@endcan
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
<span class="text-center">Multiline</span>
|
||||
<span class="text-center">Buildtime</span>
|
||||
<span class="text-center">Runtime</span>
|
||||
<span></span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@php $renderedScope = null; @endphp
|
||||
@foreach ($this->environmentVariablePageRows as $row)
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
<div class="flex items-center gap-0.5 justify-self-end">
|
||||
<div class="flex items-center justify-end gap-0.5 justify-self-end">
|
||||
@unless ($isValueHidden)
|
||||
<x-copy-button resolve="$wire.copyValue()" label="Copy value" />
|
||||
@endunless
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
<div class="flex items-center gap-0.5 justify-self-end">
|
||||
<div class="flex items-center justify-end gap-0.5 justify-self-end">
|
||||
@if ($this->connectLink)
|
||||
<a href="{{ $this->connectLink }}" class="icon-button shrink-0"
|
||||
title="Connect {{ $key }} to resources" aria-label="Connect {{ $key }} to resources">
|
||||
|
||||
@@ -269,81 +269,56 @@
|
||||
<div class="text-coollabs-subtle tabular-nums">
|
||||
{{ $entry['mtime'] ? \Illuminate\Support\Carbon::createFromTimestamp($entry['mtime'])->toDateTimeString() : '-' }}
|
||||
</div>
|
||||
{{-- Actions kebab. The entry name lives in this
|
||||
row's x-data as a JS string so the menu can
|
||||
call $wire.* with it directly - passing @js()
|
||||
through a component attribute double-encodes
|
||||
the quotes and breaks the handler. --}}
|
||||
{{-- The entry name lives in this row's x-data as a JS
|
||||
string so the menu can call $wire.* with it directly -
|
||||
passing @js() through a component attribute
|
||||
double-encodes the quotes and breaks the handler. --}}
|
||||
<div class="flex items-center justify-end"
|
||||
x-data="{ open: false, name: @js($entry['name']), isDir: @js($entry['type'] === 'dir') }"
|
||||
@keydown.escape.window="open = false">
|
||||
<div class="relative">
|
||||
<button type="button" title="Actions" aria-label="Actions" x-ref="trigger"
|
||||
x-on:click="open = !open" x-bind:aria-expanded="open"
|
||||
x-bind:class="open && 'bg-black/5 text-current dark:bg-white/[0.08]'"
|
||||
class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-coollabs-subtle hover:bg-black/5 hover:text-current focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs/40 dark:hover:bg-white/[0.06] dark:focus-visible:ring-white/30">
|
||||
<x-reicon name="dots-horizontal" class="size-4" />
|
||||
</button>
|
||||
<template x-teleport="body">
|
||||
<div x-show="open" x-cloak x-transition.opacity.duration.120ms
|
||||
x-anchor.bottom-end.fixed.offset.4="$refs.trigger"
|
||||
@click.outside="open = false"
|
||||
class="listbox-panel min-w-52! w-52! z-50!">
|
||||
@if ($entry['type'] !== 'dir')
|
||||
<button type="button" class="listbox-option"
|
||||
x-on:click="open = false; run('Opening file...', () => $wire.openEditor(name))">
|
||||
<x-reicon name="file-content" class="size-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 text-left">Edit</span>
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="listbox-option"
|
||||
x-on:click="open = false; run(isDir ? 'Preparing folder download...' : 'Preparing file download...', () => $wire.download(name))">
|
||||
<x-reicon name="download" class="size-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 text-left">Download</span>
|
||||
</button>
|
||||
|
||||
{{-- Rename --}}
|
||||
<x-modal-input title="Rename">
|
||||
<x-slot:content>
|
||||
<button type="button" class="listbox-option" x-on:click="open = false">
|
||||
<x-reicon name="edit" class="size-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 text-left">Rename</span>
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<form x-data="{ newName: @js($entry['name']) }"
|
||||
@submit.prevent="run('Renaming...', () => $wire.renameEntry(name, newName)); $dispatch('close-modal')"
|
||||
class="flex flex-col gap-4">
|
||||
<x-forms.input id="renameName-{{ $loop->index }}"
|
||||
label="New name" x-model="newName" required />
|
||||
<div class="flex justify-end">
|
||||
<x-forms.button type="submit" isHighlighted canGate="canAccessTerminal" :canResource="$resource">Rename</x-forms.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal-input>
|
||||
|
||||
{{-- Delete --}}
|
||||
<x-modal-input title="Delete this item?">
|
||||
<x-slot:content>
|
||||
<button type="button" class="listbox-option" x-on:click="open = false">
|
||||
<x-reicon name="trash" class="size-3.5 shrink-0 text-red-600 dark:text-red-500" />
|
||||
<span class="min-w-0 flex-1 text-left text-red-600 dark:text-red-500">Delete</span>
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-coollabs-subtle">
|
||||
This permanently deletes
|
||||
<span class="font-medium">{{ $entry['name'] }}</span>
|
||||
inside the container. This cannot be undone.
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<x-forms.button isError type="button" canGate="canAccessTerminal" :canResource="$resource"
|
||||
x-on:click="run(isDir ? 'Deleting folder...' : 'Deleting file...', () => $wire.deleteEntry(name)); $dispatch('close-modal')">Delete</x-forms.button>
|
||||
</div>
|
||||
x-data="{ name: @js($entry['name']), isDir: @js($entry['type'] === 'dir') }">
|
||||
<x-table.row-actions label="File actions">
|
||||
@if ($entry['type'] !== 'dir')
|
||||
<x-table.row-action
|
||||
x-on:click="run('Opening file...', () => $wire.openEditor(name))">
|
||||
Edit
|
||||
</x-table.row-action>
|
||||
@endif
|
||||
<x-table.row-action
|
||||
x-on:click="run(isDir ? 'Preparing folder download...' : 'Preparing file download...', () => $wire.download(name))">
|
||||
Download
|
||||
</x-table.row-action>
|
||||
<x-modal-input title="Rename">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Rename</x-table.row-action>
|
||||
</x-slot:content>
|
||||
<form x-data="{ newName: @js($entry['name']) }"
|
||||
@submit.prevent="run('Renaming...', () => $wire.renameEntry(name, newName)); $dispatch('close-modal')"
|
||||
class="flex flex-col gap-4">
|
||||
<x-forms.input id="renameName-{{ $loop->index }}"
|
||||
label="New name" x-model="newName" required />
|
||||
<div class="flex justify-end">
|
||||
<x-forms.button type="submit" isHighlighted canGate="canAccessTerminal" :canResource="$resource">Rename</x-forms.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal-input>
|
||||
<x-slot:danger>
|
||||
<x-modal-input title="Delete this item?">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-coollabs-subtle">
|
||||
This permanently deletes
|
||||
<span class="font-medium">{{ $entry['name'] }}</span>
|
||||
inside the container. This cannot be undone.
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<x-forms.button isError type="button" canGate="canAccessTerminal" :canResource="$resource"
|
||||
x-on:click="run(isDir ? 'Deleting folder...' : 'Deleting file...', () => $wire.deleteEntry(name)); $dispatch('close-modal')">Delete</x-forms.button>
|
||||
</div>
|
||||
</x-modal-input>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-modal-input>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -126,13 +126,13 @@
|
||||
@if ($showBackupAction)
|
||||
<div
|
||||
class="volumes-col-actions volumes-cell-actions flex flex-nowrap items-center justify-end gap-1.5">
|
||||
@if ($canUpdate)
|
||||
@if ($canUpdate && ! $form['canDeleteStale'] && ! $form['canDeleteToApplyDriverOptions'])
|
||||
<x-modal-input title="Configure Volume Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-forms.button type="button" class="!px-2.5 !text-xs" canGate="update"
|
||||
:canResource="$resource">
|
||||
Backup
|
||||
</x-forms.button>
|
||||
<button type="button" class="icon-button shrink-0" title="Configure volume backup"
|
||||
aria-label="Configure backup for {{ $form['name'] }}">
|
||||
<x-reicon name="database" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:content>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
@@ -144,43 +144,72 @@
|
||||
wire:key="configure-service-volume-backup-{{ $id }}" />
|
||||
@endif
|
||||
</x-modal-input>
|
||||
@elseif ($canUpdate || $form['canDeleteStale'] || $form['canDeleteToApplyDriverOptions'])
|
||||
<x-table.row-actions label="Storage actions">
|
||||
@if ($canUpdate)
|
||||
<x-modal-input title="Configure Volume Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Backup</x-table.row-action>
|
||||
</x-slot:content>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-volume-backup-{{ $id }}" />
|
||||
@else
|
||||
<livewire:project.service.volume-backup.create :service="$resource->service"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-service-volume-backup-{{ $id }}" />
|
||||
@endif
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@if ($form['canDeleteStale'] || $form['canDeleteToApplyDriverOptions'])
|
||||
<x-slot:danger>
|
||||
@if ($form['canDeleteStale'])
|
||||
<x-modal-confirmation title="Remove stale volume entry?" buttonFullWidth
|
||||
submitAction="delete({{ $id }})"
|
||||
:checkboxes="[[
|
||||
'id' => 'deleteDockerVolume',
|
||||
'label' => 'Also permanently delete the Docker volume and all its data.',
|
||||
'default_warning' => 'The Docker volume and its data will not be deleted.',
|
||||
]]"
|
||||
:actions="array_values(array_filter([
|
||||
'This removes only the stale volume entry from Coolify.',
|
||||
$form['replacedExternalVolume']
|
||||
? 'The next deployment uses the external volume \''.$form['replacedExternalVolume'].'\' instead of this volume.'
|
||||
: null,
|
||||
]))" confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete stale entry</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
@if ($form['canDeleteToApplyDriverOptions'])
|
||||
<x-modal-confirmation title="Delete volume entry to apply driver options?" buttonFullWidth
|
||||
submitAction="delete({{ $id }})"
|
||||
:checkboxes="[[
|
||||
'id' => 'deleteDockerVolume',
|
||||
'label' => 'Permanently delete the Docker volume and all data in it. The next deployment creates the volume again with the driver options.',
|
||||
'default_warning' => 'The Docker volume and its data stay on the server. The driver options are not applied, because Docker keeps using the existing volume.',
|
||||
]]"
|
||||
:actions="[
|
||||
'Remove the storage entry from Coolify.',
|
||||
]"
|
||||
warningMessage="Stop the resource first: Docker cannot delete a volume that a running container uses. If the volume contains data you need, use Backup before you delete it." confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
</x-slot:danger>
|
||||
@endif
|
||||
</x-table.row-actions>
|
||||
@else
|
||||
<span class="text-neutral-400 dark:text-fg-faint">—</span>
|
||||
@endif
|
||||
|
||||
@if ($form['canDeleteStale'])
|
||||
<x-modal-confirmation title="Remove stale volume entry?" isErrorButton
|
||||
buttonTitle="Delete stale volume entry" submitAction="delete({{ $id }})"
|
||||
:checkboxes="[[
|
||||
'id' => 'deleteDockerVolume',
|
||||
'label' => 'Also permanently delete the Docker volume and all its data.',
|
||||
'default_warning' => 'The Docker volume and its data will not be deleted.',
|
||||
]]"
|
||||
:actions="array_values(array_filter([
|
||||
'This removes only the stale volume entry from Coolify.',
|
||||
$form['replacedExternalVolume']
|
||||
? 'The next deployment uses the external volume \''.$form['replacedExternalVolume'].'\' instead of this volume.'
|
||||
: null,
|
||||
]))" confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name" />
|
||||
@endif
|
||||
|
||||
@if ($form['canDeleteToApplyDriverOptions'])
|
||||
<x-modal-confirmation title="Delete volume entry to apply driver options?" isErrorButton
|
||||
buttonTitle="Delete" submitAction="delete({{ $id }})"
|
||||
:checkboxes="[[
|
||||
'id' => 'deleteDockerVolume',
|
||||
'label' => 'Permanently delete the Docker volume and all data in it. The next deployment creates the volume again with the driver options.',
|
||||
'default_warning' => 'The Docker volume and its data stay on the server. The driver options are not applied, because Docker keeps using the existing volume.',
|
||||
]]"
|
||||
:actions="[
|
||||
'Remove the storage entry from Coolify.',
|
||||
]"
|
||||
warningMessage="Stop the resource first: Docker cannot delete a volume that a running container uses. If the volume contains data you need, use Backup before you delete it." confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -258,50 +287,63 @@
|
||||
|
||||
<div
|
||||
class="volumes-col-actions volumes-cell-actions flex flex-nowrap items-center justify-end gap-1.5">
|
||||
<x-forms.button type="submit" class="!px-2.5 !text-xs">
|
||||
Update
|
||||
</x-forms.button>
|
||||
|
||||
@if ($showBackupAction && ! $form['isShared'])
|
||||
<x-modal-input title="Configure Volume Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-forms.button type="button" class="!px-2.5 !text-xs" canGate="update"
|
||||
:canResource="$resource">
|
||||
Backup
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-volume-backup-{{ $id }}" />
|
||||
@else
|
||||
<livewire:project.service.volume-backup.create :service="$resource->service"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-service-volume-backup-{{ $id }}" />
|
||||
@endif
|
||||
</x-modal-input>
|
||||
@elseif (method_exists($resource, 'isBackupSolutionAvailable') && $resource->isBackupSolutionAvailable())
|
||||
<x-modal-input title="New Scheduled Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-forms.button type="button" class="!px-2.5 !text-xs" canGate="update"
|
||||
:canResource="$resource">
|
||||
Backup
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
<livewire:project.database.create-scheduled-backup :database="$resource"
|
||||
wire:key="configure-database-backup-{{ $id }}" />
|
||||
</x-modal-input>
|
||||
<button type="submit" class="sr-only" tabindex="-1" aria-hidden="true">Update</button>
|
||||
@php
|
||||
$hasBackupAction = auth()->user()?->can('update', $resource)
|
||||
&& (($showBackupAction && ! $form['isShared'])
|
||||
|| (method_exists($resource, 'isBackupSolutionAvailable') && $resource->isBackupSolutionAvailable()));
|
||||
@endphp
|
||||
@if ($form['isShared'] && ! $hasBackupAction)
|
||||
<button type="button" wire:click="submit({{ $id }})" class="icon-button shrink-0"
|
||||
title="Update storage" aria-label="Update {{ $form['name'] }}">
|
||||
<x-reicon name="check" class="size-3.5" />
|
||||
</button>
|
||||
@else
|
||||
<x-table.row-actions label="Storage actions">
|
||||
<x-table.row-action wire:click="submit({{ $id }})">Update</x-table.row-action>
|
||||
@can('update', $resource)
|
||||
@if ($showBackupAction && ! $form['isShared'])
|
||||
<x-modal-input title="Configure Volume Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Backup</x-table.row-action>
|
||||
</x-slot:content>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
<livewire:project.application.backup.create :application="$resource"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-volume-backup-{{ $id }}" />
|
||||
@else
|
||||
<livewire:project.service.volume-backup.create :service="$resource->service"
|
||||
:selected-target-key="'volume:' . $id"
|
||||
wire:key="configure-service-volume-backup-{{ $id }}" />
|
||||
@endif
|
||||
</x-modal-input>
|
||||
@elseif (method_exists($resource, 'isBackupSolutionAvailable') && $resource->isBackupSolutionAvailable())
|
||||
<x-modal-input title="New Scheduled Backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Backup</x-table.row-action>
|
||||
</x-slot:content>
|
||||
<livewire:project.database.create-scheduled-backup :database="$resource"
|
||||
wire:key="configure-database-backup-{{ $id }}" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@endcan
|
||||
@unless ($form['isShared'])
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Confirm persistent storage deletion?" buttonFullWidth
|
||||
submitAction="delete({{ $id }})" :actions="[
|
||||
'The selected persistent storage/volume will be permanently deleted.',
|
||||
'If the persistent storage/volume is actvily used by a resource data will be lost.',
|
||||
]" confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
@endunless
|
||||
</x-table.row-actions>
|
||||
@endif
|
||||
|
||||
@unless ($form['isShared'])
|
||||
<x-modal-confirmation title="Confirm persistent storage deletion?" isErrorButton
|
||||
buttonTitle="Delete" submitAction="delete({{ $id }})" :actions="[
|
||||
'The selected persistent storage/volume will be permanently deleted.',
|
||||
'If the persistent storage/volume is actvily used by a resource data will be lost.',
|
||||
]" confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name" />
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+20
-3
@@ -146,15 +146,32 @@
|
||||
<x-reicon name="upload" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
@endif
|
||||
@if ($recoveryPending)
|
||||
@if ($recoveryPending && $execution->status !== 'running')
|
||||
<x-table.row-actions label="Backup actions">
|
||||
<x-table.row-action wire:click="retryRecovery({{ $execution->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="retryRecovery({{ $execution->id }})">Retry recovery</x-table.row-action>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Confirm Backup Deletion?" buttonFullWidth
|
||||
submitAction="deleteBackup({{ $execution->id }})"
|
||||
:checkboxes="$executionCheckboxes" :actions="$deleteActions"
|
||||
confirmationText="{{ $execution->filename }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
|
||||
shortConfirmationLabel="Backup Filename">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@elseif ($recoveryPending)
|
||||
<button type="button" class="icon-button shrink-0"
|
||||
wire:click="retryRecovery({{ $execution->id }})"
|
||||
wire:loading.attr="disabled" wire:target="retryRecovery({{ $execution->id }})"
|
||||
title="Retry recovery" aria-label="Retry recovery">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
@endif
|
||||
@if ($execution->status !== 'running')
|
||||
@elseif ($execution->status !== 'running')
|
||||
<x-modal-confirmation title="Confirm Backup Deletion?" isErrorButton
|
||||
submitAction="deleteBackup({{ $execution->id }})"
|
||||
:checkboxes="$executionCheckboxes" :actions="$deleteActions"
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
class="environment-resource-count" />
|
||||
<x-table.sort-header label="Description" asc="description-asc" desc="description-desc" then="page = 1"
|
||||
class="environment-description" />
|
||||
<div></div>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
|
||||
<template x-for="environment in paginatedEnvironments" :key="environment.uuid">
|
||||
|
||||
@@ -187,23 +187,15 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
@if (auth()->id() === $token->tokenable_id)
|
||||
<x-table.dropdown panel-class="w-36!" role="menu">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="icon-button" title="Token actions"
|
||||
aria-label="Token actions" aria-haspopup="menu" :aria-expanded="open">
|
||||
<x-reicon name="dots-horizontal" class="size-4" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<button type="button" role="menuitem"
|
||||
x-on:click="close(); editToken(@js(App\Livewire\Forms\ApiTokenForm::valuesOf($token)))"
|
||||
class="listbox-option justify-start!">
|
||||
<x-table.row-actions label="Token actions">
|
||||
<x-table.row-action
|
||||
x-on:click="editToken({{ \Illuminate\Support\Js::from(App\Livewire\Forms\ApiTokenForm::valuesOf($token)) }})">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" role="menuitem" x-on:click="close()"
|
||||
wire:click="setPaused({{ $token->id }}, {{ $token->isPaused() ? 'false' : 'true' }})"
|
||||
class="listbox-option justify-start!">
|
||||
</x-table.row-action>
|
||||
<x-table.row-action
|
||||
wire:click="setPaused({{ $token->id }}, {{ $token->isPaused() ? 'false' : 'true' }})">
|
||||
{{ $token->isPaused() ? 'Resume' : 'Pause' }}
|
||||
</button>
|
||||
</x-table.row-action>
|
||||
<x-modal-confirmation title="Roll API Token?" buttonFullWidth
|
||||
submitAction="roll({{ $token->id }})" :actions="[
|
||||
'A new secret replaces this token. The current secret stops working at once.',
|
||||
@@ -214,13 +206,10 @@
|
||||
shortConfirmationLabel="Token description"
|
||||
:confirmWithPassword="false" step2ButtonText="Roll token">
|
||||
<x-slot:trigger>
|
||||
<button type="button" role="menuitem" x-on:click="close()"
|
||||
class="listbox-option justify-start!">
|
||||
Roll
|
||||
</button>
|
||||
<x-table.row-action>Roll</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="my-1 border-t border-neutral-200 dark:border-white/[0.08]"></div>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Confirm API Token Revocation?" buttonFullWidth
|
||||
submitAction="revoke({{ $token->id }})" :actions="[
|
||||
'This API token will be permanently revoked.',
|
||||
@@ -230,13 +219,11 @@
|
||||
shortConfirmationLabel="Token description"
|
||||
:confirmWithPassword="false" step2ButtonText="Revoke token">
|
||||
<x-slot:trigger>
|
||||
<button type="button" role="menuitem" x-on:click="close()"
|
||||
class="listbox-option justify-start! text-error! hover:text-error!">
|
||||
Revoke
|
||||
</button>
|
||||
<x-table.row-action>Revoke</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-table.dropdown>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
icon-name="file-content" size="sm" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_12rem_4.5rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Script</div>
|
||||
<div>Last updated</div>
|
||||
<div class="w-7"></div>
|
||||
<div class="text-right">Actions</div>
|
||||
</div>
|
||||
@foreach ($scripts as $script)
|
||||
@can('view', $script)
|
||||
@@ -40,7 +40,7 @@
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_12rem_1.75rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]"
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_12rem_4.5rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
@@ -59,10 +59,12 @@
|
||||
Updated {{ $script->updated_at->diffForHumans() }}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="icon-button" title="Edit cloud-init script"
|
||||
aria-label="Edit {{ $script->name }}" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" class="icon-button" title="Edit cloud-init script"
|
||||
aria-label="Edit {{ $script->name }}" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:security.cloud-init-script.show :cloud_init_script_uuid="$script->uuid"
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
description="Add a provider token to provision new cloud servers." icon-name="keys" size="sm" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_8rem_4.5rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_4.5rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Token</div>
|
||||
<div class="text-center">Provider</div>
|
||||
<div class="hidden sm:block">Description</div>
|
||||
<div class="w-7"></div>
|
||||
<div class="text-right">Actions</div>
|
||||
</div>
|
||||
@foreach ($tokens as $savedToken)
|
||||
<x-modal-input title="Edit Cloud Token" isFullWidth :wireIgnore="false" :contentClicks="false"
|
||||
@@ -32,7 +32,7 @@
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_8rem_1.75rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_1.75rem] dark:hover:bg-white/[0.025]"
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_8rem_4.5rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 sm:grid-cols-[minmax(0,1fr)_8rem_minmax(0,1fr)_4.5rem] dark:hover:bg-white/[0.025]"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
@@ -52,10 +52,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<p class="hidden truncate text-[12px] text-neutral-500 sm:block dark:text-fg-dim">{{ $savedToken->description ?: '-' }}</p>
|
||||
<button type="button" class="icon-button" title="Edit cloud token"
|
||||
aria-label="Edit {{ $savedToken->name }}" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" class="icon-button" title="Edit cloud token"
|
||||
aria-label="Edit {{ $savedToken->name }}" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:security.cloud-provider-token.show :cloud_token_uuid="$savedToken->uuid"
|
||||
|
||||
@@ -74,10 +74,12 @@
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<button type="button" class="icon-button" title="Edit integration token"
|
||||
:aria-label="`Edit ${tokenName}`" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" class="icon-button" title="Edit integration token"
|
||||
:aria-label="`Edit ${tokenName}`" @click="modalOpen=true">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:security.integration-token-editor
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<x-forms.input required id="{{ $form }}.description" label="Description"
|
||||
placeholder="CI deployment token" />
|
||||
<x-forms.listbox id="{{ $form }}.expiresInDays" label="Expires in" :options="$expiryOptions"
|
||||
x-effect="{{ $form === 'editForm' ? 'options[0].label = editingToken?.keepLabel ?? options[0].label' : '' }}" />
|
||||
:x-effect="$form === 'editForm' ? 'options[0].label = editingToken?.keepLabel ?? options[0].label' : null" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-t border-neutral-200 pt-3 dark:border-white/[0.08]">
|
||||
|
||||
@@ -68,18 +68,18 @@
|
||||
icon-name="keys" />
|
||||
@else
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_7rem_4.5rem] items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[13px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_4.5rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div class="pl-11">Private key</div>
|
||||
<div class="hidden sm:block">Description</div>
|
||||
<div class="text-center">Status</div>
|
||||
<div class="w-7"></div>
|
||||
<div class="text-right">Actions</div>
|
||||
</div>
|
||||
@foreach ($privateKeys as $key)
|
||||
@can('view', $key)
|
||||
<div wire:key="private-key-{{ $key->id }}"
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<div
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:hover:bg-white/[0.025]"
|
||||
class="grid min-h-14 w-full grid-cols-[minmax(0,1fr)_7rem_4.5rem] items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-neutral-50 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_4.5rem] dark:hover:bg-white/[0.025]"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
@@ -102,16 +102,18 @@
|
||||
<x-status-badge label="Unused" type="warning" />
|
||||
@endif
|
||||
</div>
|
||||
<button type="button" class="icon-button" title="Edit private key"
|
||||
aria-label="Edit {{ $key->name }}"
|
||||
@click="$dispatch('open-private-key-editor', { name: @js($key->name), description: @js($key->description ?? '') })"
|
||||
wire:click="openEditor('{{ $key->uuid }}')">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" class="icon-button" title="Edit private key"
|
||||
aria-label="Edit {{ $key->name }}"
|
||||
@click="$dispatch('open-private-key-editor', { name: @js($key->name), description: @js($key->description ?? '') })"
|
||||
wire:click="openEditor('{{ $key->uuid }}')">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid min-h-14 cursor-not-allowed grid-cols-[minmax(0,1fr)_7rem_1.75rem] items-center gap-3 border-b border-neutral-200 px-4 py-2.5 opacity-65 last:border-b-0 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_1.75rem] dark:border-white/[0.07]"
|
||||
<div class="grid min-h-14 cursor-not-allowed grid-cols-[minmax(0,1fr)_7rem_4.5rem] items-center gap-3 border-b border-neutral-200 px-4 py-2.5 opacity-65 last:border-b-0 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_7rem_4.5rem] dark:border-white/[0.07]"
|
||||
title="You do not have permission to view this private key">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
action="setSort" :current="$sort" />
|
||||
<x-table.sort-header label="Used by" asc="used_asc" desc="used_desc" action="setSort"
|
||||
:current="$sort" />
|
||||
<span>Actions</span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($visibleImages as $image)
|
||||
<div wire:key="image-{{ $image['reference'] }}"
|
||||
@@ -94,10 +94,10 @@
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-end">
|
||||
@if ($image['containers'] === [])
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Delete Docker Image?" buttonTitle="Delete"
|
||||
<x-modal-confirmation title="Delete Docker Image?"
|
||||
submitAction="delete({{ $image['reference'] }})" :actions="[
|
||||
'Delete ' . $image['reference'] . ' from this server.',
|
||||
'It has to be pulled or built again before it can be used.',
|
||||
@@ -105,7 +105,15 @@
|
||||
confirmationText="{{ $image['reference'] }}"
|
||||
confirmationLabel="Confirm by entering the image reference"
|
||||
shortConfirmationLabel="Image" step2ButtonText="Delete Image"
|
||||
isErrorButton :confirmWithPassword="false" />
|
||||
:confirmWithPassword="false">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Delete image" aria-label="Delete {{ $image['reference'] }}">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[760px] grid-cols-[minmax(0,1fr)_9rem_minmax(0,1fr)_14rem] border-b border-neutral-200 px-4 py-2 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
class="grid min-w-[760px] grid-cols-[minmax(0,1fr)_9rem_minmax(0,1fr)_4.5rem] border-b border-neutral-200 px-4 py-2 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<div>Registry</div>
|
||||
<div>Status</div>
|
||||
<div>Used by</div>
|
||||
<div></div>
|
||||
<div class="text-right">Actions</div>
|
||||
</div>
|
||||
@foreach ($visibleRows as $row)
|
||||
@php
|
||||
@@ -53,7 +53,7 @@
|
||||
@endphp
|
||||
<div wire:key="{{ $server->uuid }}-{{ $row['registry'] }}" x-data="{ showUsers: false }"
|
||||
class="min-w-[760px] border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<div class="grid min-h-11 grid-cols-[minmax(0,1fr)_9rem_minmax(0,1fr)_14rem] items-center px-4 py-2 text-[12px]">
|
||||
<div class="grid min-h-11 grid-cols-[minmax(0,1fr)_9rem_minmax(0,1fr)_4.5rem] items-center px-4 py-2 text-[12px]">
|
||||
<div class="truncate font-mono">{{ $row['registry'] }}</div>
|
||||
<div>
|
||||
@if ($row['logged_in'] && ($loginChecks[$row['registry']]['ok'] ?? true) === false)
|
||||
@@ -83,8 +83,34 @@
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
@if ($row['logged_in'])
|
||||
<div class="flex justify-end">
|
||||
@if ($row['logged_in'] && $row['source'] === 'auths')
|
||||
<x-table.row-actions label="Registry actions">
|
||||
<x-table.row-action wire:click="testLogin({{ \Illuminate\Support\Js::from($row['registry']) }})" wire:loading.attr="disabled"
|
||||
wire:target="testLogin" title="Run docker login with the saved credentials">Test</x-table.row-action>
|
||||
<x-modal-input title="Edit login for {{ $row['registry'] }}"
|
||||
subtitle="Runs docker login on {{ $server->name }} again. The new values replace the old login.">
|
||||
<x-slot:content>
|
||||
<x-table.row-action>Edit</x-table.row-action>
|
||||
</x-slot:content>
|
||||
<livewire:server.docker-registries.login :server="$server" :editRegistry="$row['registry']"
|
||||
:currentUsername="$row['username']"
|
||||
:key="'registry-edit-'.$server->uuid.'-'.$row['registry']" />
|
||||
</x-modal-input>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Delete the login for {{ $row['registry'] }}?" buttonFullWidth
|
||||
submitAction="logout({{ $row['registry'] }})" :actions="[
|
||||
'Runs docker logout on '.$server->name.' and removes the saved credentials.',
|
||||
'The server can no longer pull private images from this registry.',
|
||||
'Deployments that need this registry fail until you log in again.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@elseif ($row['logged_in'])
|
||||
<button type="button" wire:click="testLogin(@js($row['registry']))" wire:loading.attr="disabled"
|
||||
wire:target="testLogin" class="button w-fit"
|
||||
title="Run docker login with the saved credentials">Test</button>
|
||||
@@ -95,20 +121,6 @@
|
||||
:key="'registry-login-'.$server->uuid.'-'.$row['registry']" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@if ($row['logged_in'] && $row['source'] === 'auths')
|
||||
<x-modal-input buttonTitle="Edit" title="Edit login for {{ $row['registry'] }}"
|
||||
subtitle="Runs docker login on {{ $server->name }} again. The new values replace the old login.">
|
||||
<livewire:server.docker-registries.login :server="$server" :editRegistry="$row['registry']"
|
||||
:currentUsername="$row['username']"
|
||||
:key="'registry-edit-'.$server->uuid.'-'.$row['registry']" />
|
||||
</x-modal-input>
|
||||
<x-modal-confirmation title="Delete the login for {{ $row['registry'] }}?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="logout({{ $row['registry'] }})" :actions="[
|
||||
'Runs docker logout on '.$server->name.' and removes the saved credentials.',
|
||||
'The server can no longer pull private images from this registry.',
|
||||
'Deployments that need this registry fail until you log in again.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($usedByCount > 1)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<th>Resolver</th>
|
||||
<th>Alternative names</th>
|
||||
<th>Expires</th>
|
||||
<th><span class="sr-only">Actions</span></th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -71,22 +71,31 @@
|
||||
<td>
|
||||
{{ $certificate['expires_at'] ?? 'Unknown' }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<td>
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Delete TLS Certificate?"
|
||||
buttonTitle="Delete"
|
||||
submitAction="deleteTraefikCertificate({{ $certificate['id'] }})"
|
||||
:actions="[
|
||||
'Save the current acme.json as a backup that you can restore below.',
|
||||
'Delete the certificate for '.$certificate['main_domain'].' from acme.json.',
|
||||
]"
|
||||
warningMessage="The proxy keeps using the certificate until you restart it. If a route still uses the domain, Traefik requests a new certificate after the restart. You can restore the backup below to undo this."
|
||||
confirmationText="{{ $certificate['main_domain'] }}"
|
||||
confirmationLabel="Confirm by entering the domain"
|
||||
shortConfirmationLabel="Domain"
|
||||
step2ButtonText="Delete Certificate"
|
||||
isErrorButton :confirmWithPassword="false"
|
||||
:confirmWithText="true" />
|
||||
<div class="flex justify-end">
|
||||
<x-modal-confirmation title="Delete TLS Certificate?"
|
||||
submitAction="deleteTraefikCertificate({{ $certificate['id'] }})"
|
||||
:actions="[
|
||||
'Save the current acme.json as a backup that you can restore below.',
|
||||
'Delete the certificate for '.$certificate['main_domain'].' from acme.json.',
|
||||
]"
|
||||
warningMessage="The proxy keeps using the certificate until you restart it. If a route still uses the domain, Traefik requests a new certificate after the restart. You can restore the backup below to undo this."
|
||||
confirmationText="{{ $certificate['main_domain'] }}"
|
||||
confirmationLabel="Confirm by entering the domain"
|
||||
shortConfirmationLabel="Domain"
|
||||
step2ButtonText="Delete Certificate"
|
||||
:confirmWithPassword="false"
|
||||
:confirmWithText="true">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Delete certificate" aria-label="Delete certificate for {{ $certificate['main_domain'] }}">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@@ -115,7 +124,7 @@
|
||||
<th>Backup</th>
|
||||
<th>Created</th>
|
||||
<th>Size</th>
|
||||
<th><span class="sr-only">Actions</span></th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -127,28 +136,40 @@
|
||||
<td>{{ $backup['created_at'] }}</td>
|
||||
<td>{{ formatBytes($backup['size']) }}</td>
|
||||
<td>
|
||||
<div class="flex justify-end gap-2">
|
||||
<x-modal-confirmation title="Restore acme.json Backup?"
|
||||
buttonTitle="Restore"
|
||||
submitAction="restoreTraefikAcmeBackup('{{ $backup['name'] }}')"
|
||||
:checkboxes="[
|
||||
['id' => 'restartProxyAfterAcmeRestore', 'label' => 'Restart the proxy now. Sites on this server are unavailable for a few seconds.'],
|
||||
]"
|
||||
:actions="[
|
||||
'Save the current acme.json as a new backup.',
|
||||
'Replace acme.json with the backup from '.$backup['created_at'].'.',
|
||||
]"
|
||||
warningMessage="Until the proxy restarts, it keeps its loaded certificates and can write them back to acme.json. Certificates issued after this backup are removed from acme.json."
|
||||
step2ButtonText="Restore Backup"
|
||||
:confirmWithPassword="false"
|
||||
:confirmWithText="false" />
|
||||
<x-modal-confirmation title="Delete acme.json Backup?"
|
||||
buttonTitle="Delete"
|
||||
submitAction="deleteTraefikAcmeBackup('{{ $backup['name'] }}')"
|
||||
:actions="['Delete the backup '.$backup['name'].'.']"
|
||||
step2ButtonText="Delete Backup"
|
||||
isErrorButton :confirmWithPassword="false"
|
||||
:confirmWithText="false" />
|
||||
<div class="flex justify-end">
|
||||
<x-table.row-actions label="Backup actions">
|
||||
<x-modal-confirmation title="Restore acme.json Backup?"
|
||||
buttonFullWidth
|
||||
submitAction="restoreTraefikAcmeBackup('{{ $backup['name'] }}')"
|
||||
:checkboxes="[
|
||||
['id' => 'restartProxyAfterAcmeRestore', 'label' => 'Restart the proxy now. Sites on this server are unavailable for a few seconds.'],
|
||||
]"
|
||||
:actions="[
|
||||
'Save the current acme.json as a new backup.',
|
||||
'Replace acme.json with the backup from '.$backup['created_at'].'.',
|
||||
]"
|
||||
warningMessage="Until the proxy restarts, it keeps its loaded certificates and can write them back to acme.json. Certificates issued after this backup are removed from acme.json."
|
||||
step2ButtonText="Restore Backup"
|
||||
:confirmWithPassword="false"
|
||||
:confirmWithText="false">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Restore</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Delete acme.json Backup?"
|
||||
buttonFullWidth
|
||||
submitAction="deleteTraefikAcmeBackup('{{ $backup['name'] }}')"
|
||||
:actions="['Delete the backup '.$backup['name'].'.']"
|
||||
step2ButtonText="Delete Backup"
|
||||
:confirmWithPassword="false"
|
||||
:confirmWithText="false">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Delete</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<x-table.sort-header label="Name" asc="name_asc" desc="name_desc" action="setSort" :current="$sort" />
|
||||
<x-table.sort-header label="Image" asc="image_asc" desc="image_desc" action="setSort" :current="$sort" />
|
||||
<x-table.sort-header label="Status" asc="status_asc" desc="status_desc" action="setSort" :current="$sort" />
|
||||
<span>Actions</span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($resources as $resource)
|
||||
@php($containerState = (string) data_get($resource, 'State', 'unknown'))
|
||||
@@ -130,30 +130,39 @@
|
||||
? 'success'
|
||||
: ($containerState === 'exited' ? 'error' : 'warning')" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if ($containerState === 'running')
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="restart-{{ data_get($resource, 'ID') }}">
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-forms.button canGate="update" :canResource="$server" isError
|
||||
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="stop-{{ data_get($resource, 'ID') }}">
|
||||
Stop
|
||||
</x-forms.button>
|
||||
@elseif ($containerState === 'exited')
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="start-{{ data_get($resource, 'ID') }}">
|
||||
Start
|
||||
</x-forms.button>
|
||||
@elseif ($containerState === 'restarting')
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="stop-restarting-{{ data_get($resource, 'ID') }}">
|
||||
Stop
|
||||
</x-forms.button>
|
||||
<div class="flex justify-end">
|
||||
@if (in_array($containerState, ['running', 'exited', 'restarting'], true))
|
||||
@can('update', $server)
|
||||
@if ($containerState === 'running')
|
||||
<x-table.row-actions label="Container actions">
|
||||
<x-table.row-action
|
||||
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="restart-{{ data_get($resource, 'ID') }}">
|
||||
Restart
|
||||
</x-table.row-action>
|
||||
<x-table.row-action
|
||||
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="stop-{{ data_get($resource, 'ID') }}">
|
||||
Stop
|
||||
</x-table.row-action>
|
||||
</x-table.row-actions>
|
||||
@elseif ($containerState === 'exited')
|
||||
<button type="button" class="icon-button shrink-0"
|
||||
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="start-{{ data_get($resource, 'ID') }}"
|
||||
title="Start container" aria-label="Start container">
|
||||
<x-reicon name="play-circle" class="size-3.5" />
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
|
||||
wire:key="stop-restarting-{{ data_get($resource, 'ID') }}"
|
||||
title="Stop container" aria-label="Stop container">
|
||||
<x-reicon name="stop-circle" class="size-3.5" />
|
||||
</button>
|
||||
@endif
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
<div class="data-table-header package-updates-table-grid">
|
||||
<span>Package</span>
|
||||
<span>New version</span>
|
||||
<span class="text-right">Action</span>
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
@foreach ($updates as $update)
|
||||
<div wire:key="package-update-{{ data_get($update, 'package') }}-{{ data_get($update, 'architecture') }}"
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
class="grid min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(12rem,1.2fr)_4.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
|
||||
<div>Backup target</div>
|
||||
<div>Frequency</div>
|
||||
<div>Status</div>
|
||||
<div>Storage</div>
|
||||
<div class="text-right">Actions</div>
|
||||
</div>
|
||||
@foreach ($groupedBackups as $backups)
|
||||
@php
|
||||
@@ -77,7 +78,7 @@
|
||||
])->values()->all();
|
||||
@endphp
|
||||
<div
|
||||
class="grid min-h-14 min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] last:border-b-0 dark:border-white/[0.07]"
|
||||
class="grid min-h-14 min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(12rem,1.2fr)_4.5rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] last:border-b-0 dark:border-white/[0.07]"
|
||||
x-show="search === '' || '{{ strtolower(addslashes($databaseName)) }}'.includes(search.toLowerCase()) || '{{ strtolower(addslashes($backup->frequency)) }}'.includes(search.toLowerCase())">
|
||||
<div class="min-w-0">
|
||||
@if ($resourceLink)
|
||||
@@ -104,16 +105,20 @@
|
||||
portal />
|
||||
<button type="button" class="button shrink-0"
|
||||
wire:click="moveBackup({{ $backup->id }})">Move</button>
|
||||
<div wire:key="disableS3-{{ $backup->id }}">
|
||||
<x-modal-confirmation title="Remove this storage from the backup schedule?"
|
||||
submitAction="disableS3({{ $backup->id }})" :confirmWithText="false"
|
||||
:confirmWithPassword="false" step2ButtonText="Remove storage"
|
||||
:actions="['Backups from this schedule will no longer be uploaded to this storage. S3 backup turns off when no other storage remains.']">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button shrink-0 text-error">Disable</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end" wire:key="disableS3-{{ $backup->id }}">
|
||||
<x-modal-confirmation title="Remove this storage from the backup schedule?"
|
||||
submitAction="disableS3({{ $backup->id }})" :confirmWithText="false"
|
||||
:confirmWithPassword="false" step2ButtonText="Remove storage"
|
||||
:actions="['Backups from this schedule will no longer be uploaded to this storage. S3 backup turns off when no other storage remains.']">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Disable storage" aria-label="Remove this storage from the backup schedule">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -131,7 +136,7 @@
|
||||
])->values()->all();
|
||||
@endphp
|
||||
<div
|
||||
class="grid min-h-14 min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(15rem,1.2fr)] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] last:border-b-0 dark:border-white/[0.07]"
|
||||
class="grid min-h-14 min-w-[780px] grid-cols-[minmax(12rem,1fr)_9rem_7rem_minmax(12rem,1.2fr)_4.5rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] last:border-b-0 dark:border-white/[0.07]"
|
||||
x-show="search === '' || '{{ strtolower(addslashes($targetName)) }}'.includes(search.toLowerCase()) || '{{ strtolower(addslashes($targetType)) }}'.includes(search.toLowerCase()) || '{{ strtolower(addslashes($resourceName ?? '')) }}'.includes(search.toLowerCase()) || '{{ strtolower(addslashes($backup->frequency)) }}'.includes(search.toLowerCase())">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-black dark:text-fg">{{ $targetName }}</div>
|
||||
@@ -149,16 +154,20 @@
|
||||
portal />
|
||||
<button type="button" class="button shrink-0"
|
||||
wire:click="moveVolumeBackup({{ $backup->id }})">Move</button>
|
||||
<div wire:key="disableVolumeS3-{{ $backup->id }}">
|
||||
<x-modal-confirmation title="Remove this storage from the backup schedule?"
|
||||
submitAction="disableVolumeS3({{ $backup->id }})" :confirmWithText="false"
|
||||
:confirmWithPassword="false" step2ButtonText="Remove storage"
|
||||
:actions="['Backups from this schedule will no longer be uploaded to this storage. S3 backup turns off when no other storage remains.']">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button shrink-0 text-error">Disable</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end" wire:key="disableVolumeS3-{{ $backup->id }}">
|
||||
<x-modal-confirmation title="Remove this storage from the backup schedule?"
|
||||
submitAction="disableVolumeS3({{ $backup->id }})" :confirmWithText="false"
|
||||
:confirmWithPassword="false" step2ButtonText="Remove storage"
|
||||
:actions="['Backups from this schedule will no longer be uploaded to this storage. S3 backup turns off when no other storage remains.']">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Disable storage" aria-label="Remove this storage from the backup schedule">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -74,8 +74,9 @@
|
||||
shortConfirmationLabel="User name">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="text-[12px] font-medium text-red-600 transition-colors hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
|
||||
Delete
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Delete user" aria-label="Delete {{ $user->name }}">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="data-table min-w-[760px] transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
|
||||
wire:target="setSort">
|
||||
<div
|
||||
class="data-table-header grid-cols-[minmax(13rem,1.2fr)_7rem_7rem_minmax(15rem,1.5fr)_6rem]">
|
||||
class="data-table-header grid-cols-[minmax(13rem,1.2fr)_7rem_7rem_minmax(15rem,1.5fr)_4.5rem]">
|
||||
<x-table.sort-header label="Email" asc="email-asc" desc="email-desc" action="setSort"
|
||||
:current="$sort" />
|
||||
<x-table.sort-header label="Method" asc="via-asc" desc="via-desc" action="setSort"
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
@foreach ($sortedInvitations as $invite)
|
||||
<div wire:key="team-invitation-{{ $invite->id }}"
|
||||
class="data-table-row grid-cols-[minmax(13rem,1.2fr)_7rem_7rem_minmax(15rem,1.5fr)_6rem] border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
class="data-table-row grid-cols-[minmax(13rem,1.2fr)_7rem_7rem_minmax(15rem,1.5fr)_4.5rem] border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
|
||||
<div class="truncate text-[12px] font-medium text-black dark:text-fg">
|
||||
{{ $invite->email }}
|
||||
</div>
|
||||
@@ -35,12 +35,20 @@
|
||||
title="{{ $invite->link }}">{{ $invite->link }}</span>
|
||||
<x-copy-button :value="$invite->link" label="Copy invitation link" />
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="button"
|
||||
class="button h-7! px-2.5! text-[11px]! text-error! hover:text-error!"
|
||||
wire:click.prevent="deleteInvitation({{ $invite->id }})">
|
||||
Revoke
|
||||
</button>
|
||||
<div class="flex justify-end">
|
||||
<x-modal-confirmation title="Revoke invitation?"
|
||||
submitAction="deleteInvitation({{ $invite->id }})"
|
||||
:actions="['The invitation link for ' . $invite->email . ' stops working.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Revoke invitation">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Revoke invitation" aria-label="Revoke invitation for {{ $invite->email }}">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@@ -52,61 +52,56 @@
|
||||
<div class="flex justify-end">
|
||||
@can('manageMembers', currentTeam())
|
||||
@if ($member->id !== Auth::id())
|
||||
<div x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button h-7! px-2.5! text-[11px]!" x-ref="trigger"
|
||||
@click="open = !open" aria-haspopup="menu" :aria-expanded="open">
|
||||
Manage
|
||||
</button>
|
||||
<template x-teleport="body">
|
||||
<div x-show="open" x-cloak role="menu" x-anchor.bottom-end.fixed.offset.4="$refs.trigger"
|
||||
@click.outside="if (! $refs.trigger.contains($event.target)) open = false"
|
||||
class="listbox-panel w-36! min-w-0! z-50!">
|
||||
@if (Auth::user()->isOwner())
|
||||
@if (data_get($member, 'pivot.role') !== 'owner')
|
||||
<button type="button" class="listbox-option justify-start!" wire:click="makeOwner"
|
||||
@click="open = false">
|
||||
Make owner
|
||||
</button>
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') !== 'admin')
|
||||
<button type="button" class="listbox-option justify-start!" wire:click="makeAdmin"
|
||||
@click="open = false">
|
||||
Make admin
|
||||
</button>
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') !== 'member')
|
||||
<button type="button" class="listbox-option justify-start!"
|
||||
wire:click="makeReadonly" @click="open = false">
|
||||
Make member
|
||||
</button>
|
||||
@endif
|
||||
@elseif (Auth::user()->isAdmin())
|
||||
@if (data_get($member, 'pivot.role') === 'admin')
|
||||
<button type="button" class="listbox-option justify-start!"
|
||||
wire:click="makeReadonly" @click="open = false">
|
||||
Make member
|
||||
</button>
|
||||
@elseif (data_get($member, 'pivot.role') === 'member')
|
||||
<button type="button" class="listbox-option justify-start!" wire:click="makeAdmin"
|
||||
@click="open = false">
|
||||
Make admin
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') === 'member')
|
||||
<button type="button" class="listbox-option justify-start!"
|
||||
wire:click="$dispatch('open-member-access', { memberId: {{ $member->id }} })" @click="open = false">
|
||||
Edit access
|
||||
</button>
|
||||
@endif
|
||||
<div class="my-1 border-t border-neutral-200 dark:border-white/[0.08]"></div>
|
||||
<button type="button" class="listbox-option justify-start! text-error! hover:text-error!"
|
||||
wire:click="remove" @click="open = false">
|
||||
Remove member
|
||||
@if (data_get($member, 'pivot.role') === 'owner' && ! Auth::user()->isOwner())
|
||||
<x-modal-confirmation title="Remove team member?" submitAction="remove"
|
||||
:actions="['This user loses access to all resources of this team.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Remove member">
|
||||
<x-slot:trigger>
|
||||
<button type="button"
|
||||
class="icon-button shrink-0 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
title="Remove member" aria-label="Remove {{ $member->name }}">
|
||||
<x-reicon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-table.row-actions label="Member actions">
|
||||
@if (Auth::user()->isOwner())
|
||||
@if (data_get($member, 'pivot.role') !== 'owner')
|
||||
<x-table.row-action wire:click="makeOwner">Make owner</x-table.row-action>
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') !== 'admin')
|
||||
<x-table.row-action wire:click="makeAdmin">Make admin</x-table.row-action>
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') !== 'member')
|
||||
<x-table.row-action wire:click="makeReadonly">Make member</x-table.row-action>
|
||||
@endif
|
||||
@elseif (Auth::user()->isAdmin())
|
||||
@if (data_get($member, 'pivot.role') === 'admin')
|
||||
<x-table.row-action wire:click="makeReadonly">Make member</x-table.row-action>
|
||||
@elseif (data_get($member, 'pivot.role') === 'member')
|
||||
<x-table.row-action wire:click="makeAdmin">Make admin</x-table.row-action>
|
||||
@endif
|
||||
@endif
|
||||
@if (data_get($member, 'pivot.role') === 'member')
|
||||
<x-table.row-action
|
||||
wire:click="$dispatch('open-member-access', { memberId: {{ $member->id }} })">
|
||||
Edit access
|
||||
</x-table.row-action>
|
||||
@endif
|
||||
<x-slot:danger>
|
||||
<x-modal-confirmation title="Remove team member?" buttonFullWidth submitAction="remove"
|
||||
:actions="['This user loses access to all resources of this team.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Remove member">
|
||||
<x-slot:trigger>
|
||||
<x-table.row-action>Remove member</x-table.row-action>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</x-slot:danger>
|
||||
</x-table.row-actions>
|
||||
@endif
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ it('refreshes service backup buttons when the service status changes', function
|
||||
? ['service' => $this->service]
|
||||
: ['backup' => $this->backups['service']->fresh(), 'availableS3Storages' => collect()];
|
||||
$component = Livewire::test($componentClass, $parameters);
|
||||
$disabledButton = '/<button\s+disabled[^>]*wire:click(\.stop)?="backupNow/s';
|
||||
$disabledButton = '/<button(?=[^>]*\sdisabled\b)(?=[^>]*wire:click(\.stop)?="backupNow)[^>]*>/s';
|
||||
$database->update(['status' => 'running:healthy']);
|
||||
|
||||
$component->dispatch('echo-private:team.'.currentTeam()->id.','.$event);
|
||||
|
||||
@@ -105,8 +105,8 @@ it('declares update authorization on service backup mutation controls', function
|
||||
|
||||
expect($volumeBackupView)
|
||||
->toMatch('/<x-modal-input(?=[^>]*:title="\'Edit backup schedule\'")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/')
|
||||
->toMatch('/<x-forms\.button\s+type="button" canGate="update" :canResource="\$service"[^\n]*\n(?:[^\n]*\n){0,3}?\s*wire:click\.stop="backupNow\(\'database\',/')
|
||||
->toMatch('/<x-forms\.button(?=[^>]*wire:click\.stop="backupNow\(\'storage\',[^"]+")(?=[^>]*canGate="update")(?=[^>]*:canResource="\$service")[^>]*>/');
|
||||
->toMatch('/@can\(\'update\', \$service\)\s*<x-table\.row-actions\b(?:(?!@endcan).)*?wire:click="backupNow\(\'database\',/s')
|
||||
->toMatch('/@can\(\'update\', \$service\)\s*<x-table\.row-actions\b(?:(?!@endcan).)*?wire:click="backupNow\(\'storage\',/s');
|
||||
});
|
||||
|
||||
it('declares update authorization on application port controls', function () {
|
||||
|
||||
@@ -131,15 +131,13 @@ it('declares explicit authorization on the changed storage controls', function (
|
||||
->toContain('canGate="update"')
|
||||
->toContain(':canResource="$resource"');
|
||||
|
||||
preg_match_all('/<x-forms\.button\b[^>]*>\s*Backup\s*<\/x-forms\.button>/s', $view, $backupButtons);
|
||||
$backupAction = '/<x-table\.row-action>Backup<\/x-table\.row-action>/';
|
||||
preg_match('/@if \(\$canUpdate\)\s*<x-modal-input\b.*?<\/x-modal-input>/s', $view, $canUpdateBlock);
|
||||
preg_match('/@can\(\'update\', \$resource\)(.*?)@endcan/s', $view, $canBlock);
|
||||
|
||||
expect($backupButtons[0])->toHaveCount(3);
|
||||
|
||||
foreach ($backupButtons[0] as $backupButton) {
|
||||
expect($backupButton)
|
||||
->toContain('canGate="update"')
|
||||
->toContain(':canResource="$resource"');
|
||||
}
|
||||
expect(preg_match_all($backupAction, $view))->toBe(3)
|
||||
->and(preg_match_all($backupAction, $canUpdateBlock[0] ?? ''))->toBe(1)
|
||||
->and(preg_match_all($backupAction, $canBlock[0] ?? ''))->toBe(2);
|
||||
});
|
||||
|
||||
it('creates named volumes without a host path in swarm mode', function () {
|
||||
@@ -329,7 +327,8 @@ YAML,
|
||||
]);
|
||||
|
||||
Livewire::test(All::class, ['resource' => $application])
|
||||
->assertSee('Delete stale volume entry')
|
||||
->assertSee('Remove stale volume entry?')
|
||||
->assertSee('Delete stale entry')
|
||||
->call('delete', $volume->id, 'password');
|
||||
|
||||
expect($volume->fresh())->toBeNull();
|
||||
|
||||
@@ -463,11 +463,11 @@ test('service storage backups page includes schedules from all compose databases
|
||||
'service_uuid' => $this->ownService->uuid,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('>Database</span>', false)
|
||||
->assertSee('>Database</button>', false)
|
||||
->assertSee('own-db')
|
||||
->assertSee('analytics-db')
|
||||
->assertSee("wire:click=\"openSchedule('{$backups->first()->uuid}')\"", false)
|
||||
->assertSee("wire:click.stop=\"backupNow('database', '{$backups->first()->uuid}')\"", false);
|
||||
->assertSee("wire:click=\"backupNow('database', '{$backups->first()->uuid}')\"", false);
|
||||
});
|
||||
|
||||
test('service backup settings open automatically from the creation redirect', function () {
|
||||
@@ -521,18 +521,18 @@ test('service backups have explicit settings actions for database and storage sc
|
||||
$dom = new DOMDocument;
|
||||
@$dom->loadHTML($html);
|
||||
$xpath = new DOMXPath($dom);
|
||||
$buttons = $xpath->query('//button[@aria-label="Edit backup schedule"]');
|
||||
$buttons = $xpath->query('//button[@role="menuitem"][normalize-space()="Edit"]');
|
||||
$actions = [];
|
||||
foreach ($buttons as $button) {
|
||||
$actions[] = $button->getAttribute('wire:click.stop');
|
||||
$actions[] = $button->getAttribute('wire:click');
|
||||
}
|
||||
|
||||
expect($actions)->toContain("openSchedule('{$databaseBackup->uuid}')", "openSchedule('{$volumeBackup->uuid}')");
|
||||
|
||||
foreach (['database' => $databaseBackup, 'storage' => $volumeBackup] as $type => $backup) {
|
||||
$backupAction = "wire:click.stop=\"backupNow('{$type}', '{$backup->uuid}')\"";
|
||||
$settingsAction = "wire:click.stop=\"openSchedule('{$backup->uuid}')\"";
|
||||
expect(strpos($html, $backupAction))->toBeLessThan(strpos($html, $settingsAction));
|
||||
$backupAction = "wire:click=\"backupNow('{$type}', '{$backup->uuid}')\"";
|
||||
$settingsAction = "wire:click=\"openSchedule('{$backup->uuid}')\"";
|
||||
expect(strpos($html, $backupAction))->toBeLessThan(strrpos($html, $settingsAction));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user