mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 20:15:45 +00:00
Merge remote-tracking branch 'origin/next' into audit-policies
This commit is contained in:
commit
9dca7ca351
148 files changed
+5987
-1101
No files matched your search
@@ -8,6 +8,7 @@ use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Url;
|
||||
@@ -218,6 +219,23 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'remoteServerName' => 'required|string',
|
||||
'remoteServerHost' => 'required|string',
|
||||
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
||||
'remoteServerUser' => ValidationPatterns::serverUsernameRules(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function messages(): array
|
||||
{
|
||||
return [
|
||||
...ValidationPatterns::serverUsernameMessages('remoteServerUser', 'SSH User'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getProxyType()
|
||||
{
|
||||
$this->selectProxy(ProxyTypes::TRAEFIK->value);
|
||||
@@ -281,12 +299,7 @@ class Index extends Component
|
||||
|
||||
public function saveServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerName' => 'required|string',
|
||||
'remoteServerHost' => 'required|string',
|
||||
'remoteServerPort' => 'required|integer',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$this->authorize('create', Server::class);
|
||||
@@ -478,10 +491,10 @@ class Index extends Component
|
||||
|
||||
public function saveAndValidateServer()
|
||||
{
|
||||
$this->validate([
|
||||
'remoteServerPort' => 'required|integer|min:1|max:65535',
|
||||
'remoteServerUser' => 'required|string',
|
||||
]);
|
||||
$this->validate(array_intersect_key($this->rules(), array_flip([
|
||||
'remoteServerPort',
|
||||
'remoteServerUser',
|
||||
])));
|
||||
|
||||
$this->createdServer->update([
|
||||
'port' => $this->remoteServerPort,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Destination;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class Resources extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public $destination;
|
||||
|
||||
public array $resources = [];
|
||||
|
||||
public function mount(string $destination_uuid)
|
||||
{
|
||||
try {
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
return redirect()->route('destination.index');
|
||||
}
|
||||
if (! $destination instanceof StandaloneDocker) {
|
||||
return redirect()->route('destination.show', ['destination_uuid' => $destination->uuid]);
|
||||
}
|
||||
|
||||
$this->destination = $destination;
|
||||
$this->loadResources();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load applications, services, and database resources deployed to the standalone Docker destination.
|
||||
*
|
||||
* @return void Populates the resources property for display.
|
||||
*/
|
||||
public function loadResources(): void
|
||||
{
|
||||
$this->resources = $this->collectResources([
|
||||
$this->destination->applications,
|
||||
$this->destination->services,
|
||||
$this->destination->postgresqls,
|
||||
$this->destination->redis,
|
||||
$this->destination->mongodbs,
|
||||
$this->destination->mysqls,
|
||||
$this->destination->mariadbs,
|
||||
$this->destination->keydbs,
|
||||
$this->destination->dragonflies,
|
||||
$this->destination->clickhouses,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, iterable<Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse>> $groups
|
||||
* @return array<int, array{uuid:string,type:string,name:string,project:string|null,environment:string|null,url:string|null,search:string}>
|
||||
*/
|
||||
protected function collectResources(array $groups): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group as $resource) {
|
||||
$rows[] = $this->resourceRow($resource);
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource
|
||||
* @return array{uuid:string,type:string,name:string,project:string|null,environment:string|null,url:string|null,search:string}
|
||||
*/
|
||||
protected function resourceRow(BaseModel $resource): array
|
||||
{
|
||||
$type = match (true) {
|
||||
$resource instanceof Application => 'application',
|
||||
$resource instanceof Service => 'service',
|
||||
default => 'database',
|
||||
};
|
||||
$environment = $resource->environment;
|
||||
$project = $environment?->project;
|
||||
$routeName = "project.{$type}.configuration";
|
||||
$url = ($project && $environment)
|
||||
? route($routeName, [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
"{$type}_uuid" => $resource->uuid,
|
||||
])
|
||||
: null;
|
||||
|
||||
return [
|
||||
'uuid' => $resource->uuid,
|
||||
'type' => $type,
|
||||
'name' => $resource->name,
|
||||
'project' => $project?->name,
|
||||
'environment' => $environment?->name,
|
||||
'url' => $url,
|
||||
'search' => strtolower(implode(' ', array_filter([
|
||||
$type,
|
||||
$resource->name,
|
||||
$project?->name,
|
||||
$environment?->name,
|
||||
]))),
|
||||
];
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.destination.resources');
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,9 @@ class Advanced extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isConnectToDockerNetworkEnabled = false;
|
||||
|
||||
#[Validate(['integer', 'min:0'])]
|
||||
public int $maxRestartCount = 10;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -149,6 +152,7 @@ class Advanced extends Component
|
||||
$this->disableBuildCache = $this->application->settings->disable_build_cache;
|
||||
$this->injectBuildArgsToDockerfile = $this->application->settings->inject_build_args_to_dockerfile ?? true;
|
||||
$this->includeSourceCommitInBuild = $this->application->settings->include_source_commit_in_build ?? false;
|
||||
$this->maxRestartCount = $this->application->max_restart_count ?? 10;
|
||||
}
|
||||
|
||||
// Load stop_grace_period separately since it has its own save handler
|
||||
@@ -289,6 +293,21 @@ class Advanced extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function saveMaxRestartCount()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->validate([
|
||||
'maxRestartCount' => 'integer|min:0',
|
||||
]);
|
||||
$this->application->max_restart_count = $this->maxRestartCount;
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Max restart count saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.advanced');
|
||||
|
||||
@@ -28,7 +28,7 @@ class Configuration extends Component
|
||||
|
||||
$project = currentTeam()
|
||||
->projects()
|
||||
->select('id', 'uuid', 'team_id')
|
||||
->select('id', 'uuid', 'name', 'team_id')
|
||||
->where('uuid', request()->route('project_uuid'))
|
||||
->firstOrFail();
|
||||
$environment = $project->environments()
|
||||
|
||||
@@ -154,7 +154,7 @@ class General extends Component
|
||||
'staticImage' => 'required',
|
||||
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
|
||||
'publishDirectory' => ValidationPatterns::directoryPathRules(),
|
||||
'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'],
|
||||
'portsExposes' => ['nullable', 'string', 'regex:/^(\d+)(,\d+)*$/'],
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'customNetworkAliases' => 'nullable',
|
||||
'dockerfile' => 'nullable',
|
||||
@@ -212,7 +212,6 @@ class General extends Component
|
||||
'buildPack.required' => 'The Build Pack field is required.',
|
||||
'staticImage.required' => 'The Static Image field is required.',
|
||||
'baseDirectory.required' => 'The Base Directory field is required.',
|
||||
'portsExposes.required' => 'The Exposed Ports field is required.',
|
||||
'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).',
|
||||
...ValidationPatterns::portMappingMessages(),
|
||||
'isStatic.required' => 'The Static setting is required.',
|
||||
@@ -760,7 +759,7 @@ class General extends Component
|
||||
|
||||
$this->resetErrorBag();
|
||||
|
||||
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString();
|
||||
$this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString() ?: null;
|
||||
if ($this->portsMappings) {
|
||||
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class Configuration extends Component
|
||||
|
||||
$project = currentTeam()
|
||||
->projects()
|
||||
->select('id', 'uuid', 'team_id')
|
||||
->select('id', 'uuid', 'name', 'team_id')
|
||||
->where('uuid', request()->route('project_uuid'))
|
||||
->firstOrFail();
|
||||
$environment = $project->environments()
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class Select extends Component
|
||||
@@ -105,7 +107,9 @@ class Select extends Component
|
||||
public function loadServices()
|
||||
{
|
||||
$services = get_service_templates();
|
||||
$services = collect($services)->map(function ($service, $key) {
|
||||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
|
||||
|
||||
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
|
||||
$default_logo = 'images/default.webp';
|
||||
$logo = data_get($service, 'logo', $default_logo);
|
||||
$local_logo_path = public_path($logo);
|
||||
@@ -116,6 +120,7 @@ class Select extends Component
|
||||
'logo_github_url' => file_exists($local_logo_path)
|
||||
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
|
||||
: asset($default_logo),
|
||||
'templateLastUpdated' => $templateLastUpdatedMap[(string) $key] ?? null,
|
||||
] + (array) $service;
|
||||
})->all();
|
||||
|
||||
@@ -247,6 +252,7 @@ class Select extends Component
|
||||
];
|
||||
|
||||
return [
|
||||
'serviceTemplatesLastUpdated' => $this->serviceTemplatesLastUpdated(),
|
||||
'services' => $services,
|
||||
'categories' => $categories,
|
||||
'gitBasedApplications' => $gitBasedApplications,
|
||||
@@ -268,6 +274,55 @@ class Select extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function serviceTemplatesLastUpdated(): ?string
|
||||
{
|
||||
return $this->formatLastModified($this->serviceTemplatesPath());
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
|
||||
{
|
||||
$bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
|
||||
|
||||
return Cache::remember(
|
||||
"service-template-last-updated-map:{$bundleMtime}",
|
||||
now()->addDay(),
|
||||
fn () => $serviceNames
|
||||
->mapWithKeys(fn ($serviceName) => [
|
||||
(string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
|
||||
])
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdated(string $serviceName): ?string
|
||||
{
|
||||
foreach (['yaml', 'yml'] as $extension) {
|
||||
$templatePath = base_path("templates/compose/{$serviceName}.{$extension}");
|
||||
|
||||
if (file_exists($templatePath)) {
|
||||
return $this->formatLastModified($templatePath);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function serviceTemplatesPath(): string
|
||||
{
|
||||
return base_path('templates/'.config('constants.services.file_name'));
|
||||
}
|
||||
|
||||
private function formatLastModified(string $path): ?string
|
||||
{
|
||||
if (! file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CarbonImmutable::createFromTimestamp(filemtime($path))
|
||||
->timezone(config('app.timezone'))
|
||||
->format('M j, Y H:i');
|
||||
}
|
||||
|
||||
public function setType(string $type)
|
||||
{
|
||||
$type = str($type)->lower()->slug()->value();
|
||||
|
||||
@@ -44,7 +44,7 @@ class Configuration extends Component
|
||||
$this->query = request()->query();
|
||||
$project = currentTeam()
|
||||
->projects()
|
||||
->select('id', 'uuid', 'team_id')
|
||||
->select('id', 'uuid', 'name', 'team_id')
|
||||
->where('uuid', request()->route('project_uuid'))
|
||||
->firstOrFail();
|
||||
$environment = $project->environments()
|
||||
|
||||
@@ -21,8 +21,6 @@ class ConfigurationChecker extends Component
|
||||
|
||||
public array $configurationDiff = [];
|
||||
|
||||
public array $groupedConfigurationChanges = [];
|
||||
|
||||
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
|
||||
|
||||
public function getListeners(): array
|
||||
@@ -50,21 +48,56 @@ class ConfigurationChecker extends Component
|
||||
$this->configurationChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Members must never see environment variable values, so redact every
|
||||
* environment-section change before it is serialized to the browser.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $changes
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function redactEnvironmentChanges(array $changes, bool $redact): array
|
||||
{
|
||||
if (! $redact) {
|
||||
return $changes;
|
||||
}
|
||||
|
||||
return collect($changes)
|
||||
->map(function (array $change): array {
|
||||
if (data_get($change, 'section') !== 'environment') {
|
||||
return $change;
|
||||
}
|
||||
|
||||
$change['old_display_value'] = data_get($change, 'old_display_value') === '-' ? '-' : '••••••••';
|
||||
$change['new_display_value'] = data_get($change, 'new_display_value') === '-' ? '-' : '••••••••';
|
||||
$change['old_full_value'] = null;
|
||||
$change['new_full_value'] = null;
|
||||
$change['expandable'] = false;
|
||||
$change['display_summary'] = data_get($change, 'type') === 'changed' ? 'Changed' : null;
|
||||
|
||||
return $change;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
public function configurationChanged(): void
|
||||
{
|
||||
$this->resource->refresh();
|
||||
|
||||
if ($this->resource instanceof Application) {
|
||||
$diff = $this->resource->pendingDeploymentConfigurationDiff();
|
||||
// Fail closed: only owners/admins may see unlocked env values.
|
||||
$redactEnvironment = ! (bool) auth()->user()?->isAdmin();
|
||||
|
||||
$array = $diff->toArray();
|
||||
$array['changes'] = $this->redactEnvironmentChanges($array['changes'] ?? [], $redactEnvironment);
|
||||
|
||||
$this->isConfigurationChanged = $diff->isChanged();
|
||||
$this->configurationDiff = $diff->toArray();
|
||||
$this->groupedConfigurationChanges = $diff->groupedChanges();
|
||||
$this->configurationDiff = $array;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isConfigurationChanged = $this->resource->isConfigurationChanged();
|
||||
$this->configurationDiff = [];
|
||||
$this->groupedConfigurationChanges = [];
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use App\Models\EnvironmentVariable;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
class All extends Component
|
||||
@@ -25,6 +27,8 @@ class All extends Component
|
||||
|
||||
public string $view = 'normal';
|
||||
|
||||
public string $search = '';
|
||||
|
||||
public bool $is_env_sorting_enabled = false;
|
||||
|
||||
public bool $use_build_secrets = false;
|
||||
@@ -35,6 +39,20 @@ class All extends Component
|
||||
'environmentVariableDeleted' => 'refreshEnvs',
|
||||
];
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
}
|
||||
|
||||
private function clearEnvironmentVariableCaches(): void
|
||||
{
|
||||
unset($this->environmentVariables);
|
||||
unset($this->environmentVariablesPreview);
|
||||
unset($this->hardcodedEnvironmentVariables);
|
||||
unset($this->hardcodedEnvironmentVariablesPreview);
|
||||
unset($this->hasEnvironmentVariables);
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
|
||||
@@ -65,8 +83,27 @@ class All extends Component
|
||||
|
||||
public function getEnvironmentVariablesProperty()
|
||||
{
|
||||
$query = $this->resource->environment_variables()
|
||||
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
||||
return $this->getEnvironmentVariables(false);
|
||||
}
|
||||
|
||||
public function getEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
return $this->getEnvironmentVariables(true);
|
||||
}
|
||||
|
||||
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection
|
||||
{
|
||||
$query = $isPreview
|
||||
? $this->resource->environment_variables_preview()
|
||||
: $this->resource->environment_variables();
|
||||
|
||||
$query->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
||||
|
||||
if ($withSearch && $this->searchTerm() !== '') {
|
||||
$escapedSearch = addcslashes(Str::lower($this->searchTerm()), '%_\\');
|
||||
|
||||
$query->whereRaw("LOWER(key) LIKE ? ESCAPE '\\'", ['%'.$escapedSearch.'%']);
|
||||
}
|
||||
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$query->orderBy('key');
|
||||
@@ -77,18 +114,17 @@ class All extends Component
|
||||
return $this->nullLockedValues($query->get());
|
||||
}
|
||||
|
||||
public function getEnvironmentVariablesPreviewProperty()
|
||||
private function searchTerm(): string
|
||||
{
|
||||
$query = $this->resource->environment_variables_preview()
|
||||
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
||||
return trim($this->search);
|
||||
}
|
||||
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$query->orderBy('key');
|
||||
} else {
|
||||
$query->orderBy('order');
|
||||
}
|
||||
|
||||
return $this->nullLockedValues($query->get());
|
||||
public function getHasEnvironmentVariablesProperty(): bool
|
||||
{
|
||||
return $this->environmentVariables->isNotEmpty() ||
|
||||
$this->environmentVariablesPreview->isNotEmpty() ||
|
||||
$this->hardcodedEnvironmentVariables->isNotEmpty() ||
|
||||
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty();
|
||||
}
|
||||
|
||||
private function nullLockedValues($envs)
|
||||
@@ -105,6 +141,11 @@ class All extends Component
|
||||
return $envs;
|
||||
}
|
||||
|
||||
public function getIsSearchActiveProperty(): bool
|
||||
{
|
||||
return $this->searchTerm() !== '';
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(false);
|
||||
@@ -152,6 +193,12 @@ class All extends Component
|
||||
return ! in_array($var['key'], $managedKeys);
|
||||
});
|
||||
|
||||
if ($this->searchTerm() !== '') {
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
||||
return str($var['key'])->contains($this->searchTerm(), true);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting based on is_env_sorting_enabled
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$hardcodedVars = $hardcodedVars->sortBy('key')->values();
|
||||
@@ -163,9 +210,9 @@ class All extends Component
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
|
||||
$this->variables = $this->formatEnvironmentVariables($this->getEnvironmentVariables(false, false));
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->formatEnvironmentVariables($this->environmentVariablesPreview);
|
||||
$this->variablesPreview = $this->formatEnvironmentVariables($this->getEnvironmentVariables(true, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +348,7 @@ class All extends Component
|
||||
$environment->order = $maxOrder + 1;
|
||||
$environment->save();
|
||||
|
||||
// Clear computed property cache to force refresh
|
||||
unset($this->environmentVariables);
|
||||
unset($this->environmentVariablesPreview);
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
|
||||
$this->dispatch('success', 'Environment variable added.');
|
||||
}
|
||||
@@ -432,9 +477,7 @@ class All extends Component
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
// Clear computed property cache to force refresh
|
||||
unset($this->environmentVariables);
|
||||
unset($this->environmentVariablesPreview);
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
$this->getDevView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Models\Service;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class ResourceDetails extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $resource;
|
||||
|
||||
public ?string $project_uuid = null;
|
||||
|
||||
public ?string $project_name = null;
|
||||
|
||||
public ?string $environment_uuid = null;
|
||||
|
||||
public ?string $environment_name = null;
|
||||
|
||||
public ?string $server_uuid = null;
|
||||
|
||||
public ?string $server_name = null;
|
||||
|
||||
public array $stack_applications = [];
|
||||
|
||||
public array $stack_databases = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->resource);
|
||||
|
||||
$environment = $this->resource->environment ?? null;
|
||||
if ($environment) {
|
||||
$this->environment_uuid = $environment->uuid;
|
||||
$this->environment_name = $environment->name;
|
||||
$project = $environment->project ?? null;
|
||||
if ($project) {
|
||||
$this->project_uuid = $project->uuid;
|
||||
$this->project_name = $project->name;
|
||||
}
|
||||
}
|
||||
|
||||
$server = $this->resolveServer();
|
||||
if ($server) {
|
||||
$this->server_uuid = $server->uuid;
|
||||
$this->server_name = $server->name;
|
||||
}
|
||||
|
||||
if ($this->resource instanceof Service) {
|
||||
$this->stack_applications = $this->resource->applications
|
||||
->map(fn ($app) => [
|
||||
'name' => $app->human_name ?: $app->name,
|
||||
'uuid' => $app->uuid,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$this->stack_databases = $this->resource->databases
|
||||
->map(fn ($db) => [
|
||||
'name' => $db->human_name ?: $db->name,
|
||||
'uuid' => $db->uuid,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveServer()
|
||||
{
|
||||
try {
|
||||
if (isset($this->resource->destination) && $this->resource->destination && isset($this->resource->destination->server)) {
|
||||
return $this->resource->destination->server;
|
||||
}
|
||||
if (method_exists($this->resource, 'server') && $this->resource->server) {
|
||||
return $this->resource->server;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.resource-details');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Charts extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Server $server;
|
||||
|
||||
public $chartId = 'server';
|
||||
@@ -28,6 +32,29 @@ class Charts extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleMetrics(): void
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->settings->is_metrics_enabled = ! $this->server->settings->is_metrics_enabled;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
|
||||
if ($this->server->isMetricsEnabled()) {
|
||||
StartSentinel::run($this->server, true);
|
||||
$this->dispatch('success', 'Metrics enabled. Starting Sentinel.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->redirect(route('server.metrics', ['server_uuid' => $this->server->uuid]), navigate: true);
|
||||
} else {
|
||||
$this->server->restartSentinel();
|
||||
$this->dispatch('success', 'Metrics disabled. Restarting Sentinel.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function pollData()
|
||||
{
|
||||
if ($this->poll || $this->interval <= 10) {
|
||||
|
||||
@@ -57,7 +57,7 @@ class ByIp extends Component
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'ip' => ['required', 'string', new ValidServerIp],
|
||||
'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||
'user' => ValidationPatterns::serverUsernameRules(),
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'is_build_server' => 'required|boolean',
|
||||
];
|
||||
@@ -75,6 +75,7 @@ class ByIp extends Component
|
||||
'ip.string' => 'The IP Address/Domain must be a string.',
|
||||
'user.required' => 'The User field is required.',
|
||||
'user.string' => 'The User field must be a string.',
|
||||
...ValidationPatterns::serverUsernameMessages(),
|
||||
'port.required' => 'The Port field is required.',
|
||||
'port.integer' => 'The Port field must be an integer.',
|
||||
'port.between' => 'The Port field must be between 1 and 65535.',
|
||||
|
||||
@@ -15,8 +15,6 @@ class Sentinel extends Component
|
||||
|
||||
public Server $server;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
public bool $isMetricsEnabled;
|
||||
|
||||
#[Validate(['required', 'string', 'max:500', 'regex:/\A[a-zA-Z0-9._\-+=\/]+\z/'])]
|
||||
@@ -51,15 +49,9 @@ class Sentinel extends Component
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->syncData();
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
@@ -112,27 +104,29 @@ class Sentinel extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsSentinelEnabled($value)
|
||||
public function toggleSentinel(): void
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageSentinel', $this->server);
|
||||
if ($value === true) {
|
||||
if (! $this->isSentinelEnabled) {
|
||||
if ($this->server->isBuildServer()) {
|
||||
$this->isSentinelEnabled = false;
|
||||
$this->dispatch('error', 'Sentinel cannot be enabled on build servers.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->isSentinelEnabled = true;
|
||||
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
|
||||
StartSentinel::run($this->server, true, null, $customImage);
|
||||
} else {
|
||||
$this->isSentinelEnabled = false;
|
||||
$this->isMetricsEnabled = false;
|
||||
$this->isSentinelDebugEnabled = false;
|
||||
StopSentinel::dispatch($this->server);
|
||||
}
|
||||
$this->submit();
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Sentinel;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Logs extends Component
|
||||
{
|
||||
public ?Server $server = null;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.server.sentinel.logs');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Sentinel;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public ?Server $server = null;
|
||||
|
||||
public array $parameters = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.server.sentinel.show');
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class Show extends Component
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'ip' => ['required', new ValidServerIp],
|
||||
'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||
'user' => ValidationPatterns::serverUsernameRules(),
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'connectionTimeout' => 'required|integer|min:1|max:300',
|
||||
'validationLogs' => 'nullable',
|
||||
@@ -140,6 +140,7 @@ class Show extends Component
|
||||
[
|
||||
'ip.required' => 'The IP Address field is required.',
|
||||
'user.required' => 'The User field is required.',
|
||||
...ValidationPatterns::serverUsernameMessages(),
|
||||
'port.required' => 'The Port field is required.',
|
||||
'connectionTimeout.required' => 'The SSH Connection Timeout field is required.',
|
||||
'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.',
|
||||
|
||||
@@ -8,6 +8,15 @@ use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\ScheduledTaskExecution;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Services\SchedulerLogParser;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -125,7 +134,21 @@ class ScheduledJobs extends Component
|
||||
: collect();
|
||||
|
||||
$backups = $backupIds->isNotEmpty()
|
||||
? ScheduledDatabaseBackup::with(['database.environment.project'])->whereIn('id', $backupIds)->get()->keyBy('id')
|
||||
? ScheduledDatabaseBackup::with('database')
|
||||
->whereIn('id', $backupIds)
|
||||
->get()
|
||||
->loadMorph('database', [
|
||||
ServiceDatabase::class => ['service.environment.project'],
|
||||
StandaloneClickhouse::class => ['environment.project'],
|
||||
StandaloneDragonfly::class => ['environment.project'],
|
||||
StandaloneKeydb::class => ['environment.project'],
|
||||
StandaloneMariadb::class => ['environment.project'],
|
||||
StandaloneMongodb::class => ['environment.project'],
|
||||
StandaloneMysql::class => ['environment.project'],
|
||||
StandalonePostgresql::class => ['environment.project'],
|
||||
StandaloneRedis::class => ['environment.project'],
|
||||
])
|
||||
->keyBy('id')
|
||||
: collect();
|
||||
|
||||
$servers = $serverIds->isNotEmpty()
|
||||
@@ -161,14 +184,29 @@ class ScheduledJobs extends Component
|
||||
if ($backup) {
|
||||
$database = $backup->database;
|
||||
$skip['resource_name'] = $database?->name ?? 'Database backup';
|
||||
$environment = $database?->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment && $database) {
|
||||
$skip['link'] = route('project.database.backup.index', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
]);
|
||||
|
||||
if ($database instanceof ServiceDatabase) {
|
||||
$service = $database->service;
|
||||
$environment = $service?->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment && $service) {
|
||||
$skip['link'] = route('project.service.database.backups', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'service_uuid' => $service->uuid,
|
||||
'stack_service_uuid' => $database->uuid,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$environment = $database?->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment && $database) {
|
||||
$skip['link'] = route('project.database.backup.index', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($skip['type'] === 'docker_cleanup') {
|
||||
|
||||
@@ -210,6 +210,9 @@ class Change extends Component
|
||||
|
||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->syncData(false);
|
||||
$this->name = str($this->github_app->name)->kebab();
|
||||
|
||||
$this->dispatch('success', 'Github App permissions updated.');
|
||||
} catch (\Throwable $e) {
|
||||
// Provide better error message for unsupported key formats
|
||||
|
||||
@@ -61,7 +61,7 @@ class InviteLink extends Component
|
||||
if ($member_emails->contains($this->email)) {
|
||||
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
||||
}
|
||||
$uuid = new Cuid2(32);
|
||||
$uuid = (string) new Cuid2(32);
|
||||
$link = url('/').config('constants.invitation.link.base_url').$uuid;
|
||||
$user = User::whereEmail($this->email)->first();
|
||||
|
||||
@@ -73,7 +73,7 @@ class InviteLink extends Component
|
||||
'password' => Hash::make($password),
|
||||
'force_password_reset' => true,
|
||||
]);
|
||||
$token = Crypt::encryptString("{$user->email}@@@$password");
|
||||
$token = Crypt::encryptString("{$user->email}@@@{$uuid}@@@{$password}");
|
||||
$link = route('auth.link', ['token' => $token]);
|
||||
}
|
||||
$invitation = TeamInvitation::whereEmail($this->email)->first();
|
||||
|
||||
Reference in new issue
Block a user