Merge pull request #1339 from coollabsio/next

v4.0.0-beta.93
This commit is contained in:
Andras Bacsai 2023-10-18 11:30:40 +02:00 committed by GitHub
commit 59d6818f70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 160 additions and 44 deletions

View File

@ -3,12 +3,9 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component class General extends Component
{ {
@ -23,6 +20,10 @@ class General extends Component
public ?string $git_commit_sha = null; public ?string $git_commit_sha = null;
public string $build_pack; public string $build_pack;
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled; public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled; public bool $is_git_lfs_enabled;
@ -52,6 +53,7 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@ -73,16 +75,47 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
]; ];
public function updatedApplicationBuildPack(){ public function mount()
{
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (is_null(data_get($this->application, 'custom_labels'))) {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false; $this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save(); $this->application->settings->save();
} }
$this->submit(); $this->submit();
} }
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode(",", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function instantSave() public function instantSave()
{ {
// @TODO: find another way - if possible // @TODO: find another way - if possible
@ -102,37 +135,36 @@ class General extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
public function getWildcardDomain() { public function getWildcardDomain()
{
$server = data_get($this->application, 'destination.server'); $server = data_get($this->application, 'destination.server');
if ($server) { if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
} }
} }
public function mount() public function resetDefaultLabels($showToaster = true)
{ {
if (data_get($this->application,'settings')) { $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->is_static = $this->application->settings->is_static; $this->submit($showToaster);
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
} }
public function submit() public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->emit('success', 'Labels reseted to default!');
}
public function submit($showToaster = true)
{ {
try { try {
$this->validate(); $this->validate();
if (data_get($this->application,'build_pack') === 'dockerimage') { if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([ $this->validate([
'application.docker_registry_image_name' => 'required', 'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required', 'application.docker_registry_image_tag' => 'required',
@ -156,10 +188,17 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') { if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/'); $this->application->publish_directory = rtrim($this->application->publish_directory, '/');
} }
if (gettype($this->customLabels) === 'string') {
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
} }
} }
} }

View File

@ -54,7 +54,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ApplicationPreview|null $preview = null; private ApplicationPreview|null $preview = null;
private string $container_name; private string $container_name;
private string|null $currently_running_container_name = null; private ?string $currently_running_container_name = null;
private string $basedir; private string $basedir;
private string $workdir; private string $workdir;
private ?string $build_pack = null; private ?string $build_pack = null;
@ -78,7 +78,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); // ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue; $this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
@ -166,7 +166,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Get user home directory // Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
ray("test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'");
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
try { try {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
@ -186,6 +185,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
ray($e); ray($e);
$this->fail($e); $this->fail($e);
@ -354,14 +354,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->execute_remote_command([ $this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]); ]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'" "echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
]); ]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
}
} }
$this->cleanup_git(); $this->cleanup_git();
$this->generate_nixpacks_confs(); $this->generate_nixpacks_confs();
@ -650,6 +655,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray();
}
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@ -658,7 +667,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name, 'container_name' => $this->container_name,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview, $ports), 'labels' => $labels,
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,

View File

@ -277,4 +277,31 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function isConfigurationChanged($save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
} }

View File

@ -538,7 +538,7 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
@ -568,7 +568,7 @@ class Service extends BaseModel
'networks' => $topLevelNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
]; ];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2); $this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2); $this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save(); $this->save();
$this->saveComposeConfigs(); $this->saveComposeConfigs();
return collect([]); return collect([]);

View File

@ -147,12 +147,11 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; return $labels;
} }
function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled, $onlyPort = null) function fqdnLabelsForTraefik($uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
foreach ($domains as $domain) { foreach ($domains as $domain) {
$uuid = (string)new Cuid2(7);
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
@ -205,20 +204,21 @@ function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled,
return $labels; return $labels;
} }
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null, $ports): array function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
{ {
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$onlyPort = null; $onlyPort = null;
if (count($ports) === 1) { if (count($ports) === 1) {
$onlyPort = $ports[0]; $onlyPort = $ports[0];
} }
$pull_request_id = data_get($preview, 'pull_request_id', 0); $pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application, $pull_request_id); // $container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id; $appId = $application->id;
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $pull_request_id; $appId = $appId . '-pr-' . $pull_request_id;
} }
$labels = collect([]); $labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id)); $labels = $labels->merge(defaultLabels($appId, $application->uuid, $pull_request_id));
if ($application->fqdn) { if ($application->fqdn) {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(','); $domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@ -226,7 +226,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled,$onlyPort)); $labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
} }
return $labels->all(); return $labels->all();
} }

View File

@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.92', 'release' => '4.0.0-beta.93',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.92'; return '4.0.0-beta.93';

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_labels')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('custom_labels');
});
}
};

View File

@ -5,8 +5,12 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($isConfigurationChanged && !is_null($application->config_hash))
<div class="font-bold text-warning">Configuration not applied to the running application. You need to
redeploy.</div>
@endif
</div> </div>
<div class="">General configuration for your application.</div> <div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row"> <div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.name" label="Name" required /> <x-forms.input id="application.name" label="Name" required />
@ -81,7 +85,6 @@
@if ($application->dockerfile) @if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static)
@ -93,6 +96,12 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." /> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div> </div>
@if ($labelsChanged)
<x-forms.textarea label="Custom Labels" rows="15" id="customLabels"></x-forms.textarea>
@else
<x-forms.textarea label="Coolify Generated Labels" rows="15" id="customLabels"></x-forms.textarea>
@endif
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
</div> </div>
<h3>Advanced</h3> <h3>Advanced</h3>
<div class="flex flex-col"> <div class="flex flex-col">

View File

@ -46,9 +46,11 @@
</div> </div>
@if (Str::of(data_get($application, 'status'))->startsWith('running')) @if (Str::of(data_get($application, 'status'))->startsWith('running'))
<div class="absolute bg-green-400 -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('exited')) @elseif (Str::of(data_get($application, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($application, 'status'))->startsWith('restarting'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif @endif
</a> </a>
@endforeach @endforeach
@ -60,9 +62,11 @@
<div class="text-xs text-gray-400 group-hover:text-white">{{ $database->description }}</div> <div class="text-xs text-gray-400 group-hover:text-white">{{ $database->description }}</div>
</div> </div>
@if (Str::of(data_get($database, 'status'))->startsWith('running')) @if (Str::of(data_get($database, 'status'))->startsWith('running'))
<div class="absolute bg-green-400 -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('exited')) @elseif (Str::of(data_get($database, 'status'))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(data_get($database, 'status'))->startsWith('restaring'))
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@endif @endif
</a> </a>
@endforeach @endforeach
@ -74,9 +78,9 @@
<div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div> <div class="text-xs text-gray-400 group-hover:text-white">{{ $service->description }}</div>
</div> </div>
@if (Str::of(serviceStatus($service))->startsWith('running')) @if (Str::of(serviceStatus($service))->startsWith('running'))
<div class="absolute bg-green-400 -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('degraded')) @elseif (Str::of(serviceStatus($service))->startsWith('degraded'))
<div class="absolute bg-yellow-400 -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
@elseif (Str::of(serviceStatus($service))->startsWith('exited')) @elseif (Str::of(serviceStatus($service))->startsWith('exited'))
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div> <div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
@endif @endif

View File

@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.92" "version": "4.0.0-beta.93"
} }
} }
} }