mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-20 00:15:06 +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:
1 parent
0b843bb07c
commit
8ae56587d5
497 files changed
+44045
-22630
No files matched your search
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Application\Deployment;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -28,6 +30,18 @@ class Index extends Component
|
||||
|
||||
public ?string $pull_request_id = null;
|
||||
|
||||
public array $pullRequestOptions = [];
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public string $deploymentFilter = 'all';
|
||||
|
||||
public string $deploymentSort = 'newest';
|
||||
|
||||
public array $statusFilterOptions = [];
|
||||
|
||||
public array $sourceFilterOptions = [];
|
||||
|
||||
protected $queryString = ['pull_request_id'];
|
||||
|
||||
public function getListeners()
|
||||
@@ -64,8 +78,16 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->defaultTake, $this->pull_request_id);
|
||||
$this->application = $application;
|
||||
$this->loadPullRequestOptions();
|
||||
$this->loadDeploymentFilterOptions();
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(
|
||||
search: $this->search,
|
||||
filter: $this->deploymentFilter,
|
||||
sort: $this->deploymentSort,
|
||||
take: $this->defaultTake,
|
||||
pullRequestId: $this->pull_request_id,
|
||||
);
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
$this->current_url = url()->current();
|
||||
@@ -114,14 +136,75 @@ class Index extends Component
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function goToPage(int $page): void
|
||||
{
|
||||
$lastPage = max(1, (int) ceil($this->deployments_count / $this->defaultTake));
|
||||
$page = max(1, min($page, $lastPage));
|
||||
$this->skip = ($page - 1) * $this->defaultTake;
|
||||
$this->showPrev = $page > 1;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function loadDeployments()
|
||||
{
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->defaultTake, $this->pull_request_id);
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments(
|
||||
skip: $this->skip,
|
||||
take: $this->defaultTake,
|
||||
pullRequestId: $this->pull_request_id,
|
||||
search: $this->search,
|
||||
filter: $this->deploymentFilter,
|
||||
sort: $this->deploymentSort,
|
||||
);
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
$this->showMore();
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
public function setDeploymentFilter(string $filter): void
|
||||
{
|
||||
$validFilters = collect($this->statusFilterOptions)
|
||||
->concat($this->sourceFilterOptions)
|
||||
->pluck('value')
|
||||
->push('all');
|
||||
|
||||
if (! $validFilters->contains($filter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deploymentFilter = $filter;
|
||||
$this->pull_request_id = null;
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
public function setPullRequestFilter(string $pullRequestId): void
|
||||
{
|
||||
$validPullRequestIds = collect($this->pullRequestOptions)->pluck('value');
|
||||
|
||||
if (! $validPullRequestIds->contains($pullRequestId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pull_request_id = $pullRequestId === '' ? null : $pullRequestId;
|
||||
$this->deploymentFilter = 'all';
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
public function setDeploymentSort(string $sort): void
|
||||
{
|
||||
if (! in_array($sort, ['newest', 'oldest'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deploymentSort = $sort;
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
public function updatedPullRequestId($value)
|
||||
{
|
||||
// Sanitize and validate the pull request ID
|
||||
@@ -139,20 +222,15 @@ class Index extends Component
|
||||
$this->pull_request_id = null;
|
||||
}
|
||||
|
||||
// Reset pagination when filter changes
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
$this->deploymentFilter = 'all';
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
public function clearFilter()
|
||||
{
|
||||
$this->pull_request_id = null;
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
$this->deploymentFilter = 'all';
|
||||
$this->resetPaginationAndLoad();
|
||||
}
|
||||
|
||||
private function updateCurrentPage()
|
||||
@@ -160,6 +238,95 @@ class Index extends Component
|
||||
$this->currentPage = intval($this->skip / $this->defaultTake) + 1;
|
||||
}
|
||||
|
||||
private function loadPullRequestOptions(): void
|
||||
{
|
||||
$pullRequestIds = ApplicationDeploymentQueue::query()
|
||||
->where('application_id', $this->application->id)
|
||||
->where('pull_request_id', '>', 0)
|
||||
->distinct()
|
||||
->orderByDesc('pull_request_id')
|
||||
->pluck('pull_request_id')
|
||||
->map(fn ($pullRequestId) => (string) $pullRequestId)
|
||||
->values();
|
||||
|
||||
if ($this->pull_request_id && ! $pullRequestIds->contains($this->pull_request_id)) {
|
||||
$this->pull_request_id = null;
|
||||
}
|
||||
|
||||
$this->pullRequestOptions = collect([
|
||||
['value' => '', 'label' => 'All deployments'],
|
||||
])
|
||||
->concat($pullRequestIds->map(fn ($pullRequestId) => [
|
||||
'value' => $pullRequestId,
|
||||
'label' => "Pull request #{$pullRequestId}",
|
||||
]))
|
||||
->all();
|
||||
}
|
||||
|
||||
private function loadDeploymentFilterOptions(): void
|
||||
{
|
||||
$statuses = ApplicationDeploymentQueue::query()
|
||||
->where('application_id', $this->application->id)
|
||||
->distinct()
|
||||
->pluck('status');
|
||||
|
||||
$statusLabels = [
|
||||
ApplicationDeploymentStatus::FINISHED->value => 'Success',
|
||||
ApplicationDeploymentStatus::FAILED->value => 'Failed',
|
||||
ApplicationDeploymentStatus::IN_PROGRESS->value => 'In progress',
|
||||
ApplicationDeploymentStatus::QUEUED->value => 'Queued',
|
||||
ApplicationDeploymentStatus::CANCELLED_BY_USER->value => 'Cancelled',
|
||||
];
|
||||
|
||||
$this->statusFilterOptions = collect($statusLabels)
|
||||
->filter(fn (string $label, string $status) => $statuses->contains($status))
|
||||
->map(fn (string $label, string $status) => [
|
||||
'value' => "status:{$status}",
|
||||
'label' => $label,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$sourceKeys = ApplicationDeploymentQueue::query()
|
||||
->where('application_id', $this->application->id)
|
||||
->selectRaw("
|
||||
CASE
|
||||
WHEN pull_request_id > 0 THEN 'pull-request'
|
||||
WHEN is_webhook THEN 'webhook'
|
||||
WHEN rollback THEN 'rollback'
|
||||
WHEN is_api THEN 'api'
|
||||
ELSE 'manual'
|
||||
END AS source_key
|
||||
")
|
||||
->distinct()
|
||||
->pluck('source_key');
|
||||
|
||||
$sourceLabels = [
|
||||
'manual' => 'Manual',
|
||||
'pull-request' => 'Pull requests',
|
||||
'webhook' => 'Webhooks',
|
||||
'api' => 'API',
|
||||
'rollback' => 'Rollbacks',
|
||||
];
|
||||
|
||||
$this->sourceFilterOptions = collect($sourceLabels)
|
||||
->filter(fn (string $label, string $source) => $sourceKeys->contains($source))
|
||||
->map(fn (string $label, string $source) => [
|
||||
'value' => "source:{$source}",
|
||||
'label' => $label,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function resetPaginationAndLoad(): void
|
||||
{
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.deployment.index');
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -94,6 +94,9 @@ class General extends Component
|
||||
|
||||
public bool $isSpa = false;
|
||||
|
||||
/** UI-only aggregate of isStatic/isSpa: dynamic | static | spa */
|
||||
public string $siteType = 'dynamic';
|
||||
|
||||
public bool $isBuildServerEnabled = false;
|
||||
|
||||
public bool $isPreserveRepositoryEnabled = false;
|
||||
@@ -437,6 +440,7 @@ class General extends Component
|
||||
// Application settings properties
|
||||
$this->isStatic = $this->application->settings->is_static;
|
||||
$this->isSpa = $this->application->settings->is_spa;
|
||||
$this->siteType = $this->isStatic ? ($this->isSpa ? 'spa' : 'static') : 'dynamic';
|
||||
$this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
|
||||
$this->isPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->isContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
@@ -444,6 +448,13 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function setSiteType(): void
|
||||
{
|
||||
$this->isStatic = $this->siteType !== 'dynamic';
|
||||
$this->isSpa = $this->siteType === 'spa';
|
||||
$this->instantSave();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
@@ -680,7 +691,10 @@ class General extends Component
|
||||
if ($this->application->additional_servers->count() === 0) {
|
||||
foreach ($domains as $domain) {
|
||||
if (! validateDNSEntry($domain, $this->application->destination->server)) {
|
||||
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$server = $this->application->destination->server;
|
||||
$target = serverDnsTargetIp($server) ?? $server->ip;
|
||||
$guidance = dnsMismatchGuidanceMessage($target, $target);
|
||||
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "{$guidance}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,7 +736,10 @@ class General extends Component
|
||||
$this->application->redirect = $this->redirect;
|
||||
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
if ($has_www === 0 && $this->application->redirect === 'www') {
|
||||
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
|
||||
$server = $this->application->destination?->server;
|
||||
$target = $server ? (serverDnsTargetIp($server) ?? $server->ip) : null;
|
||||
$dnsHint = dnsMismatchGuidanceMessage($target, $target);
|
||||
$this->dispatch('error', "You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list ({$dnsHint}).");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -847,7 +864,10 @@ class General extends Component
|
||||
$domain = data_get($service, 'domain');
|
||||
if ($domain) {
|
||||
if (! validateDNSEntry($domain, $this->application->destination->server)) {
|
||||
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$server = $this->application->destination->server;
|
||||
$target = serverDnsTargetIp($server) ?? $server->ip;
|
||||
$guidance = dnsMismatchGuidanceMessage($target, $target);
|
||||
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "{$guidance}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ class Heading extends Component
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public string $activeRouteName = '';
|
||||
|
||||
protected string $deploymentUuid;
|
||||
|
||||
public bool $docker_cleanup = true;
|
||||
@@ -38,6 +40,7 @@ class Heading extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->activeRouteName = request()->route()?->getName() ?? '';
|
||||
$this->parameters = [
|
||||
'project_uuid' => $this->application->project()->uuid,
|
||||
'environment_uuid' => $this->application->environment->uuid,
|
||||
|
||||
@@ -129,7 +129,10 @@ class Previews extends Component
|
||||
$this->previewFqdns[$previewKey] = $fqdn;
|
||||
|
||||
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
|
||||
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$server = $this->application->destination->server;
|
||||
$target = serverDnsTargetIp($server) ?? $server->ip;
|
||||
$guidance = dnsMismatchGuidanceMessage($target, $target);
|
||||
$this->dispatch('error', 'Validating DNS failed.', "{$guidance}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$success = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -252,9 +252,17 @@ class BackupEdit extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
// Instance databases (e.g. coolify-db) have no project/environment.
|
||||
// Stay on the current page (settings.backup) instead of redirecting.
|
||||
$project = $database->project();
|
||||
$environment = $database->environment;
|
||||
if (! $project || ! $environment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return redirect()->route('project.database.backup.executions', [
|
||||
'project_uuid' => $database->project()->uuid,
|
||||
'environment_uuid' => $database->environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
'backup_uuid' => $this->backup->uuid,
|
||||
]);
|
||||
|
||||
@@ -2,28 +2,71 @@
|
||||
|
||||
namespace App\Livewire\Project;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $projects;
|
||||
|
||||
public $servers;
|
||||
|
||||
public $private_keys;
|
||||
|
||||
public function mount()
|
||||
public function mount(): void
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeamCached();
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->servers = Server::ownedByCurrentTeamCached();
|
||||
// Only load what the page renders. Servers/private keys were previously
|
||||
// hydrated into public Livewire state but never used by the view.
|
||||
$this->projects = Project::ownedByCurrentTeam()
|
||||
->with(['environments:id,uuid,name,project_id'])
|
||||
->withCount([
|
||||
'applications',
|
||||
'services',
|
||||
'postgresqls',
|
||||
'redis',
|
||||
'keydbs',
|
||||
'dragonflies',
|
||||
'clickhouses',
|
||||
'mongodbs',
|
||||
'mysqls',
|
||||
'mariadbs',
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.project.index');
|
||||
return view('livewire.project.index', [
|
||||
'projectsJs' => $this->projects->map(function (Project $project): array {
|
||||
$firstEnvironment = $project->environments->first();
|
||||
$resourceCount = collect([
|
||||
$project->applications_count,
|
||||
$project->services_count,
|
||||
$project->postgresqls_count,
|
||||
$project->redis_count,
|
||||
$project->keydbs_count,
|
||||
$project->dragonflies_count,
|
||||
$project->clickhouses_count,
|
||||
$project->mongodbs_count,
|
||||
$project->mysqls_count,
|
||||
$project->mariadbs_count,
|
||||
])->sum();
|
||||
|
||||
return [
|
||||
'uuid' => $project->uuid,
|
||||
'name' => $project->name,
|
||||
'description' => $project->description,
|
||||
'href' => $project->navigateTo(),
|
||||
'environmentCount' => $project->environments->count(),
|
||||
'resourceCount' => $resourceCount,
|
||||
'settingsHref' => auth()->user()->can('update', $project)
|
||||
? route('project.edit', ['project_uuid' => $project->uuid])
|
||||
: null,
|
||||
'addResourceHref' => $firstEnvironment && auth()->user()->can('createAnyResource')
|
||||
? route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $firstEnvironment->uuid,
|
||||
])
|
||||
: null,
|
||||
];
|
||||
})->values()->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -161,13 +161,15 @@ class Select extends Component
|
||||
[
|
||||
'id' => 'public',
|
||||
'name' => 'Public Repository',
|
||||
'description' => 'You can deploy any kind of public repositories from the supported git providers.',
|
||||
'description' => 'Connect any public Git repository and let Coolify build and deploy it from source.',
|
||||
'documentation' => 'https://coolify.io/docs/applications/ci-cd',
|
||||
'logo' => asset('svgs/git.svg'),
|
||||
],
|
||||
[
|
||||
'id' => 'private-gh-app',
|
||||
'name' => 'Private Repository (with GitHub App)',
|
||||
'description' => 'You can deploy public & private repositories through your GitHub Apps.',
|
||||
'description' => 'Deploy a private GitHub repository with automatic webhooks and pull request support.',
|
||||
'documentation' => 'https://coolify.io/docs/applications/ci-cd/github/setup-app',
|
||||
'logo' => asset('svgs/github.svg'),
|
||||
],
|
||||
[
|
||||
@@ -179,7 +181,8 @@ class Select extends Component
|
||||
[
|
||||
'id' => 'private-deploy-key',
|
||||
'name' => 'Private Repository (with Deploy Key)',
|
||||
'description' => 'You can deploy private repositories with a deploy key.',
|
||||
'description' => 'Connect a private Git repository over SSH using a repository-scoped deploy key.',
|
||||
'documentation' => 'https://coolify.io/docs/applications/ci-cd/github/deploy-key',
|
||||
'logo' => asset('svgs/git.svg'),
|
||||
],
|
||||
];
|
||||
@@ -187,19 +190,22 @@ class Select extends Component
|
||||
[
|
||||
'id' => 'dockerfile',
|
||||
'name' => 'Dockerfile',
|
||||
'description' => 'You can deploy a simple Dockerfile, without Git.',
|
||||
'description' => 'Build and deploy an application from a Dockerfile without connecting a Git repository.',
|
||||
'documentation' => 'https://coolify.io/docs/applications/build-packs/dockerfile',
|
||||
'logo' => asset('svgs/docker.svg'),
|
||||
],
|
||||
[
|
||||
'id' => 'docker-compose-empty',
|
||||
'name' => 'Docker Compose Empty',
|
||||
'description' => 'You can deploy complex application easily with Docker Compose, without Git.',
|
||||
'description' => 'Create a multi-container application and provide the Docker Compose definition manually.',
|
||||
'documentation' => 'https://coolify.io/docs/applications/build-packs/docker-compose',
|
||||
'logo' => asset('svgs/docker.svg'),
|
||||
],
|
||||
[
|
||||
'id' => 'docker-image',
|
||||
'name' => 'Docker Image',
|
||||
'description' => 'You can deploy an existing Docker Image from any Registry, without Git.',
|
||||
'description' => 'Run a prebuilt image from Docker Hub or another compatible container registry.',
|
||||
'documentation' => 'https://coolify.io/docs/applications',
|
||||
'logo' => asset('svgs/docker.svg'),
|
||||
],
|
||||
];
|
||||
@@ -286,6 +292,13 @@ class Select extends Component
|
||||
|
||||
private function serviceTemplatesLastUpdated(): ?string
|
||||
{
|
||||
$fetchedAt = get_service_templates_fetched_at();
|
||||
if ($fetchedAt instanceof CarbonImmutable) {
|
||||
return $fetchedAt
|
||||
->timezone(config('app.timezone'))
|
||||
->format('M j, Y H:i');
|
||||
}
|
||||
|
||||
return $this->formatLastModified($this->serviceTemplatesPath());
|
||||
}
|
||||
|
||||
|
||||
@@ -188,24 +188,26 @@ class Index extends Component
|
||||
'dragonflies' => $this->dragonflies,
|
||||
'clickhouses' => $this->clickhouses,
|
||||
'services' => $this->services,
|
||||
'applicationsJs' => $this->toSearchableArray($this->applications),
|
||||
'postgresqlsJs' => $this->toSearchableArray($this->postgresqls),
|
||||
'redisJs' => $this->toSearchableArray($this->redis),
|
||||
'mongodbsJs' => $this->toSearchableArray($this->mongodbs),
|
||||
'mysqlsJs' => $this->toSearchableArray($this->mysqls),
|
||||
'mariadbsJs' => $this->toSearchableArray($this->mariadbs),
|
||||
'keydbsJs' => $this->toSearchableArray($this->keydbs),
|
||||
'dragonfliesJs' => $this->toSearchableArray($this->dragonflies),
|
||||
'clickhousesJs' => $this->toSearchableArray($this->clickhouses),
|
||||
'servicesJs' => $this->toSearchableArray($this->services),
|
||||
'applicationsJs' => $this->toSearchableArray($this->applications, 'application', 'Application'),
|
||||
'postgresqlsJs' => $this->toSearchableArray($this->postgresqls, 'database', 'PostgreSQL'),
|
||||
'redisJs' => $this->toSearchableArray($this->redis, 'database', 'Redis'),
|
||||
'mongodbsJs' => $this->toSearchableArray($this->mongodbs, 'database', 'MongoDB'),
|
||||
'mysqlsJs' => $this->toSearchableArray($this->mysqls, 'database', 'MySQL'),
|
||||
'mariadbsJs' => $this->toSearchableArray($this->mariadbs, 'database', 'MariaDB'),
|
||||
'keydbsJs' => $this->toSearchableArray($this->keydbs, 'database', 'KeyDB'),
|
||||
'dragonfliesJs' => $this->toSearchableArray($this->dragonflies, 'database', 'Dragonfly'),
|
||||
'clickhousesJs' => $this->toSearchableArray($this->clickhouses, 'database', 'ClickHouse'),
|
||||
'servicesJs' => $this->toSearchableArray($this->services, 'service', 'Service'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function toSearchableArray(Collection $items): array
|
||||
private function toSearchableArray(Collection $items, string $type, string $typeLabel): array
|
||||
{
|
||||
return $items->map(fn ($item) => [
|
||||
'uuid' => $item->uuid,
|
||||
'name' => $item->name,
|
||||
'type' => $type,
|
||||
'typeLabel' => $item instanceof V5Application ? 'Application (V5)' : $typeLabel,
|
||||
'fqdn' => $item->fqdn ?? null,
|
||||
'description' => $item instanceof V5Application ? 'Managed by Coolify V5' : ($item->description ?? null),
|
||||
'status' => $item->status ?? '',
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -282,6 +282,18 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function enablePublicAccess(): void
|
||||
{
|
||||
$this->isPublic = true;
|
||||
$this->instantSave();
|
||||
}
|
||||
|
||||
public function disablePublicAccess(): void
|
||||
{
|
||||
$this->isPublic = false;
|
||||
$this->instantSave();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@ class Show extends Component
|
||||
|
||||
public string $type;
|
||||
|
||||
public int $tableAlphabeticalOrder = 0;
|
||||
|
||||
public int $tableCreationOrder = 0;
|
||||
|
||||
public string $key;
|
||||
|
||||
public ?string $value = null;
|
||||
@@ -63,6 +67,18 @@ class Show extends Component
|
||||
|
||||
public bool $isValueHidden = false;
|
||||
|
||||
/**
|
||||
* Decrypted value / real_value are only needed in the edit modal (or after save).
|
||||
* Keeping them unloaded for table rows avoids decrypting every visible env on each page change.
|
||||
*/
|
||||
public bool $valuesLoaded = false;
|
||||
|
||||
/**
|
||||
* Entangled with the edit modal open state so the modal stays open across the
|
||||
* async loadValues() re-render (open immediately, decrypt after).
|
||||
*/
|
||||
public bool $editorOpen = false;
|
||||
|
||||
public array $problematicVariables = [];
|
||||
|
||||
protected $listeners = [
|
||||
@@ -116,10 +132,33 @@ class Show extends Component
|
||||
if (! $this->env->exists || ! $this->env->fresh()) {
|
||||
return;
|
||||
}
|
||||
$this->valuesLoaded = false;
|
||||
$this->syncData();
|
||||
$this->checkEnvs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and resolve values only when the edit modal is opened.
|
||||
*/
|
||||
public function loadValues(): void
|
||||
{
|
||||
if ($this->valuesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// List queries omit the encrypted value column; refresh so edit has a full model.
|
||||
if ($this->env->exists) {
|
||||
$fresh = $this->env->fresh();
|
||||
if ($fresh) {
|
||||
$fresh->setAppends([]);
|
||||
$this->env = $fresh;
|
||||
}
|
||||
}
|
||||
|
||||
$this->hydrateValueFields();
|
||||
$this->valuesLoaded = true;
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
@@ -149,29 +188,55 @@ class Show extends Component
|
||||
$this->env->is_literal = $this->is_literal;
|
||||
$this->env->is_shown_once = $this->is_shown_once;
|
||||
$this->env->save();
|
||||
$this->valuesLoaded = true;
|
||||
} else {
|
||||
// Table metadata only — never decrypt here. Values load via loadValues().
|
||||
$this->env->setAppends([]);
|
||||
$this->key = $this->env->key;
|
||||
$this->value = $this->env->value;
|
||||
$this->comment = $this->env->comment;
|
||||
$this->is_multiline = $this->env->is_multiline;
|
||||
$this->is_literal = $this->env->is_literal;
|
||||
$this->is_shown_once = $this->env->is_shown_once;
|
||||
$this->is_runtime = $this->env->is_runtime ?? true;
|
||||
$this->is_buildtime = $this->env->is_buildtime ?? true;
|
||||
$this->is_required = $this->env->is_required ?? false;
|
||||
$this->is_really_required = $this->env->is_really_required ?? false;
|
||||
$this->is_shared = $this->env->is_shared ?? false;
|
||||
$this->real_value = $this->env->real_value;
|
||||
$this->is_multiline = (bool) $this->env->is_multiline;
|
||||
$this->is_literal = (bool) $this->env->is_literal;
|
||||
$this->is_shown_once = (bool) $this->env->is_shown_once;
|
||||
$this->is_runtime = (bool) ($this->env->is_runtime ?? true);
|
||||
$this->is_buildtime = (bool) ($this->env->is_buildtime ?? true);
|
||||
$this->is_required = (bool) ($this->env->is_required ?? false);
|
||||
// Use the stored column, not the value-based accessor (that decrypts).
|
||||
$this->is_shared = (bool) ($this->env->getAttributes()['is_shared'] ?? false);
|
||||
$this->isValueHidden = auth()->user()?->isMember() ?? false;
|
||||
|
||||
if ($this->env->is_shown_once || auth()->user()?->isMember()) {
|
||||
if ($this->valuesLoaded) {
|
||||
$this->hydrateValueFields();
|
||||
} else {
|
||||
$this->value = null;
|
||||
$this->real_value = null;
|
||||
// Required badge: without decrypting, show when flagged required.
|
||||
// Exact empty-value state is refined when the edit modal opens.
|
||||
$this->is_really_required = $this->is_required;
|
||||
}
|
||||
|
||||
$this->isValueHidden = auth()->user()?->isMember() ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private function hydrateValueFields(): void
|
||||
{
|
||||
$this->value = $this->env->value;
|
||||
$this->is_shared = (bool) ($this->env->is_shared ?? false);
|
||||
|
||||
if ($this->is_shared) {
|
||||
$this->real_value = $this->env->real_value;
|
||||
$this->is_really_required = $this->is_required && blank($this->real_value);
|
||||
} else {
|
||||
$this->real_value = null;
|
||||
$this->is_really_required = $this->is_required && blank($this->value);
|
||||
}
|
||||
|
||||
if ($this->env->is_shown_once || auth()->user()?->isMember()) {
|
||||
$this->value = null;
|
||||
$this->real_value = null;
|
||||
}
|
||||
|
||||
$this->isValueHidden = auth()->user()?->isMember() ?? false;
|
||||
}
|
||||
|
||||
public function checkEnvs()
|
||||
{
|
||||
$this->isDisabled = false;
|
||||
@@ -215,6 +280,7 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->env);
|
||||
$this->loadValues();
|
||||
|
||||
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
|
||||
$oldValue = $this->env->getOriginal('value');
|
||||
@@ -238,7 +304,23 @@ class Show extends Component
|
||||
#[Computed]
|
||||
public function availableSharedVariables(): array
|
||||
{
|
||||
// Shared across all Show row components in the same request (edit modals).
|
||||
static $requestCache = [];
|
||||
|
||||
$team = currentTeam();
|
||||
$cacheKey = implode('|', [
|
||||
$team?->id ?? 'none',
|
||||
data_get($this->parameters, 'project_uuid', ''),
|
||||
data_get($this->parameters, 'environment_uuid', ''),
|
||||
data_get($this->parameters, 'server_uuid', ''),
|
||||
data_get($this->parameters, 'application_uuid', ''),
|
||||
data_get($this->parameters, 'service_uuid', ''),
|
||||
]);
|
||||
|
||||
if (array_key_exists($cacheKey, $requestCache)) {
|
||||
return $requestCache[$cacheKey];
|
||||
}
|
||||
|
||||
$result = [
|
||||
'team' => [],
|
||||
'project' => [],
|
||||
@@ -248,7 +330,7 @@ class Show extends Component
|
||||
|
||||
// Early return if no team
|
||||
if (! $team) {
|
||||
return $result;
|
||||
return $requestCache[$cacheKey] = $result;
|
||||
}
|
||||
|
||||
// Check if user can view team variables
|
||||
@@ -359,7 +441,7 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $requestCache[$cacheKey] = $result;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
|
||||
@@ -16,6 +16,8 @@ class ShowHardcoded extends Component
|
||||
|
||||
public ?string $serviceName = null;
|
||||
|
||||
public bool $isPreview = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->key = $this->env['key'];
|
||||
|
||||
@@ -80,14 +80,31 @@ class Logs extends Component
|
||||
]);
|
||||
|
||||
return $containers->toArray();
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
|
||||
if ($containers && $containers->count() > 0) {
|
||||
return $containers->sort()->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Docker labels differ by resource type:
|
||||
// applications → coolify.applicationId, services → coolify.serviceId, databases → coolify.databaseId
|
||||
$containers = match (true) {
|
||||
$this->resource instanceof Application => getCurrentApplicationContainerStatus(
|
||||
$server,
|
||||
$this->resource->id,
|
||||
includePullrequests: true
|
||||
),
|
||||
$this->resource instanceof Service => getCurrentServiceContainerStatus(
|
||||
$server,
|
||||
$this->resource->id
|
||||
),
|
||||
default => getCurrentDatabaseContainerStatus(
|
||||
$server,
|
||||
$this->resource->id
|
||||
),
|
||||
};
|
||||
|
||||
if ($containers && $containers->count() > 0) {
|
||||
return $containers->sort()->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
// Log error but don't fail the entire operation
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ class Terminal extends Component
|
||||
|
||||
public bool $isTerminalConnected = false;
|
||||
|
||||
public string $variant = 'default';
|
||||
|
||||
private function checkShellAvailability(Server $server, string $container): bool
|
||||
{
|
||||
$escapedContainer = escapeshellarg($container);
|
||||
|
||||
@@ -62,7 +62,7 @@ class Webhooks extends Component
|
||||
'manual_webhook_secret_bitbucket' => $this->bitbucketManualWebhookSecret,
|
||||
'manual_webhook_secret_gitea' => $this->giteaManualWebhookSecret,
|
||||
]);
|
||||
$this->dispatch('success', 'Secret Saved.');
|
||||
$this->dispatch('success', 'Webhook secrets saved.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Project;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -34,7 +35,25 @@ class Show extends Component
|
||||
public function mount(string $project_uuid)
|
||||
{
|
||||
try {
|
||||
$this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail();
|
||||
$this->project = Project::where('team_id', currentTeam()->id)
|
||||
->where('uuid', $project_uuid)
|
||||
->with([
|
||||
'environments' => fn ($query) => $query
|
||||
->withCount([
|
||||
'applications',
|
||||
'services',
|
||||
'postgresqls',
|
||||
'redis',
|
||||
'keydbs',
|
||||
'dragonflies',
|
||||
'clickhouses',
|
||||
'mongodbs',
|
||||
'mysqls',
|
||||
'mariadbs',
|
||||
])
|
||||
->orderBy('created_at'),
|
||||
])
|
||||
->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -68,8 +87,49 @@ class Show extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.project.show');
|
||||
$canUpdateProject = auth()->user()->can('update', $this->project);
|
||||
$canCreateResource = auth()->user()->can('createAnyResource');
|
||||
|
||||
return view('livewire.project.show', [
|
||||
'environmentsJs' => $this->project->environments->map(function (Environment $environment) use ($canCreateResource, $canUpdateProject): array {
|
||||
$resourceCount = collect([
|
||||
$environment->applications_count,
|
||||
$environment->services_count,
|
||||
$environment->postgresqls_count,
|
||||
$environment->redis_count,
|
||||
$environment->keydbs_count,
|
||||
$environment->dragonflies_count,
|
||||
$environment->clickhouses_count,
|
||||
$environment->mongodbs_count,
|
||||
$environment->mysqls_count,
|
||||
$environment->mariadbs_count,
|
||||
])->sum();
|
||||
|
||||
return [
|
||||
'uuid' => $environment->uuid,
|
||||
'name' => $environment->name,
|
||||
'description' => $environment->description,
|
||||
'resourceCount' => $resourceCount,
|
||||
'href' => route('project.resource.index', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
]),
|
||||
'settingsHref' => $canUpdateProject
|
||||
? route('project.environment.edit', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
])
|
||||
: null,
|
||||
'addResourceHref' => $canCreateResource
|
||||
? route('project.resource.create', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
])
|
||||
: null,
|
||||
];
|
||||
})->values()->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user