v4.0.0-beta.473 (#9521)
This commit is contained in:
commit
57ea0764b8
43 changed files with 1304 additions and 436 deletions
|
|
@ -23,24 +23,42 @@ class Upgrade extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->devMode = isDev();
|
||||
$this->refreshUpgradeState();
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
{
|
||||
try {
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
|
||||
if (isDev()) {
|
||||
$this->isUpgradeAvailable = true;
|
||||
}
|
||||
$this->refreshUpgradeState();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function refreshUpgradeState(): void
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->devMode = isDev();
|
||||
|
||||
if ($this->devMode) {
|
||||
$this->isUpgradeAvailable = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::find(0);
|
||||
$hasNewerVersion = version_compare($this->latestVersion, $this->currentVersion, '>');
|
||||
$newVersionAvailable = (bool) data_get($settings, 'new_version_available', false);
|
||||
|
||||
if ($settings && $newVersionAvailable && ! $hasNewerVersion) {
|
||||
$settings->update(['new_version_available' => false]);
|
||||
$newVersionAvailable = false;
|
||||
}
|
||||
|
||||
$this->isUpgradeAvailable = $hasNewerVersion && $newVersionAvailable;
|
||||
}
|
||||
|
||||
public function upgrade()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -71,25 +71,31 @@ protected static function booted()
|
|||
}
|
||||
});
|
||||
|
||||
static::deleting(function ($team) {
|
||||
$keys = $team->privateKeys;
|
||||
foreach ($keys as $key) {
|
||||
static::deleting(function (Team $team) {
|
||||
foreach ($team->privateKeys as $key) {
|
||||
$key->delete();
|
||||
}
|
||||
$sources = $team->sources();
|
||||
foreach ($sources as $source) {
|
||||
|
||||
// Transfer instance-wide sources to root team so they remain available
|
||||
GithubApp::where('team_id', $team->id)->where('is_system_wide', true)->update(['team_id' => 0]);
|
||||
GitlabApp::where('team_id', $team->id)->where('is_system_wide', true)->update(['team_id' => 0]);
|
||||
|
||||
// Delete non-instance-wide sources owned by this team
|
||||
$teamSources = GithubApp::where('team_id', $team->id)->get()
|
||||
->merge(GitlabApp::where('team_id', $team->id)->get());
|
||||
foreach ($teamSources as $source) {
|
||||
$source->delete();
|
||||
}
|
||||
$tags = Tag::whereTeamId($team->id)->get();
|
||||
foreach ($tags as $tag) {
|
||||
|
||||
foreach (Tag::whereTeamId($team->id)->get() as $tag) {
|
||||
$tag->delete();
|
||||
}
|
||||
$shared_variables = $team->environment_variables();
|
||||
foreach ($shared_variables as $shared_variable) {
|
||||
$shared_variable->delete();
|
||||
|
||||
foreach ($team->environment_variables()->get() as $sharedVariable) {
|
||||
$sharedVariable->delete();
|
||||
}
|
||||
$s3s = $team->s3s;
|
||||
foreach ($s3s as $s3) {
|
||||
|
||||
foreach ($team->s3s as $s3) {
|
||||
$s3->delete();
|
||||
}
|
||||
});
|
||||
|
|
@ -338,4 +344,5 @@ public function webhookNotificationSettings()
|
|||
{
|
||||
return $this->hasOne(WebhookNotificationSettings::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,10 +203,24 @@ public static function volumeNameMessages(string $field = 'name'): array
|
|||
}
|
||||
|
||||
/**
|
||||
* Pattern for port mappings (e.g. 3000:3000, 8080:80, 8000-8010:8000-8010)
|
||||
* Each entry requires host:container format, where each side can be a number or a range (number-number)
|
||||
* Pattern for port mappings with optional IP binding and protocol suffix on either side.
|
||||
* Format: [ip:]port[:ip:port] where IP is IPv4 or [IPv6], port can be a range, protocol suffix optional.
|
||||
* Examples: 8080:80, 127.0.0.1:8080:80, [::1]::80/udp, 127.0.0.1:8080:80/tcp
|
||||
*/
|
||||
public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/';
|
||||
public const PORT_MAPPINGS_PATTERN = '/^
|
||||
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)? # optional IP
|
||||
(?:\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?)? # optional host port
|
||||
:
|
||||
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)? # optional IP
|
||||
\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))? # container port
|
||||
(?:,
|
||||
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)?
|
||||
(?:\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?)?
|
||||
:
|
||||
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)?
|
||||
\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?
|
||||
)*
|
||||
$/x';
|
||||
|
||||
/**
|
||||
* Get validation rules for container name fields
|
||||
|
|
@ -230,7 +244,7 @@ public static function portMappingRules(): array
|
|||
public static function portMappingMessages(string $field = 'portsMappings'): array
|
||||
{
|
||||
return [
|
||||
"{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges (e.g. 3000:3000,8080:80,8000-8010:8000-8010).',
|
||||
"{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges with optional IP and protocol (e.g. 3000:3000, 8080:80/udp, 127.0.0.1:8080:80, [::1]::80).',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
14
composer.lock
generated
14
composer.lock
generated
|
|
@ -72,7 +72,6 @@
|
|||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67b6b6210af47319c74c5666388d71bc1bc58276",
|
||||
"reference": "67b6b6210af47319c74c5666388d71bc1bc58276",
|
||||
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -157,7 +156,6 @@
|
|||
"source": "https://github.com/aws/aws-sdk-php/tree/3.374.2"
|
||||
},
|
||||
"time": "2026-03-27T18:05:55+00:00"
|
||||
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
|
|
@ -5158,16 +5156,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.50",
|
||||
"version": "3.0.51",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -5248,7 +5246,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -5264,7 +5262,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-19T02:57:58+00:00"
|
||||
"time": "2026-04-10T01:33:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.472',
|
||||
'version' => '4.0.0-beta.473',
|
||||
'helper_version' => '1.0.13',
|
||||
'realtime_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.13',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12'
|
||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.13'
|
||||
ports:
|
||||
- "${SOKETI_PORT:-6001}:6001"
|
||||
- "6002:6002"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
|
||||
pull_policy: always
|
||||
container_name: coolify-realtime
|
||||
restart: always
|
||||
|
|
|
|||
21
docker/coolify-realtime/package-lock.json
generated
21
docker/coolify-realtime/package-lock.json
generated
|
|
@ -7,7 +7,7 @@
|
|||
"dependencies": {
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "6.0.0",
|
||||
"axios": "1.13.6",
|
||||
"axios": "1.15.0",
|
||||
"cookie": "1.1.1",
|
||||
"dotenv": "17.3.1",
|
||||
"node-pty": "1.1.0",
|
||||
|
|
@ -36,14 +36,14 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
|
|
@ -344,10 +344,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "6.0.0",
|
||||
"cookie": "1.1.1",
|
||||
"axios": "1.13.6",
|
||||
"axios": "1.15.0",
|
||||
"dotenv": "17.3.1",
|
||||
"node-pty": "1.1.0",
|
||||
"ws": "8.19.0"
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12'
|
||||
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.13'
|
||||
ports:
|
||||
- "${SOKETI_PORT:-6001}:6001"
|
||||
- "6002:6002"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
|
||||
pull_policy: always
|
||||
container_name: coolify-realtime
|
||||
restart: always
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.472"
|
||||
"version": "4.0.0-beta.473"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"version": "1.0.13"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "1.0.12"
|
||||
"version": "1.0.13"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "0.0.21"
|
||||
|
|
|
|||
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -16,7 +16,7 @@
|
|||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"axios": "1.13.2",
|
||||
"axios": "1.15.0",
|
||||
"laravel-echo": "2.2.7",
|
||||
"laravel-vite-plugin": "2.0.1",
|
||||
"postcss": "8.5.6",
|
||||
|
|
@ -1474,15 +1474,15 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
|
|
@ -2501,11 +2501,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pusher-js": {
|
||||
"version": "8.4.0",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"axios": "1.13.2",
|
||||
"axios": "1.15.0",
|
||||
"laravel-echo": "2.2.7",
|
||||
"laravel-vite-plugin": "2.0.1",
|
||||
"postcss": "8.5.6",
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ @utility dropdown-item {
|
|||
@apply flex relative gap-2 justify-start items-center py-1 pr-4 pl-2 w-full text-xs transition-colors cursor-pointer select-none dark:text-white hover:bg-neutral-100 dark:hover:bg-coollabs outline-none data-disabled:pointer-events-none data-disabled:opacity-50 focus-visible:bg-neutral-100 dark:focus-visible:bg-coollabs;
|
||||
}
|
||||
|
||||
@utility dropdown-item-touch {
|
||||
@apply min-h-10 px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
@utility dropdown-item-no-padding {
|
||||
@apply flex relative gap-2 justify-start items-center py-1 w-full text-xs transition-colors cursor-pointer select-none dark:text-white hover:bg-neutral-100 dark:hover:bg-coollabs outline-none data-disabled:pointer-events-none data-disabled:opacity-50 focus-visible:bg-neutral-100 dark:focus-visible:bg-coollabs;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,44 @@
|
|||
<div x-data="{
|
||||
dropdownOpen: false
|
||||
}" class="relative" @click.outside="dropdownOpen = false">
|
||||
<button @click="dropdownOpen=true"
|
||||
dropdownOpen: false,
|
||||
panelStyles: '',
|
||||
open() {
|
||||
this.dropdownOpen = true;
|
||||
this.updatePanelPosition();
|
||||
},
|
||||
close() {
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
updatePanelPosition() {
|
||||
if (window.innerWidth >= 768) {
|
||||
this.panelStyles = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const triggerRect = this.$refs.trigger.getBoundingClientRect();
|
||||
const panelRect = this.$refs.panel.getBoundingClientRect();
|
||||
const viewportPadding = 8;
|
||||
let left = triggerRect.left;
|
||||
|
||||
if ((left + panelRect.width + viewportPadding) > window.innerWidth) {
|
||||
left = window.innerWidth - panelRect.width - viewportPadding;
|
||||
}
|
||||
|
||||
left = Math.max(viewportPadding, left);
|
||||
|
||||
let top = triggerRect.bottom + 4;
|
||||
const maxTop = window.innerHeight - panelRect.height - viewportPadding;
|
||||
|
||||
if (top > maxTop) {
|
||||
top = Math.max(viewportPadding, triggerRect.top - panelRect.height - viewportPadding);
|
||||
}
|
||||
|
||||
this.panelStyles = `position: fixed; left: ${left}px; top: ${top}px;`;
|
||||
});
|
||||
}
|
||||
}" class="relative" @click.outside="close()" x-on:resize.window="if (dropdownOpen) updatePanelPosition()">
|
||||
<button x-ref="trigger" @click="dropdownOpen ? close() : open()"
|
||||
class="inline-flex items-center justify-start pr-8 transition-colors focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none">
|
||||
<span class="flex flex-col items-start h-full leading-none">
|
||||
{{ $title }}
|
||||
|
|
@ -13,11 +50,11 @@ class="inline-flex items-center justify-start pr-8 transition-colors focus:outli
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="dropdownOpen" @click.away="dropdownOpen=false" x-transition:enter="ease-out duration-200"
|
||||
<div x-ref="panel" x-show="dropdownOpen" @click.away="close()" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="-translate-y-2" x-transition:enter-end="translate-y-0"
|
||||
class="absolute top-0 z-50 mt-6 min-w-max" x-cloak>
|
||||
:style="panelStyles" class="absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6" x-cloak>
|
||||
<div
|
||||
class="p-1 mt-1 bg-white border rounded-sm shadow-sm dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
|
||||
class="border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@
|
|||
])
|
||||
|
||||
<div @class([
|
||||
'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit',
|
||||
'form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2',
|
||||
'w-full' => $fullWidth,
|
||||
'dark:hover:bg-coolgray-100 cursor-pointer' => !$disabled,
|
||||
])>
|
||||
<label @class(['flex gap-4 items-center px-0 min-w-fit label w-full'])>
|
||||
<span class="flex grow gap-2">
|
||||
<label @class(['label flex w-full max-w-full min-w-0 items-center gap-4 px-0'])>
|
||||
<span class="flex min-w-0 grow gap-2 break-words">
|
||||
@if ($label)
|
||||
@if ($disabled)
|
||||
<span class="opacity-60">{!! $label !!}</span>
|
||||
|
|
@ -29,16 +29,16 @@
|
|||
@endif
|
||||
</span>
|
||||
@if ($instantSave)
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
wire:loading.attr="disabled"
|
||||
wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
wire:model={{ $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@else
|
||||
@if ($domValue)
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@else
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@endif
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -129,7 +129,11 @@
|
|||
}"
|
||||
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
|
||||
class="relative w-auto h-auto">
|
||||
@if ($customButton)
|
||||
@if (isset($trigger))
|
||||
<div @click="modalOpen=true">
|
||||
{{ $trigger }}
|
||||
</div>
|
||||
@elseif ($customButton)
|
||||
@if ($buttonFullWidth)
|
||||
<x-forms.button @click="modalOpen=true" class="w-full">
|
||||
{{ $customButton }}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,47 @@
|
|||
$currentProjectUuid = data_get($resource, 'environment.project.uuid');
|
||||
$currentEnvironmentUuid = data_get($resource, 'environment.uuid');
|
||||
$currentResourceUuid = data_get($resource, 'uuid');
|
||||
$resourceUuid = data_get($resource, 'uuid');
|
||||
$resourceType = $resource->getMorphClass();
|
||||
$isApplication = $resourceType === 'App\Models\Application';
|
||||
$isService = $resourceType === 'App\Models\Service';
|
||||
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
|
||||
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') &&
|
||||
($resource->relationLoaded('additional_servers') ? $resource->additional_servers->count() > 0 : ($resource->additional_servers_count ?? 0) > 0);
|
||||
$serverName = $hasMultipleServers ? null : data_get($resource, 'destination.server.name');
|
||||
$routeParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
];
|
||||
if ($isApplication) {
|
||||
$routeParams['application_uuid'] = $resourceUuid;
|
||||
} elseif ($isService) {
|
||||
$routeParams['service_uuid'] = $resourceUuid;
|
||||
} else {
|
||||
$routeParams['database_uuid'] = $resourceUuid;
|
||||
}
|
||||
@endphp
|
||||
<nav class="flex pt-2 pb-10">
|
||||
<ol class="flex flex-wrap items-center gap-y-1">
|
||||
<nav class="pt-2 pb-4 md:pb-10">
|
||||
<div class="flex min-w-0 flex-col gap-1 md:hidden">
|
||||
<div class="flex min-w-0 items-center text-xs text-neutral-400">
|
||||
<a class="min-w-0 truncate text-neutral-300 hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ $isApplication
|
||||
? route('project.application.configuration', $routeParams)
|
||||
: ($isService
|
||||
? route('project.service.configuration', $routeParams)
|
||||
: route('project.database.configuration', $routeParams)) }}"
|
||||
title="{{ data_get($resource, 'name') }}{{ $serverName ? ' ('.$serverName.')' : '' }}">
|
||||
{{ data_get($resource, 'name') }}
|
||||
</a>
|
||||
</div>
|
||||
@if ($resource->getMorphClass() == 'App\Models\Service')
|
||||
<x-status.services :service="$resource" />
|
||||
@else
|
||||
<x-status.index :resource="$resource" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<ol class="hidden flex-wrap items-center gap-y-1 md:flex">
|
||||
<!-- Project Level -->
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false, closeTimeout: null, toggle() { this.projectOpen = !this.projectOpen }, open() { clearTimeout(this.closeTimeout); this.projectOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.projectOpen = false }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
|
||||
|
|
@ -204,27 +242,6 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
|
|||
</li>
|
||||
|
||||
<!-- Resource Level -->
|
||||
@php
|
||||
$resourceUuid = data_get($resource, 'uuid');
|
||||
$resourceType = $resource->getMorphClass();
|
||||
$isApplication = $resourceType === 'App\Models\Application';
|
||||
$isService = $resourceType === 'App\Models\Service';
|
||||
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
|
||||
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') &&
|
||||
($resource->relationLoaded('additional_servers') ? $resource->additional_servers->count() > 0 : ($resource->additional_servers_count ?? 0) > 0);
|
||||
$serverName = $hasMultipleServers ? null : data_get($resource, 'destination.server.name');
|
||||
$routeParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
];
|
||||
if ($isApplication) {
|
||||
$routeParams['application_uuid'] = $resourceUuid;
|
||||
} elseif ($isService) {
|
||||
$routeParams['service_uuid'] = $resourceUuid;
|
||||
} else {
|
||||
$routeParams['database_uuid'] = $resourceUuid;
|
||||
}
|
||||
@endphp
|
||||
<li class="inline-flex items-center mr-2">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ $isApplication
|
||||
|
|
|
|||
|
|
@ -3,35 +3,37 @@
|
|||
'lastDeploymentLink' => null,
|
||||
'resource' => null,
|
||||
])
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('degraded'))
|
||||
<x-status.degraded :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting'))
|
||||
<x-status.restarting :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@else
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited'))
|
||||
<div class="flex items-center pl-2">
|
||||
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
|
||||
({{ $resource->restart_count }}x restarts)
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('degraded'))
|
||||
<x-status.degraded :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting'))
|
||||
<x-status.restarting :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@else
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited'))
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
|
||||
({{ $resource->restart_count }}x restarts)
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,32 @@
|
|||
@php
|
||||
$displayStatus = formatContainerStatus($complexStatus);
|
||||
@endphp
|
||||
@if (str($displayStatus)->lower()->contains('running'))
|
||||
<x-status.running :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('starting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('restarting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('degraded'))
|
||||
<x-status.degraded :status="$displayStatus" />
|
||||
@else
|
||||
<x-status.stopped :status="$displayStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
@if (str($displayStatus)->lower()->contains('running'))
|
||||
<x-status.running :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('starting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('restarting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('degraded'))
|
||||
<x-status.degraded :status="$displayStatus" />
|
||||
@else
|
||||
<x-status.stopped :status="$displayStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<x-modal-input buttonTitle="Add" title="New Project">
|
||||
<x-slot:content>
|
||||
<button
|
||||
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
|
|
@ -81,7 +81,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<button
|
||||
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
@endif
|
||||
</div>
|
||||
@if (!isCloud())
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSave()" id="useInstanceEmailSettings"
|
||||
label="Use system wide (transactional) email settings" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
<h3 class="text-lg font-medium mb-3">Deployments</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentSuccessTelegramNotifications"
|
||||
label="Deployment Success" />
|
||||
</div>
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
id="telegramNotificationsDeploymentSuccessThreadId" />
|
||||
</div>
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentFailureTelegramNotifications"
|
||||
label="Deployment Failure" />
|
||||
</div>
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
id="telegramNotificationsDeploymentFailureThreadId" />
|
||||
</div>
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="statusChangeTelegramNotifications"
|
||||
label="Container Status Changes"
|
||||
helper="Send a notification when a container status changes. It will send a notification for Stopped and Restarted events of a container." />
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<h3 class="text-lg font-medium mb-3">Backups</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupSuccessTelegramNotifications"
|
||||
label="Backup Success" />
|
||||
</div>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupFailureTelegramNotifications"
|
||||
label="Backup Failure" />
|
||||
</div>
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
<h3 class="text-lg font-medium mb-3">Scheduled Tasks</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskSuccessTelegramNotifications"
|
||||
label="Scheduled Task Success" />
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskFailureTelegramNotifications"
|
||||
label="Scheduled Task Failure" />
|
||||
</div>
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
<h3 class="text-lg font-medium mb-3">Server</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupSuccessTelegramNotifications"
|
||||
label="Docker Cleanup Success" />
|
||||
</div>
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupFailureTelegramNotifications"
|
||||
label="Docker Cleanup Failure" />
|
||||
</div>
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverDiskUsageTelegramNotifications"
|
||||
label="Server Disk Usage" />
|
||||
</div>
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverReachableTelegramNotifications"
|
||||
label="Server Reachable" />
|
||||
</div>
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverUnreachableTelegramNotifications"
|
||||
label="Server Unreachable" />
|
||||
</div>
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchTelegramNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedTelegramNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<div class="flex h-[calc(100vh-10rem)] min-h-[50rem] flex-col overflow-hidden">
|
||||
<x-slot:title>
|
||||
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
|
||||
</x-slot>
|
||||
|
|
@ -165,13 +165,13 @@
|
|||
this.scheduleScroll();
|
||||
}
|
||||
}
|
||||
}">
|
||||
}" class="flex flex-1 min-h-0 flex-col overflow-hidden">
|
||||
<livewire:project.application.deployment-navbar
|
||||
:application_deployment_queue="$application_deployment_queue" />
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 relative'">
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 flex flex-1 min-h-0 flex-col overflow-hidden'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
|
||||
class="flex min-h-0 flex-col w-full overflow-hidden bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'flex-1 border border-dotted rounded-sm'">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -328,8 +328,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
|
|||
</div>
|
||||
</div>
|
||||
<div id="logsContainer"
|
||||
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
|
||||
class="flex min-h-40 flex-1 flex-col overflow-y-auto p-2 px-4 scrollbar">
|
||||
<div id="logs" class="flex flex-col font-logs">
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
|
|
@ -363,4 +362,4 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($baseDirectory . $dockerComposeLocation, '/') }}</span>"
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
|
||||
label="Preserve Repository During Deployment"
|
||||
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
||||
|
|
@ -382,7 +382,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
x-bind:disabled="!canUpdate" />
|
||||
|
||||
@if ($buildPack !== 'dockercompose')
|
||||
<div class="pt-2 w-96">
|
||||
<div class="pt-2 w-full sm:w-96">
|
||||
<x-forms.checkbox
|
||||
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
instantSave id="isBuildServerEnabled" label="Use a Build Server?"
|
||||
|
|
@ -422,7 +422,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="isContainerLabelEscapeEnabled" instantSave
|
||||
|
|
@ -521,7 +521,7 @@ class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blu
|
|||
|
||||
<h3 class="pt-8">HTTP Basic Authentication</h3>
|
||||
<div>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||
label="Enable" id="isHttpBasicAuthEnabled" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
|
|
@ -543,7 +543,7 @@ class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blu
|
|||
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"
|
||||
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="isContainerLabelReadonlyEnabled" instantSave
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.deployment.index', $parameters) }}">
|
||||
Deployments
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.logs', $parameters) }}">
|
||||
<div class="flex items-center gap-1">
|
||||
Logs
|
||||
|
|
@ -23,100 +24,160 @@
|
|||
</a>
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.command', $parameters) }}">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
<x-applications.links :application="$application" />
|
||||
<div class="shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
|
||||
<div>Please load a Compose file.</div>
|
||||
@else
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div>
|
||||
<x-applications.advanced :application="$application" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Redeploy
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Update Service
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='restart'>
|
||||
Restart
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Deploy
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
|
||||
@if ($application->status === 'running')
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='force_deploy_without_cache'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@else
|
||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div>
|
||||
<x-applications.advanced :application="$application" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,18 +10,18 @@
|
|||
</x-slide-over>
|
||||
<div class="navbar-main">
|
||||
<nav
|
||||
class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
|
||||
<a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.database.logs', $parameters) }}">
|
||||
Logs
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.database.command', $parameters) }}">
|
||||
Terminal
|
||||
</a>
|
||||
|
|
@ -31,7 +31,7 @@ class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:o
|
|||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
|
||||
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.backup.index', $parameters) }}">
|
||||
Backups
|
||||
</a>
|
||||
|
|
@ -39,57 +39,97 @@ class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:o
|
|||
</nav>
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:button-title>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch">
|
||||
Restart
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Start
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:button-title>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Start
|
||||
</button>
|
||||
@endif
|
||||
Start
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('startEvent', () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
@can('update', $resource)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's path for preview deployments (e.g. ./scripts becomes ./scripts-pr-1). Disable this for volumes that contain shared config or scripts from your repository."></x-forms.checkbox>
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
@if (!$fileStorage->is_directory)
|
||||
@can('update', $resource)
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
@endif
|
||||
@else
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
</div>
|
||||
@endcan
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,120 +9,198 @@
|
|||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.service.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.service.command', $parameters) }}">
|
||||
<button>Terminal</button>
|
||||
</a>
|
||||
@endcan
|
||||
<x-services.links :service="$service" />
|
||||
<div class="shrink-0">
|
||||
<x-services.links :service="$service" />
|
||||
</div>
|
||||
</nav>
|
||||
@if ($service->isDeployable)
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<div class="order-first flex flex-wrap items-center gap-2 sm:order-last">
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (str($service->status)->contains('running'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
Pull Latest Images & Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
|||
</div>
|
||||
</div>
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave="instantSaveExclude"
|
||||
label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
placeholder="My super WordPress site" />
|
||||
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork"
|
||||
label="Connect To Predefined Network"
|
||||
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
||||
|
|
@ -46,4 +46,4 @@ class="font-bold">{{ data_get($field, 'serviceName') }}</span>{{ data_get($field
|
|||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
@endif
|
||||
@if (!$isService)
|
||||
@can('update', $resource)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's name for preview deployments (e.g. myvolume becomes myvolume-pr-1). Disable this for volumes that should be shared between the main and preview deployments."></x-forms.checkbox>
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
@endif
|
||||
@if (!$isService)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's name for preview deployments (e.g. myvolume becomes myvolume-pr-1). Disable this for volumes that should be shared between the main and preview deployments."></x-forms.checkbox>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
</x-callout>
|
||||
@endif
|
||||
<h3>Advanced</h3>
|
||||
<div class="pb-6 w-96">
|
||||
<div class="pb-6 w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
|
||||
id="generateExactLabels"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
|
||||
label="Enable Sentinel" />
|
||||
@if ($server->isSentinelEnabled())
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ class="w-full input opacity-50 cursor-not-allowed"
|
|||
|
||||
<div class="w-full">
|
||||
@if (!$server->isLocalhost())
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
@if ($isBuildServerLocked)
|
||||
<x-forms.checkbox disabled instantSave id="isBuildServer"
|
||||
helper="You can't use this server as a build server because it has defined resources."
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ class=""
|
|||
<div>You need to register a GitHub App before using this source.</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2 pt-4 w-96">
|
||||
<div class="flex w-full flex-col gap-2 pt-4 sm:w-96">
|
||||
<x-forms.checkbox disabled id="default_permissions" label="Mandatory"
|
||||
helper="Contents: read<br>Metadata: read<br>Email: read" />
|
||||
<x-forms.checkbox id="preview_deployment_permissions" label="Preview Deployments "
|
||||
|
|
|
|||
71
tests/Feature/BreadcrumbsVisibilityTest.php
Normal file
71
tests/Feature/BreadcrumbsVisibilityTest.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
InstanceSettings::unguarded(function () {
|
||||
InstanceSettings::query()->create([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'name' => 'Pure Dockerfile Example',
|
||||
'status' => 'running',
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides the breadcrumb trail on mobile while keeping the current status visible', function () {
|
||||
$response = $this->get(route('project.application.configuration', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
]));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('flex min-w-0 flex-col gap-1 md:hidden', false);
|
||||
$response->assertSee('flex min-w-0 items-center text-xs text-neutral-400', false);
|
||||
$response->assertSee('hidden flex-wrap items-center gap-y-1 md:flex', false);
|
||||
$response->assertSee('flex flex-wrap items-center gap-1', false);
|
||||
$response->assertSee(
|
||||
'scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible',
|
||||
false,
|
||||
);
|
||||
$response->assertSee('shrink-0', false);
|
||||
$response->assertSee('Actions');
|
||||
$response->assertSee('dropdown-item-touch', false);
|
||||
$response->assertSee('hidden flex-wrap items-center gap-2 md:flex', false);
|
||||
$response->assertSee('window.innerWidth >= 768', false);
|
||||
$response->assertSee(':style="panelStyles"', false);
|
||||
$response->assertSee('absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6', false);
|
||||
$response->assertSee('Pure Dockerfile Example');
|
||||
$response->assertSee('Running');
|
||||
$response->assertSee('pt-2 pb-4 md:pb-10', false);
|
||||
|
||||
expect($response->getContent())->not->toContain('hidden pt-2 pb-10 md:flex');
|
||||
});
|
||||
79
tests/Feature/DeploymentLogsLayoutTest.php
Normal file
79
tests/Feature/DeploymentLogsLayoutTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
InstanceSettings::unguarded(function () {
|
||||
InstanceSettings::query()->create([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders deployment logs in a full-height layout', function () {
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => $this->application->id,
|
||||
'deployment_uuid' => 'deploy-layout-test',
|
||||
'server_id' => $this->server->id,
|
||||
'status' => ApplicationDeploymentStatus::FINISHED->value,
|
||||
'logs' => json_encode([
|
||||
[
|
||||
'command' => null,
|
||||
'output' => 'rolling update started',
|
||||
'type' => 'stdout',
|
||||
'timestamp' => now()->toISOString(),
|
||||
'hidden' => false,
|
||||
'batch' => 1,
|
||||
'order' => 1,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$response = $this->get(route('project.application.deployment.show', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
'deployment_uuid' => $deployment->deployment_uuid,
|
||||
]));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('rolling update started');
|
||||
$response->assertSee('flex h-[calc(100vh-10rem)] min-h-40 flex-col overflow-hidden', false);
|
||||
$response->assertSee('flex flex-1 min-h-0 flex-col overflow-hidden', false);
|
||||
$response->assertSee('mt-4 flex flex-1 min-h-0 flex-col overflow-hidden', false);
|
||||
$response->assertSee('flex min-h-0 flex-col w-full overflow-hidden bg-white', false);
|
||||
$response->assertSee('flex min-h-40 flex-1 flex-col overflow-y-auto p-2 px-4 scrollbar', false);
|
||||
|
||||
expect($response->getContent())->not->toContain('max-h-[30rem]');
|
||||
});
|
||||
113
tests/Feature/ResponsiveCheckboxLayoutTest.php
Normal file
113
tests/Feature/ResponsiveCheckboxLayoutTest.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(function () {
|
||||
InstanceSettings::query()->create([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->user = User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$this->team = Team::factory()->create([
|
||||
'show_boarding' => false,
|
||||
]);
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->privateKey = PrivateKey::create([
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'Test Key',
|
||||
'description' => 'Test SSH key',
|
||||
'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----',
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::TRAEFIK->value,
|
||||
'status' => ProxyStatus::EXITED->value,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->destination = StandaloneDocker::query()
|
||||
->where('server_id', $this->server->id)
|
||||
->firstOrFail();
|
||||
|
||||
$this->project = Project::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = Environment::factory()->create([
|
||||
'project_id' => $this->project->id,
|
||||
]);
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders responsive checkbox classes on the application configuration page', function () {
|
||||
$response = $this->get(route('project.application.configuration', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
]));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Use a Build Server?');
|
||||
$response->assertSee('form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2', false);
|
||||
$response->assertSee('label flex w-full max-w-full min-w-0 items-center gap-4 px-0', false);
|
||||
$response->assertSee('flex min-w-0 grow gap-2 break-words', false);
|
||||
$response->assertSee('shrink-0', false);
|
||||
$response->assertSee('pt-2 w-full sm:w-96', false);
|
||||
|
||||
expect($response->getContent())->not->toContain('min-w-fit');
|
||||
});
|
||||
|
||||
it('renders responsive checkbox classes on the server page', function () {
|
||||
$response = $this->get(route('server.show', [
|
||||
'server_uuid' => $this->server->uuid,
|
||||
]));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Use it as a build server?');
|
||||
$response->assertSee('form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2', false);
|
||||
$response->assertSee('label flex w-full max-w-full min-w-0 items-center gap-4 px-0', false);
|
||||
$response->assertSee('flex min-w-0 grow gap-2 break-words', false);
|
||||
$response->assertSee('shrink-0', false);
|
||||
$response->assertSee('w-full sm:w-96', false);
|
||||
|
||||
expect($response->getContent())->not->toContain('min-w-fit');
|
||||
});
|
||||
100
tests/Feature/UpgradeComponentTest.php
Normal file
100
tests/Feature/UpgradeComponentTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Upgrade;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('initializes latest version during mount from cached versions data', function () {
|
||||
config(['constants.coolify.version' => '4.0.0-beta.998']);
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
'new_version_available' => true,
|
||||
]);
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->assertSet('currentVersion', '4.0.0-beta.998')
|
||||
->assertSet('latestVersion', '4.0.0-beta.999')
|
||||
->assertSet('isUpgradeAvailable', true)
|
||||
->assertSee('4.0.0-beta.998')
|
||||
->assertSee('4.0.0-beta.999');
|
||||
});
|
||||
|
||||
it('falls back to 0.0.0 during mount when cached versions data is unavailable', function () {
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
'new_version_available' => false,
|
||||
]);
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
|
||||
->andReturn(null);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->assertSet('latestVersion', '0.0.0');
|
||||
});
|
||||
|
||||
it('clears stale upgrade availability when current version already matches latest version', function () {
|
||||
config(['constants.coolify.version' => '4.0.0-beta.999']);
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
'new_version_available' => true,
|
||||
]);
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->assertSet('latestVersion', '4.0.0-beta.999')
|
||||
->assertSet('isUpgradeAvailable', false);
|
||||
|
||||
expect(InstanceSettings::findOrFail(0)->new_version_available)->toBeFalse();
|
||||
});
|
||||
|
||||
it('clears stale upgrade availability when current version is newer than cached latest version', function () {
|
||||
config(['constants.coolify.version' => '4.0.0-beta.1000']);
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
'new_version_available' => true,
|
||||
]);
|
||||
|
||||
Cache::shouldReceive('remember')
|
||||
->once()
|
||||
->with('coolify:versions:all', 3600, Mockery::type(\Closure::class))
|
||||
->andReturn([
|
||||
'coolify' => [
|
||||
'v4' => [
|
||||
'version' => '4.0.0-beta.999',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::test(Upgrade::class)
|
||||
->assertSet('latestVersion', '4.0.0-beta.999')
|
||||
->assertSet('isUpgradeAvailable', false);
|
||||
|
||||
expect(InstanceSettings::findOrFail(0)->new_version_available)->toBeFalse();
|
||||
});
|
||||
218
tests/Feature/UserDeletionWithGithubAppTest.php
Normal file
218
tests/Feature/UserDeletionWithGithubAppTest.php
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Create root team and admin user (instance admin)
|
||||
$this->rootTeam = Team::factory()->create(['id' => 0, 'name' => 'Root Team']);
|
||||
$this->adminUser = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($this->adminUser->id, ['role' => 'owner']);
|
||||
$this->actingAs($this->adminUser);
|
||||
session(['currentTeam' => $this->rootTeam]);
|
||||
});
|
||||
|
||||
it('deletes a user whose team has a github app with applications', function () {
|
||||
// Create the user to be deleted with their own team
|
||||
$targetUser = User::factory()->create();
|
||||
$targetTeam = $targetUser->teams()->first(); // created by User::created event
|
||||
|
||||
// Create a private key for the team
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]);
|
||||
|
||||
// Create a server and destination for the team
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $targetTeam->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
]);
|
||||
$destination = StandaloneDocker::factory()->create(['server_id' => $server->id]);
|
||||
|
||||
// Create a project and environment
|
||||
$project = Project::factory()->create(['team_id' => $targetTeam->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
// Create a GitHub App owned by the target team
|
||||
$githubApp = GithubApp::create([
|
||||
'name' => 'Test GitHub App',
|
||||
'team_id' => $targetTeam->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
// Create an application that uses the GitHub App as its source
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => $githubApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
// Delete the user — this should NOT throw a GithubApp exception
|
||||
$targetUser->delete();
|
||||
|
||||
// Assert user is deleted
|
||||
expect(User::find($targetUser->id))->toBeNull();
|
||||
|
||||
// Assert the GitHub App is deleted
|
||||
expect(GithubApp::find($githubApp->id))->toBeNull();
|
||||
|
||||
// Assert the application is deleted
|
||||
expect(Application::find($application->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('does not delete system-wide github apps when deleting a different team', function () {
|
||||
// Create a system-wide GitHub App owned by the root team
|
||||
$rootPrivateKey = PrivateKey::factory()->create(['team_id' => $this->rootTeam->id]);
|
||||
$systemGithubApp = GithubApp::create([
|
||||
'name' => 'System GitHub App',
|
||||
'team_id' => $this->rootTeam->id,
|
||||
'private_key_id' => $rootPrivateKey->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'is_system_wide' => true,
|
||||
]);
|
||||
|
||||
// Create a target user with their own team
|
||||
$targetUser = User::factory()->create();
|
||||
$targetTeam = $targetUser->teams()->first();
|
||||
|
||||
// Create an application on the target team that uses the system-wide GitHub App
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]);
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $targetTeam->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
]);
|
||||
$destination = StandaloneDocker::factory()->create(['server_id' => $server->id]);
|
||||
$project = Project::factory()->create(['team_id' => $targetTeam->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => $systemGithubApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
// Delete the target user — should NOT throw or delete the system-wide GitHub App
|
||||
$targetUser->delete();
|
||||
|
||||
// Assert user is deleted
|
||||
expect(User::find($targetUser->id))->toBeNull();
|
||||
|
||||
// Assert the system-wide GitHub App still exists
|
||||
expect(GithubApp::find($systemGithubApp->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('transfers instance-wide github app to root team when owning user is deleted', function () {
|
||||
// Create a user whose team owns an instance-wide GitHub App
|
||||
$targetUser = User::factory()->create();
|
||||
$targetTeam = $targetUser->teams()->first();
|
||||
|
||||
$targetPrivateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]);
|
||||
$instanceWideApp = GithubApp::create([
|
||||
'name' => 'Instance-Wide GitHub App',
|
||||
'team_id' => $targetTeam->id,
|
||||
'private_key_id' => $targetPrivateKey->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'is_system_wide' => true,
|
||||
]);
|
||||
|
||||
// Create an application on the ROOT team that uses this instance-wide GitHub App
|
||||
$rootPrivateKey = PrivateKey::factory()->create(['team_id' => $this->rootTeam->id]);
|
||||
$rootServer = Server::factory()->create([
|
||||
'team_id' => $this->rootTeam->id,
|
||||
'private_key_id' => $rootPrivateKey->id,
|
||||
]);
|
||||
$rootDestination = StandaloneDocker::factory()->create(['server_id' => $rootServer->id]);
|
||||
$rootProject = Project::factory()->create(['team_id' => $this->rootTeam->id]);
|
||||
$rootEnvironment = Environment::factory()->create(['project_id' => $rootProject->id]);
|
||||
|
||||
$otherTeamApp = Application::factory()->create([
|
||||
'environment_id' => $rootEnvironment->id,
|
||||
'destination_id' => $rootDestination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => $instanceWideApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
// Delete the user — should succeed and transfer the instance-wide app to root team
|
||||
$targetUser->delete();
|
||||
|
||||
// Assert user is deleted
|
||||
expect(User::find($targetUser->id))->toBeNull();
|
||||
|
||||
// Assert the instance-wide GitHub App is preserved and transferred to root team
|
||||
$instanceWideApp->refresh();
|
||||
expect($instanceWideApp)->not->toBeNull();
|
||||
expect($instanceWideApp->team_id)->toBe($this->rootTeam->id);
|
||||
|
||||
// Assert the other team's application still has its source intact
|
||||
$otherTeamApp->refresh();
|
||||
expect($otherTeamApp->source_id)->toBe($instanceWideApp->id);
|
||||
expect($otherTeamApp->source_type)->toBe(GithubApp::class);
|
||||
});
|
||||
|
||||
it('transfers instance-wide github app to root team when team is deleted directly', function () {
|
||||
// Create a team that owns an instance-wide GitHub App
|
||||
$targetUser = User::factory()->create();
|
||||
$targetTeam = $targetUser->teams()->first();
|
||||
|
||||
$targetPrivateKey = PrivateKey::factory()->create(['team_id' => $targetTeam->id]);
|
||||
$instanceWideApp = GithubApp::create([
|
||||
'name' => 'Instance-Wide GitHub App',
|
||||
'team_id' => $targetTeam->id,
|
||||
'private_key_id' => $targetPrivateKey->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'is_system_wide' => true,
|
||||
]);
|
||||
|
||||
// Create an application on the ROOT team that uses this instance-wide GitHub App
|
||||
$rootPrivateKey = PrivateKey::factory()->create(['team_id' => $this->rootTeam->id]);
|
||||
$rootServer = Server::factory()->create([
|
||||
'team_id' => $this->rootTeam->id,
|
||||
'private_key_id' => $rootPrivateKey->id,
|
||||
]);
|
||||
$rootDestination = StandaloneDocker::factory()->create(['server_id' => $rootServer->id]);
|
||||
$rootProject = Project::factory()->create(['team_id' => $this->rootTeam->id]);
|
||||
$rootEnvironment = Environment::factory()->create(['project_id' => $rootProject->id]);
|
||||
|
||||
$otherTeamApp = Application::factory()->create([
|
||||
'environment_id' => $rootEnvironment->id,
|
||||
'destination_id' => $rootDestination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => $instanceWideApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
// Delete the team directly — should transfer instance-wide app to root team
|
||||
$targetTeam->delete();
|
||||
|
||||
// Assert the instance-wide GitHub App is preserved and transferred to root team
|
||||
$instanceWideApp->refresh();
|
||||
expect($instanceWideApp)->not->toBeNull();
|
||||
expect($instanceWideApp->team_id)->toBe($this->rootTeam->id);
|
||||
|
||||
// Assert the other team's application still has its source intact
|
||||
$otherTeamApp->refresh();
|
||||
expect($otherTeamApp->source_id)->toBe($instanceWideApp->id);
|
||||
expect($otherTeamApp->source_type)->toBe(GithubApp::class);
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.472"
|
||||
"version": "4.0.0-beta.473"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"version": "1.0.13"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "1.0.12"
|
||||
"version": "1.0.13"
|
||||
},
|
||||
"sentinel": {
|
||||
"version": "0.0.21"
|
||||
|
|
|
|||
Loading…
Reference in a new issue