mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-21 06:26:22 +00:00
feat(ui): Shadow UI redesign, domain management, and DNS autoconfigure (#11119)
Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
This commit is contained in:
co-authored by
ShadowArcanist
parent
0b843bb07c
commit
8ae56587d5
@@ -6,6 +6,7 @@ use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -29,10 +30,22 @@ class All extends Component
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public string $environmentFilter = 'all';
|
||||
|
||||
public int $page = 1;
|
||||
|
||||
public int $perPage = 10;
|
||||
|
||||
public bool $is_env_sorting_enabled = false;
|
||||
|
||||
public bool $use_build_secrets = false;
|
||||
|
||||
/**
|
||||
* Environment variable rows are loaded after first paint via wire:init
|
||||
* so the surrounding configuration page can render immediately.
|
||||
*/
|
||||
public bool $readyToLoad = false;
|
||||
|
||||
protected $listeners = [
|
||||
'saveKey' => 'submit',
|
||||
'refreshEnvs',
|
||||
@@ -41,6 +54,7 @@ class All extends Component
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->page = 1;
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
}
|
||||
|
||||
@@ -51,6 +65,11 @@ class All extends Component
|
||||
unset($this->hardcodedEnvironmentVariables);
|
||||
unset($this->hardcodedEnvironmentVariablesPreview);
|
||||
unset($this->hasEnvironmentVariables);
|
||||
unset($this->environmentVariableRows);
|
||||
unset($this->environmentVariablePageRows);
|
||||
unset($this->environmentVariableRowCount);
|
||||
unset($this->environmentVariableLastPage);
|
||||
unset($this->currentEnvironmentVariablePage);
|
||||
}
|
||||
|
||||
public function mount()
|
||||
@@ -63,7 +82,25 @@ class All extends Component
|
||||
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
|
||||
$this->showPreview = true;
|
||||
}
|
||||
$this->getDevView();
|
||||
// Intentionally skip loading env vars / developer-view bulk text here.
|
||||
// loadEnvironmentVariables() is triggered from the frontend via wire:init.
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend-initiated load of environment variables after the page shell paints.
|
||||
*/
|
||||
public function loadEnvironmentVariables(): void
|
||||
{
|
||||
if ($this->readyToLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->readyToLoad = true;
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
|
||||
if ($this->view === 'dev') {
|
||||
$this->getDevView();
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
@@ -71,10 +108,14 @@ class All extends Component
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->resource);
|
||||
|
||||
$this->page = 1;
|
||||
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
|
||||
$this->resource->settings->use_build_secrets = $this->use_build_secrets;
|
||||
$this->resource->settings->save();
|
||||
$this->getDevView();
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
if ($this->readyToLoad && $this->view === 'dev') {
|
||||
$this->getDevView();
|
||||
}
|
||||
$this->dispatch('success', 'Environment variable settings updated.');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -84,20 +125,30 @@ class All extends Component
|
||||
|
||||
public function getEnvironmentVariablesProperty()
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->getEnvironmentVariables(false);
|
||||
}
|
||||
|
||||
public function getEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->getEnvironmentVariables(true);
|
||||
}
|
||||
|
||||
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection
|
||||
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true, bool $withValue = true): Collection
|
||||
{
|
||||
if ($isPreview && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Full-value loads (dev view / tests) still use relationship queries.
|
||||
// List/table pagination uses fetchManagedEnvironmentVariables() without values.
|
||||
$query = $isPreview
|
||||
? $this->resource->environment_variables_preview()
|
||||
: $this->resource->environment_variables();
|
||||
@@ -116,7 +167,15 @@ class All extends Component
|
||||
$query->orderBy('order');
|
||||
}
|
||||
|
||||
return $this->nullLockedValues($query->get());
|
||||
if (! $withValue) {
|
||||
$query->select(self::LIST_COLUMNS);
|
||||
}
|
||||
|
||||
$variables = $query->get()->each(fn (EnvironmentVariable $environmentVariable) => $environmentVariable->setAppends([]));
|
||||
|
||||
return $withValue
|
||||
? $this->nullLockedValues($variables)
|
||||
: $variables;
|
||||
}
|
||||
|
||||
private function searchTerm(): string
|
||||
@@ -131,14 +190,11 @@ class All extends Component
|
||||
|
||||
public function getHasEnvironmentVariablesProperty(): bool
|
||||
{
|
||||
$hasPreviewEnvironmentVariables = $this->supportsPreviewEnvironmentVariables() && (
|
||||
$this->environmentVariablesPreview->isNotEmpty() ||
|
||||
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty()
|
||||
);
|
||||
if (! $this->readyToLoad) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->environmentVariables->isNotEmpty() ||
|
||||
$this->hardcodedEnvironmentVariables->isNotEmpty() ||
|
||||
$hasPreviewEnvironmentVariables;
|
||||
return $this->environmentVariableRowCount > 0;
|
||||
}
|
||||
|
||||
private function nullLockedValues($envs)
|
||||
@@ -162,14 +218,341 @@ class All extends Component
|
||||
|
||||
public function getHardcodedEnvironmentVariablesProperty()
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->getHardcodedVariables(false);
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->getHardcodedVariables(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full in-memory row list. Prefer page/count helpers for UI rendering so large
|
||||
* variable sets stay cheap; this remains for tests and non-paginated callers.
|
||||
*/
|
||||
public function getEnvironmentVariableRowsProperty(): Collection
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$rows = collect();
|
||||
|
||||
foreach ($this->environmentVariableSegments() as $segment) {
|
||||
if ($segment['kind'] === 'managed') {
|
||||
foreach ($this->fetchManagedEnvironmentVariables($segment['is_preview'], null, null) as $environmentVariable) {
|
||||
$rows->push($this->managedEnvironmentVariableRow($environmentVariable));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$hardcoded = $segment['is_preview']
|
||||
? $this->hardcodedEnvironmentVariablesPreview
|
||||
: $this->hardcodedEnvironmentVariables;
|
||||
|
||||
foreach ($hardcoded->values() as $index => $environmentVariable) {
|
||||
$rows->push($this->hardcodedEnvironmentVariableRow(
|
||||
$environmentVariable,
|
||||
$segment['is_preview'],
|
||||
$index,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $rows->values();
|
||||
}
|
||||
|
||||
public function getEnvironmentVariablePageRowsProperty(): Collection
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$page = $this->currentEnvironmentVariablePage;
|
||||
$offset = max(0, ($page - 1) * $this->perPage);
|
||||
$remaining = $this->perPage;
|
||||
$rows = collect();
|
||||
|
||||
foreach ($this->environmentVariableSegments() as $segment) {
|
||||
$segmentCount = $segment['count'];
|
||||
|
||||
if ($segmentCount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($offset >= $segmentCount) {
|
||||
$offset -= $segmentCount;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$take = min($remaining, $segmentCount - $offset);
|
||||
|
||||
if ($segment['kind'] === 'managed') {
|
||||
$variables = $this->fetchManagedEnvironmentVariables($segment['is_preview'], $offset, $take);
|
||||
foreach ($variables as $environmentVariable) {
|
||||
$rows->push($this->managedEnvironmentVariableRow($environmentVariable));
|
||||
}
|
||||
} else {
|
||||
$hardcoded = $segment['is_preview']
|
||||
? $this->hardcodedEnvironmentVariablesPreview
|
||||
: $this->hardcodedEnvironmentVariables;
|
||||
|
||||
foreach ($hardcoded->slice($offset, $take)->values() as $index => $environmentVariable) {
|
||||
$rows->push($this->hardcodedEnvironmentVariableRow(
|
||||
$environmentVariable,
|
||||
$segment['is_preview'],
|
||||
$offset + $index,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$remaining -= $take;
|
||||
$offset = 0;
|
||||
|
||||
if ($remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows->values();
|
||||
}
|
||||
|
||||
public function getEnvironmentVariableRowCountProperty(): int
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return collect($this->environmentVariableSegments())->sum('count');
|
||||
}
|
||||
|
||||
public function getEnvironmentVariableLastPageProperty(): int
|
||||
{
|
||||
return max(1, (int) ceil($this->environmentVariableRowCount / $this->perPage));
|
||||
}
|
||||
|
||||
public function getCurrentEnvironmentVariablePageProperty(): int
|
||||
{
|
||||
return min($this->page, $this->environmentVariableLastPage);
|
||||
}
|
||||
|
||||
public function setEnvironmentFilter(string $filter): void
|
||||
{
|
||||
if (! in_array($filter, ['all', 'production', 'preview'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->environmentFilter = $filter;
|
||||
$this->page = 1;
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
}
|
||||
|
||||
public function setEnvironmentVariablePage(int $page): void
|
||||
{
|
||||
$this->page = max(1, min($page, $this->environmentVariableLastPage));
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
}
|
||||
|
||||
public function previousEnvironmentVariablePage(): void
|
||||
{
|
||||
$this->setEnvironmentVariablePage($this->currentEnvironmentVariablePage - 1);
|
||||
}
|
||||
|
||||
public function nextEnvironmentVariablePage(): void
|
||||
{
|
||||
$this->setEnvironmentVariablePage($this->currentEnvironmentVariablePage + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered segments used for pagination: production managed → production hardcoded
|
||||
* → preview managed → preview hardcoded (matching the historical table order).
|
||||
*
|
||||
* @return list<array{kind: string, is_preview: bool, count: int}>
|
||||
*/
|
||||
private function environmentVariableSegments(): array
|
||||
{
|
||||
$includeProduction = $this->environmentFilter === 'all' || $this->environmentFilter === 'production';
|
||||
$includePreview = $this->supportsPreviewEnvironmentVariables()
|
||||
&& ($this->environmentFilter === 'all' || $this->environmentFilter === 'preview');
|
||||
|
||||
$segments = [];
|
||||
|
||||
if ($includeProduction) {
|
||||
$segments[] = [
|
||||
'kind' => 'managed',
|
||||
'is_preview' => false,
|
||||
'count' => $this->countManagedEnvironmentVariables(false),
|
||||
];
|
||||
|
||||
if ($this->showsHardcodedEnvironmentVariables()) {
|
||||
$segments[] = [
|
||||
'kind' => 'hardcoded',
|
||||
'is_preview' => false,
|
||||
'count' => $this->hardcodedEnvironmentVariables->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($includePreview) {
|
||||
$segments[] = [
|
||||
'kind' => 'managed',
|
||||
'is_preview' => true,
|
||||
'count' => $this->countManagedEnvironmentVariables(true),
|
||||
];
|
||||
|
||||
if ($this->showsHardcodedEnvironmentVariables()) {
|
||||
$segments[] = [
|
||||
'kind' => 'hardcoded',
|
||||
'is_preview' => true,
|
||||
'count' => $this->hardcodedEnvironmentVariablesPreview->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $segments;
|
||||
}
|
||||
|
||||
private function managedEnvironmentVariablesQuery(bool $isPreview): Builder
|
||||
{
|
||||
$query = EnvironmentVariable::query()
|
||||
->where('resourceable_type', $this->resource->getMorphClass())
|
||||
->where('resourceable_id', $this->resource->id)
|
||||
->where('is_preview', $isPreview);
|
||||
|
||||
$query->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
||||
|
||||
if ($this->searchTerm() !== '') {
|
||||
$escapedSearch = addcslashes(Str::lower($this->searchTerm()), '%_\\');
|
||||
$query->whereRaw("LOWER(key) LIKE ? ESCAPE '\\'", ['%'.$escapedSearch.'%']);
|
||||
}
|
||||
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$query->orderBy('key');
|
||||
} else {
|
||||
$query->orderBy('order')->orderBy('id');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function countManagedEnvironmentVariables(bool $isPreview): int
|
||||
{
|
||||
if ($isPreview && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->managedEnvironmentVariablesQuery($isPreview)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Columns needed to render the table row / nested Show component without decrypting secrets.
|
||||
* The encrypted `value` column is intentionally omitted until edit/dev view loads it.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const LIST_COLUMNS = [
|
||||
'id',
|
||||
'uuid',
|
||||
'key',
|
||||
'comment',
|
||||
'order',
|
||||
'is_preview',
|
||||
'is_multiline',
|
||||
'is_literal',
|
||||
'is_runtime',
|
||||
'is_buildtime',
|
||||
'is_required',
|
||||
'is_shown_once',
|
||||
'is_shared',
|
||||
'resourceable_type',
|
||||
'resourceable_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'version',
|
||||
];
|
||||
|
||||
private function fetchManagedEnvironmentVariables(?bool $isPreview, ?int $offset, ?int $limit, bool $withValue = false): Collection
|
||||
{
|
||||
if ($isPreview === true && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
if ($isPreview === null) {
|
||||
$variables = collect();
|
||||
|
||||
if ($this->environmentFilter === 'all' || $this->environmentFilter === 'production') {
|
||||
$variables = $variables->concat($this->fetchManagedEnvironmentVariables(false, null, null, $withValue));
|
||||
}
|
||||
|
||||
if ($this->supportsPreviewEnvironmentVariables()
|
||||
&& ($this->environmentFilter === 'all' || $this->environmentFilter === 'preview')) {
|
||||
$variables = $variables->concat($this->fetchManagedEnvironmentVariables(true, null, null, $withValue));
|
||||
}
|
||||
|
||||
return $variables->values();
|
||||
}
|
||||
|
||||
$query = $this->managedEnvironmentVariablesQuery($isPreview);
|
||||
|
||||
if (! $withValue) {
|
||||
$query->select(self::LIST_COLUMNS);
|
||||
}
|
||||
|
||||
if ($offset !== null) {
|
||||
$query->skip($offset);
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$query->take($limit);
|
||||
}
|
||||
|
||||
$variables = $query->get()->each(function (EnvironmentVariable $environmentVariable) {
|
||||
// Prevent accidental real_value / is_shared accessor work during Livewire hydration.
|
||||
$environmentVariable->setAppends([]);
|
||||
});
|
||||
|
||||
return $withValue
|
||||
? $this->nullLockedValues($variables)
|
||||
: $variables;
|
||||
}
|
||||
|
||||
private function managedEnvironmentVariableRow(EnvironmentVariable $environmentVariable): array
|
||||
{
|
||||
return [
|
||||
'id' => 'environment-'.$environmentVariable->id,
|
||||
'kind' => 'managed',
|
||||
'scope' => $environmentVariable->is_preview ? 'preview' : 'production',
|
||||
'environmentVariable' => $environmentVariable,
|
||||
];
|
||||
}
|
||||
|
||||
private function hardcodedEnvironmentVariableRow(array $environmentVariable, bool $isPreview, int $index): array
|
||||
{
|
||||
$scope = $isPreview ? 'preview' : 'production';
|
||||
|
||||
return [
|
||||
'id' => 'hardcoded-'.$scope.'-'.$environmentVariable['key'].'-'.($environmentVariable['service_name'] ?? 'default').'-'.$index,
|
||||
'kind' => 'hardcoded',
|
||||
'scope' => $scope,
|
||||
'environmentVariable' => $environmentVariable,
|
||||
];
|
||||
}
|
||||
|
||||
private function showsHardcodedEnvironmentVariables(): bool
|
||||
{
|
||||
return $this->resource->type() === 'service' || $this->resource?->build_pack === 'dockercompose';
|
||||
}
|
||||
|
||||
protected function getHardcodedVariables(bool $isPreview)
|
||||
{
|
||||
if ($isPreview && ! $this->supportsPreviewEnvironmentVariables()) {
|
||||
@@ -256,13 +639,17 @@ class All extends Component
|
||||
public function switch()
|
||||
{
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
if ($this->view === 'dev') {
|
||||
$this->ensureEnvironmentVariablesLoaded();
|
||||
$this->getDevView();
|
||||
}
|
||||
}
|
||||
|
||||
public function submit($data = null)
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->resource);
|
||||
$this->ensureEnvironmentVariablesLoaded();
|
||||
if ($data === null) {
|
||||
$this->handleBulkSubmit();
|
||||
} else {
|
||||
@@ -270,7 +657,9 @@ class All extends Component
|
||||
}
|
||||
|
||||
$this->updateOrder();
|
||||
$this->getDevView();
|
||||
if ($this->view === 'dev') {
|
||||
$this->getDevView();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
@@ -278,6 +667,13 @@ class All extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureEnvironmentVariablesLoaded(): void
|
||||
{
|
||||
if (! $this->readyToLoad) {
|
||||
$this->loadEnvironmentVariables();
|
||||
}
|
||||
}
|
||||
|
||||
private function updateOrder()
|
||||
{
|
||||
$variables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variables));
|
||||
@@ -495,7 +891,10 @@ class All extends Component
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
$this->readyToLoad = true;
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
$this->getDevView();
|
||||
if ($this->view === 'dev') {
|
||||
$this->getDevView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user