mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 16:14:42 +00:00
feat(project): add icons and improve persistent volume management
Add project icon storage and serving with local/S3 support, prevent deletion of compose-managed volumes, and optimize volume backups. Refine project and backup UI components with consistent styling and coverage.
This commit is contained in:
1 parent
a02d02d19b
commit
fd5eb3e0cd
43 files changed
+728
-60
No files matched your search
+20
-21
@@ -1570,17 +1570,6 @@ html[data-theme="custom"] textarea:disabled {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.application-settings-workspace .button,
|
||||
.application-settings-form .button {
|
||||
height: 2rem;
|
||||
min-height: 2rem;
|
||||
border-radius: 8px;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.application-settings-workspace .form-control,
|
||||
.application-settings-form .form-control {
|
||||
border-radius: 8px;
|
||||
@@ -2698,9 +2687,9 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.backup-executions-table-grid {
|
||||
grid-template-columns: 6.5rem minmax(7rem, 1fr) 7rem 5rem 4rem minmax(8rem, 1fr) 5rem;
|
||||
grid-template-columns: 6.5rem minmax(7rem, 0.8fr) minmax(14rem, 1.5fr) 7rem 5rem 4rem minmax(8rem, 1fr) 5rem;
|
||||
gap: 0.75rem;
|
||||
min-width: 49rem;
|
||||
min-width: 66rem;
|
||||
}
|
||||
|
||||
.volume-backup-executions-grid {
|
||||
@@ -2760,11 +2749,11 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.backup-executions-table-grid {
|
||||
grid-template-columns: 7.5rem minmax(9rem, 1fr) 8rem 6rem minmax(9rem, auto);
|
||||
grid-template-columns: 7.5rem minmax(9rem, 0.8fr) minmax(14rem, 1.5fr) 8rem 6rem minmax(9rem, auto);
|
||||
}
|
||||
|
||||
.backup-executions-table-grid > :nth-child(5),
|
||||
.backup-executions-table-grid > :nth-child(6) {
|
||||
.backup-executions-table-grid > :nth-child(6),
|
||||
.backup-executions-table-grid > :nth-child(7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -2871,11 +2860,14 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.backup-executions-table-grid {
|
||||
grid-template-columns: 7.5rem minmax(0, 1fr) minmax(7rem, auto);
|
||||
grid-template-columns: 7.5rem 9rem minmax(14rem, 1fr) minmax(7rem, auto);
|
||||
min-width: 42rem;
|
||||
}
|
||||
|
||||
.backup-executions-table-grid > :nth-child(3),
|
||||
.backup-executions-table-grid > :nth-child(4) {
|
||||
.backup-executions-table-grid > :nth-child(4),
|
||||
.backup-executions-table-grid > :nth-child(5),
|
||||
.backup-executions-table-grid > :nth-child(6),
|
||||
.backup-executions-table-grid > :nth-child(7) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -3198,7 +3190,14 @@ html[data-theme="custom"] .logs-viewer-timestamp {
|
||||
|
||||
.logs-viewer-viewport {
|
||||
min-width: 0;
|
||||
padding: 0.5rem 0.75rem 2rem;
|
||||
padding: 0.5rem 0.75rem 0;
|
||||
}
|
||||
|
||||
/* A flex item is reliably included in the scrollable overflow area, unlike
|
||||
bottom padding on overflow containers in some browser/layout combinations. */
|
||||
.logs-viewer-viewport::after {
|
||||
content: "";
|
||||
flex: 0 0 2rem;
|
||||
}
|
||||
|
||||
.logs-viewer-line {
|
||||
@@ -3292,7 +3291,7 @@ html[data-theme="custom"] .logs-viewer-timestamp {
|
||||
}
|
||||
|
||||
.logs-viewer-viewport {
|
||||
padding: 0.5rem 1rem 2rem;
|
||||
padding: 0.5rem 1rem 0;
|
||||
}
|
||||
|
||||
.logs-viewer-line {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
@props([
|
||||
'value',
|
||||
'label' => 'Copy to clipboard',
|
||||
])
|
||||
|
||||
<button type="button"
|
||||
x-data="{ copied: false }"
|
||||
x-on:click.prevent.stop="await window.copyToClipboard({{ Js::from($value) }}); copied = true; setTimeout(() => copied = false, 1000)"
|
||||
{{ $attributes->class('inline-flex size-6 shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-40 dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-white') }}
|
||||
title="{{ $label }}" aria-label="{{ $label }}" @disabled(blank($value))>
|
||||
<svg x-show="!copied" class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path d="M8 8.75H6.5A2.25 2.25 0 0 0 4.25 11v6.5a2.25 2.25 0 0 0 2.25 2.25H13a2.25 2.25 0 0 0 2.25-2.25V16"
|
||||
stroke-width="1.5" stroke-linecap="round" />
|
||||
<rect x="8.75" y="4.25" width="11" height="11" rx="2.25" stroke-width="1.5" />
|
||||
</svg>
|
||||
<svg x-show="copied" x-cloak class="size-3.5 text-green-500" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path d="m6.75 12.25 3.5 3.5 7-7" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -22,7 +22,7 @@
|
||||
this.collapsed = !this.collapsed;
|
||||
localStorage.setItem('sidebarCollapsed', this.collapsed);
|
||||
}
|
||||
}" x-cloak class="dark:text-inherit text-black">
|
||||
}" @open-global-search.window="open = false" x-cloak class="dark:text-inherit text-black">
|
||||
<livewire:deployments-indicator />
|
||||
|
||||
{{-- ============ DESKTOP TOP BAR ============ --}}
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
{{ data_get_str($application, 'name')->limit(10) }} > Backups | Coolify
|
||||
</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-backup-index" />
|
||||
<livewire:project.application.heading :application="$application"
|
||||
wire:key="application-heading-backup-index-{{ $application->id }}" />
|
||||
|
||||
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
||||
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<div>
|
||||
@if ($section === 'retention')
|
||||
@if ($backup->database_id === 0)
|
||||
@include('livewire.project.database.backup-edit.general')
|
||||
@include('livewire.project.database.backup-edit.s3')
|
||||
@include('livewire.project.database.backup-edit.retention')
|
||||
@elseif ($section === 'retention')
|
||||
@include('livewire.project.database.backup-edit.retention')
|
||||
@elseif ($section === 's3')
|
||||
@include('livewire.project.database.backup-edit.s3')
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
class="data-table-header backup-executions-table-grid h-auto rounded-none px-4 py-2.5 text-[11px]">
|
||||
<span>Status</span>
|
||||
<span>Database</span>
|
||||
<span>Backup path</span>
|
||||
<span>Finished</span>
|
||||
<span>Duration</span>
|
||||
<span>Size</span>
|
||||
@@ -79,6 +80,11 @@
|
||||
<div class="truncate text-[12px] font-medium text-black dark:text-fg">
|
||||
{{ data_get($execution, 'database_name', 'N/A') }}
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<code class="select-all truncate font-mono text-[11px] text-neutral-600 dark:text-fg-dim"
|
||||
title="Backup path: {{ data_get($execution, 'filename', 'N/A') }}">{{ data_get($execution, 'filename', 'N/A') }}</code>
|
||||
<x-copy-button :value="data_get($execution, 'filename', '')" label="Copy backup path" />
|
||||
</div>
|
||||
<div class="text-[11px] text-neutral-600 dark:text-fg-dim">
|
||||
@if ($executionStatus === 'running')
|
||||
Running now
|
||||
|
||||
@@ -7,6 +7,86 @@
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<section class="application-settings-section" x-data="{
|
||||
preview: null,
|
||||
processing: false,
|
||||
uploadError: null,
|
||||
async prepareIcon(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
this.processing = true;
|
||||
this.uploadError = null;
|
||||
|
||||
try {
|
||||
const image = await new Promise((resolve, reject) => {
|
||||
const element = new Image();
|
||||
element.onload = () => resolve(element);
|
||||
element.onerror = reject;
|
||||
element.src = URL.createObjectURL(file);
|
||||
});
|
||||
const cropSize = Math.min(image.naturalWidth, image.naturalHeight);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = '#ffffff';
|
||||
context.fillRect(0, 0, 256, 256);
|
||||
context.drawImage(image, (image.naturalWidth - cropSize) / 2, (image.naturalHeight - cropSize) / 2, cropSize, cropSize, 0, 0, 256, 256);
|
||||
const blob = await new Promise((resolve, reject) => canvas.toBlob(value => value ? resolve(value) : reject(new Error('JPEG compression failed')), 'image/jpeg', 0.8));
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
const compressed = new File([blob], 'project-icon.jpg', { type: 'image/jpeg' });
|
||||
this.$wire.upload('icon', compressed, async () => {
|
||||
const uploaded = await this.$wire.uploadIcon();
|
||||
if (uploaded) {
|
||||
if (this.preview) URL.revokeObjectURL(this.preview);
|
||||
this.preview = previewUrl;
|
||||
} else {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
}
|
||||
this.processing = false;
|
||||
}, () => {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
this.processing = false;
|
||||
this.uploadError = 'The image could not be uploaded.';
|
||||
});
|
||||
} catch (error) {
|
||||
this.processing = false;
|
||||
this.uploadError = 'The image could not be processed in this browser.';
|
||||
}
|
||||
},
|
||||
}">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Project icon</h2>
|
||||
<p>Upload a JPG, PNG, or WebP image. It will appear in the projects list.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body flex items-center gap-4">
|
||||
<div class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<img x-cloak x-show="preview" :src="preview" alt="Project icon preview" class="h-full w-full object-cover">
|
||||
@if ($project->icon_path)
|
||||
<img x-show="!preview" src="{{ route('project.icon', ['project_uuid' => $project->uuid, 'v' => $project->updated_at->timestamp]) }}"
|
||||
alt="{{ $project->name }} icon" class="h-full w-full object-cover">
|
||||
@else
|
||||
<x-reicon x-show="!preview" name="projects" class="size-6" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input x-ref="iconInput" type="file" x-on:change="prepareIcon($event)" accept="image/jpeg,image/png,image/webp" class="hidden">
|
||||
<x-forms.button type="button" x-on:click="$refs.iconInput.click()" x-bind:disabled="processing">
|
||||
<span x-text="processing ? 'Uploading…' : 'Browse…'"></span>
|
||||
</x-forms.button>
|
||||
@if ($project->icon_path)
|
||||
<x-forms.button type="button" wire:click="removeIcon" x-bind:disabled="processing" isError>Remove</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<p x-cloak x-show="uploadError" x-text="uploadError" class="text-xs text-red-500"></p>
|
||||
@error('icon') <p class="text-xs text-red-500">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<section class="application-settings-section">
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
<button type="button" x-on:click="setViewMode('table')"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'table'
|
||||
@@ -110,7 +110,12 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
<template x-if="project.iconUrl">
|
||||
<img :src="project.iconUrl" :alt="`${project.name} icon`" class="h-full w-full rounded-lg object-cover">
|
||||
</template>
|
||||
<template x-if="!project.iconUrl">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2
|
||||
@@ -191,7 +196,12 @@
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
<template x-if="project.iconUrl">
|
||||
<img :src="project.iconUrl" :alt="`${project.name} icon`" class="h-full w-full rounded-lg object-cover">
|
||||
</template>
|
||||
<template x-if="!project.iconUrl">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
</template>
|
||||
</div>
|
||||
<a :href="project.href" {{ wireNavigate() }}
|
||||
class="truncate text-[13px] font-semibold text-black hover:underline dark:text-fg"
|
||||
|
||||
@@ -151,8 +151,7 @@
|
||||
rel="noopener noreferrer" @click.stop>
|
||||
Docs
|
||||
</a>
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1 text-[12px] font-medium text-neutral-400 transition-colors group-hover:text-coollabs dark:text-fg-faint dark:group-hover:text-coollabs-100">
|
||||
<span class="button button-highlighted ml-auto">
|
||||
Deploy
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</span>
|
||||
@@ -188,8 +187,7 @@
|
||||
rel="noopener noreferrer" @click.stop>
|
||||
Docs
|
||||
</a>
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1 text-[12px] font-medium text-neutral-400 transition-colors group-hover:text-coollabs dark:text-fg-faint dark:group-hover:text-coollabs-100">
|
||||
<span class="button button-highlighted ml-auto">
|
||||
Deploy
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</span>
|
||||
@@ -254,8 +252,7 @@
|
||||
class="button" @click.stop>
|
||||
Website
|
||||
</a>
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1 text-[12px] font-medium text-neutral-400 transition-colors group-hover:text-coollabs dark:text-fg-faint dark:group-hover:text-coollabs-100">
|
||||
<span class="button button-highlighted ml-auto">
|
||||
Deploy
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</span>
|
||||
@@ -334,8 +331,7 @@
|
||||
target="_blank" rel="noopener noreferrer" class="button" @click.stop>
|
||||
Website
|
||||
</a>
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1 text-[12px] font-medium text-neutral-400 transition-colors group-hover:text-coollabs dark:text-fg-faint dark:group-hover:text-coollabs-100">
|
||||
<span class="button button-highlighted ml-auto">
|
||||
Deploy
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</span>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
<button type="button" x-on:click="setViewMode('table')"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'table'
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
</div>
|
||||
<div class="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-start">
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
<button type="button" x-on:click="setViewMode('table')"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'table'
|
||||
|
||||
@@ -121,6 +121,21 @@
|
||||
@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="[
|
||||
'This removes only the stale volume entry from Coolify.',
|
||||
]" confirmationText="{{ $form['name'] }}"
|
||||
confirmationLabel="Please confirm by entering the Storage Name below"
|
||||
shortConfirmationLabel="Storage Name" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
<button type="button" x-on:click="setViewMode('table')"
|
||||
class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'table'
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
@endif
|
||||
</button>
|
||||
@elseif ($trigger === 'account-menu')
|
||||
<button wire:click="openWhatsNewModal" @click="open = false" type="button"
|
||||
<button wire:click="openWhatsNewModal" type="button"
|
||||
class="listbox-option relative w-full text-left">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="size-4 opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -112,8 +112,8 @@
|
||||
<section
|
||||
class="application-settings-form application-settings-section relative flex max-h-[calc(100dvh-3rem)] !w-full max-w-5xl flex-col overflow-hidden"
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
<header>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<header class="relative !pr-11">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-500 dark:bg-white/[0.05] dark:text-fg-dim">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -134,8 +134,8 @@
|
||||
Product updates, fixes, and improvements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isDev())
|
||||
<x-forms.button wire:click="manualFetchChangelog" class="gap-1.5">
|
||||
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -151,12 +151,12 @@
|
||||
Mark all read
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<button wire:click="closeWhatsNewModal"
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded-md text-neutral-500 outline-0 hover:bg-neutral-100 hover:text-black focus-visible:ring-1 focus-visible:ring-accent dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
class="absolute right-2 top-2 flex size-7 cursor-pointer items-center justify-center rounded-md text-neutral-500 outline-0 hover:bg-neutral-100 hover:text-black focus-visible:ring-1 focus-visible:ring-accent dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Close What's new">
|
||||
<x-reicon name="x" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="application-settings-section-body flex min-h-0 flex-1 flex-col !p-0">
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="avatar-storage-section" title="Profile picture storage"
|
||||
helper="Choose where compressed user profile pictures are stored. Use S3 for multi-instance or cloud deployments so every application replica can access the same files.">
|
||||
<x-application.settings-section id="avatar-storage-section" title="Image storage"
|
||||
helper="Choose where compressed profile pictures and project icons are stored. Use S3 for multi-instance or cloud deployments so every application replica can access the same files.">
|
||||
<div class="max-w-md">
|
||||
<x-forms.listbox id="avatar_storage" label="Storage destination" onChange="instantSave"
|
||||
:options="$avatar_storage_options" />
|
||||
|
||||
Reference in new issue
Block a user