Compare commits

..

24 commits

Author SHA1 Message Date
rosslh
6fda0ab8f6 style(branding): remap neutral/gray to stone and fix chart theming
All checks were successful
Build MapleDeploy Coolify Image / build (push) Successful in 1m14s
Remap neutral and gray scales to stone equivalents in @theme block for
warm tone consistency across all three apps. Interpolate coolgray-200 to
avoid invisible dark-mode elements. Add gridColor/textColor to ApexCharts
initial options so axis labels render on first load. Update warning color
and fix add-tag opacity.
2026-04-11 15:31:53 -04:00
rosslh
521d0fe278 style(branding): replace gray-50 with stone-50 and darken base to stone-800 2026-04-11 15:31:53 -04:00
rosslh
4788e72ca2 fix(sentinel): pull sentinel image from ghcr.io and fall back to latest tag 2026-04-11 15:31:53 -04:00
rosslh
40db4966d5 feat(branding): disable FQDN and IP fields instead of hiding/leaving editable 2026-04-11 15:31:53 -04:00
rosslh
e4e10a11b9 fix(versioning): move version bumping to CI build time
Version was never bumped because git rebase doesn't run pre-commit
hooks. CI now appends a YYYYMMDDHHmm timestamp to the upstream base
version and injects it into the Docker image via build arg.
2026-04-11 15:31:53 -04:00
rosslh
48cdf91d9b feat(branding): yellow scrollbar and hide FQDN settings field
- Change scrollbar thumb from Coolify red to MapleDeploy yellow
- Hide FQDN field from settings UI (managed during provisioning)
2026-04-11 15:31:53 -04:00
rosslh
7ca7bbd204 feat(auth): notify MapleDeploy on first-user registration
After the first user registers, clears the setup token and dispatches
NotifySetupCompleteJob to POST the token to MapleDeploy's callback URL.
Adds setup_callback_url column to instance_settings.
2026-04-11 15:31:53 -04:00
rosslh
4f0c914437 feat(auth): gate first-user registration with setup token
Require a setup_token query parameter to access the registration page
when no users exist, preventing admin hijacking on new instances.
2026-04-11 15:31:53 -04:00
rosslh
a3fa47197d fix(dns): replace Cloudflare defaults with CIRA Canadian Shield
Replace hardcoded 1.1.1.1/1.0.0.1 (Cloudflare) DNS defaults with
CIRA Canadian Shield (149.112.121.10, 149.112.122.10) to maintain
Canadian data sovereignty for DNS resolution.
2026-04-11 15:31:53 -04:00
rosslh
66dfd140e9 chore(branding): remove sponsorship popup remnants 2026-04-11 15:31:53 -04:00
rosslh
35ab01e65c style(branding): replace Coolify purple and yellow with MapleDeploy palette
Replace #6b16ed (Coolify purple) with #d52b1e (MapleDeploy red) and
form focus states, dirty indicators, chart colors, and theme tokens.
Also fix warning color scale to use standard Tailwind yellow values.
2026-04-11 15:31:53 -04:00
rosslh
3593df1268 feat(onboarding): skip boarding wizard for new users
Customers land directly on the dashboard instead of the setup wizard,
which shows a misleading localhost vs remote server warning that
conflicts with MapleDeploy's single-VM model.
2026-04-11 15:31:53 -04:00
rosslh
048371a8fc style(branding): add header logo gap and use stone-900 background
Add 4px gap between logo and header elements with flex-shrink support.
Change base background from near-black (#0c0a09) to stone-900 (#1c1917)
to match the marketing site's darkest dark-mode color.
2026-04-11 15:31:53 -04:00
rosslh
339880c86c fix(branding): preserve sidebar logo aspect ratio when constrained 2026-04-11 15:31:53 -04:00
rosslh
8146e71590 fix(branding): replace "self-hosted" subtitle on dashboard 2026-04-11 15:31:53 -04:00
rosslh
1dec7e1ed9 fix(branding): constrain sidebar logo to prevent overflow
Replace max-w-none with max-w-full so the logo respects the sidebar
container width instead of overflowing it.
2026-04-11 15:31:53 -04:00
rosslh
6bbba1d4d3 fix(branding): preserve sidebar logo aspect ratio, bump to 463.2
Add max-w-none to logo images to prevent Tailwind base styles from
constraining the image width within the narrow sidebar flex container.
2026-04-11 15:31:53 -04:00
rosslh
f1608afe89 chore: remove broken feedback form and all entrypoints
The feedback form relies on instance SMTP being configured, which
MapleDeploy customers won't do. Without SMTP it silently logs to
the server and shows a misleading success message. Removed the
navbar button, onboarding modal, subscription page link, API
endpoint, and all associated dead code.
2026-04-11 15:31:53 -04:00
rosslh
6a9674f6d9 fix(ci): recreate CDN storage zone in EU (DE) region
Storage zone was accidentally created in NY. Recreated as
'coolify-update' in DE (Falkenstein) to keep update artifacts
in EU jurisdiction, consistent with Bunny.net data sovereignty
justification. Updated storage zone name, pull zone ID, and
reverted storage endpoint to default (storage.bunnycdn.com).
2026-04-11 15:31:53 -04:00
rosslh
1bceb95673 fix(ci): use NY region storage endpoint for Bunny CDN uploads 2026-04-11 15:31:53 -04:00
rosslh
95e0ffb361 fix(ci): use apk instead of apt-get for curl install 2026-04-11 15:31:53 -04:00
rosslh
77a58efc27 fix(ci): install curl in runner for CDN uploads 2026-04-11 15:31:53 -04:00
rosslh
39eeb2b19f feat: add update pipeline with MapleDeploy CDN and versioning
Route all Coolify update artifacts (versions.json, upgrade.sh,
compose files) through updates.mapledeploy.ca instead of upstream
cdn.coollabs.io. Extend CI to publish artifacts to Bunny CDN
storage zone and purge cache on each build.

- Point CDN_URL, versions_url, upgrade_script_url to updates.mapledeploy.ca
- Hardcode helper/realtime images to ghcr.io (not mirrored to Forgejo)
- Pass registry_url as 3rd arg to upgrade.sh for main image pulls
- Adopt versioning scheme 4.0.0-beta.X.N (bump to 4.0.0-beta.463.1)
- Add CI steps: generate versions.json, upload to Bunny, purge cache
2026-04-11 15:31:53 -04:00
rosslh
21b7754a03 feat(branding): apply MapleDeploy branding to Coolify
Replace Coolify branding with MapleDeploy throughout the UI: logos,
favicon, fonts (Overlock 900), color scheme, help links, and page
titles. Remove GitHub Actions workflows and add Forgejo CI build
workflow. Strip cloud-only features (subscription prompts, sponsor
links, server creation cloud options).
2026-04-11 15:31:27 -04:00
43 changed files with 437 additions and 1305 deletions

View file

@ -23,42 +23,24 @@ class Upgrade extends Component
public function mount()
{
$this->refreshUpgradeState();
$this->currentVersion = config('constants.coolify.version');
$this->devMode = isDev();
}
public function checkUpdate()
{
try {
$this->refreshUpgradeState();
$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;
}
} 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 {

View file

@ -71,31 +71,25 @@ protected static function booted()
}
});
static::deleting(function (Team $team) {
foreach ($team->privateKeys as $key) {
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
$key->delete();
}
// 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) {
$sources = $team->sources();
foreach ($sources as $source) {
$source->delete();
}
foreach (Tag::whereTeamId($team->id)->get() as $tag) {
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
$tag->delete();
}
foreach ($team->environment_variables()->get() as $sharedVariable) {
$sharedVariable->delete();
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
$shared_variable->delete();
}
foreach ($team->s3s as $s3) {
$s3s = $team->s3s;
foreach ($s3s as $s3) {
$s3->delete();
}
});
@ -344,5 +338,4 @@ public function webhookNotificationSettings()
{
return $this->hasOne(WebhookNotificationSettings::class);
}
}

View file

@ -203,24 +203,10 @@ public static function volumeNameMessages(string $field = 'name'): array
}
/**
* 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
* 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)
*/
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';
public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/';
/**
* Get validation rules for container name fields
@ -244,7 +230,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 with optional IP and protocol (e.g. 3000:3000, 8080:80/udp, 127.0.0.1:8080:80, [::1]::80).',
"{$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).',
];
}

14
composer.lock generated
View file

@ -72,6 +72,7 @@
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67b6b6210af47319c74c5666388d71bc1bc58276",
"reference": "67b6b6210af47319c74c5666388d71bc1bc58276",
"shasum": ""
},
"require": {
@ -156,6 +157,7 @@
"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",
@ -5156,16 +5158,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.51",
"version": "3.0.50",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"shasum": ""
},
"require": {
@ -5246,7 +5248,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
},
"funding": [
{
@ -5262,7 +5264,7 @@
"type": "tidelift"
}
],
"time": "2026-04-10T01:33:53+00:00"
"time": "2026-03-19T02:57:58+00:00"
},
{
"name": "phpstan/phpdoc-parser",

View file

@ -3,9 +3,9 @@
return [
// MapleDeploy branding: registry pointed to Forgejo, auto-update disabled by default
'coolify' => [
'version' => '4.0.0-beta.473',
'version' => '4.0.0-beta.472',
'helper_version' => '1.0.13',
'realtime_version' => '1.0.13',
'realtime_version' => '1.0.12',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE', false),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View file

@ -60,7 +60,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13' # MapleDeploy branding: pinned to ghcr.io, not mirrored
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12' # MapleDeploy branding: pinned to ghcr.io, not mirrored
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"

View file

@ -96,7 +96,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
pull_policy: always
container_name: coolify-realtime
restart: always

View file

@ -7,7 +7,7 @@
"dependencies": {
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
"axios": "1.15.0",
"axios": "1.13.6",
"cookie": "1.1.1",
"dotenv": "17.3.1",
"node-pty": "1.1.0",
@ -36,14 +36,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
@ -344,13 +344,10 @@
}
},
"node_modules/proxy-from-env": {
"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"
}
"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"
},
"node_modules/ws": {
"version": "8.19.0",

View file

@ -5,7 +5,7 @@
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
"cookie": "1.1.1",
"axios": "1.15.0",
"axios": "1.13.6",
"dotenv": "17.3.1",
"node-pty": "1.1.0",
"ws": "8.19.0"

View file

@ -60,7 +60,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.13'
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12'
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"

View file

@ -96,7 +96,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
pull_policy: always
container_name: coolify-realtime
restart: always

View file

@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.473"
"version": "4.0.0-beta.472"
},
"nightly": {
"version": "4.0.0"
@ -10,7 +10,7 @@
"version": "1.0.13"
},
"realtime": {
"version": "1.0.13"
"version": "1.0.12"
},
"sentinel": {
"version": "0.0.21"

25
package-lock.json generated
View file

@ -16,7 +16,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
"@vitejs/plugin-vue": "6.0.3",
"axios": "1.15.0",
"axios": "1.13.2",
"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.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
@ -2501,14 +2501,11 @@
}
},
"node_modules/proxy-from-env": {
"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==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
"license": "MIT"
},
"node_modules/pusher-js": {
"version": "8.4.0",

View file

@ -9,7 +9,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
"@vitejs/plugin-vue": "6.0.3",
"axios": "1.15.0",
"axios": "1.13.2",
"laravel-echo": "2.2.7",
"laravel-vite-plugin": "2.0.1",
"postcss": "8.5.6",

View file

@ -145,10 +145,6 @@ @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;
}

View file

@ -1,44 +1,7 @@
<div x-data="{
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()"
dropdownOpen: false
}" class="relative" @click.outside="dropdownOpen = false">
<button @click="dropdownOpen=true"
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 }}
@ -50,11 +13,11 @@ class="inline-flex items-center justify-start pr-8 transition-colors focus:outli
</svg>
</button>
<div x-ref="panel" x-show="dropdownOpen" @click.away="close()" x-transition:enter="ease-out duration-200"
<div x-show="dropdownOpen" @click.away="dropdownOpen=false" x-transition:enter="ease-out duration-200"
x-transition:enter-start="-translate-y-2" x-transition:enter-end="translate-y-0"
: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>
class="absolute top-0 z-50 mt-6 min-w-max" x-cloak>
<div
class="border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200">
class="p-1 mt-1 bg-white border rounded-sm shadow-sm dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
{{ $slot }}
</div>
</div>

View file

@ -11,12 +11,12 @@
])
<div @class([
'form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2',
'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit',
'w-full' => $fullWidth,
'dark:hover:bg-coolgray-100 cursor-pointer' => !$disabled,
])>
<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">
<label @class(['flex gap-4 items-center px-0 min-w-fit label w-full'])>
<span class="flex grow gap-2">
@if ($label)
@if ($disabled)
<span class="opacity-60">{!! $label !!}</span>
@ -29,16 +29,16 @@
@endif
</span>
@if ($instantSave)
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
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->class([$defaultClass, 'shrink-0']) }}
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif />
@else
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
@endif
@endif

View file

@ -129,11 +129,7 @@
}"
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if (isset($trigger))
<div @click="modalOpen=true">
{{ $trigger }}
</div>
@elseif ($customButton)
@if ($customButton)
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="w-full">
{{ $customButton }}

View file

@ -29,47 +29,9 @@
$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="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">
<nav class="flex pt-2 pb-10">
<ol class="flex flex-wrap items-center gap-y-1">
<!-- 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()">
@ -242,6 +204,27 @@ 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

View file

@ -3,37 +3,35 @@
'lastDeploymentLink' => null,
'resource' => null,
])
<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>
@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

View file

@ -1,32 +1,30 @@
@php
$displayStatus = formatContainerStatus($complexStatus);
@endphp
<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>
@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

View file

@ -16,7 +16,7 @@
<x-modal-input buttonTitle="Add" title="New Project">
<x-slot:content>
<button
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">
class="flex items-center justify-center size-4 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" />
@ -82,7 +82,7 @@ class="flex items-center justify-center size-4 text-black dark:text-white rounde
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content>
<button
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">
class="flex items-center justify-center size-4 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" />

View file

@ -30,7 +30,7 @@
@endif
</div>
@if (!isCloud())
<div class="w-full sm:w-96">
<div class="w-96">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSave()" id="useInstanceEmailSettings"
label="Use system wide (transactional) email settings" />
</div>

View file

@ -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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="w-96">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedTelegramNotifications"
label="Traefik Proxy Outdated" />
</div>

View file

@ -1,4 +1,4 @@
<div class="flex h-[calc(100vh-10rem)] min-h-[50rem] flex-col overflow-hidden">
<div>
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | MapleDeploy
</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 flex flex-1 min-h-0 flex-col overflow-hidden'">
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 relative'">
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
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'">
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'">
<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,7 +328,8 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
</div>
</div>
<div id="logsContainer"
class="flex min-h-40 flex-1 flex-col overflow-y-auto p-2 px-4 scrollbar">
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
<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">
@ -362,4 +363,4 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
</div>
</div>
</div>
</div>
</div>

View file

@ -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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="pt-2 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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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

View file

@ -1,17 +1,16 @@
<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="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() }}
<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() }}
href="{{ route('project.application.configuration', $parameters) }}">
Configuration
</a>
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', $parameters) }}">
Deployments
</a>
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<div class="flex items-center gap-1">
Logs
@ -24,160 +23,100 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
</a>
@if (!$application->destination->server->isSwarm())
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
Terminal
</a>
@endcan
@endif
<div class="shrink-0">
<x-applications.links :application="$application" />
</div>
<x-applications.links :application="$application" />
</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
<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->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())
<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
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
Force deploy (without cache)
</div>
@endif
@endif
</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"
<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 d="M7 4v16l13 -8z" />
<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>
Deploy
Redeploy
</x-forms.button>
@endif
</div>
@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>
@endif
</div>

View file

@ -10,18 +10,18 @@
</x-slide-over>
<div class="navbar-main">
<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.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
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() }}
href="{{ route('project.database.configuration', $parameters) }}">
Configuration
</a>
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
Logs
</a>
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
Terminal
</a>
@ -31,7 +31,7 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="shrink-0 {{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.backup.index', $parameters) }}">
Backups
</a>
@ -39,97 +39,57 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
</nav>
@if ($database->destination->server->isFunctional())
<div class="flex flex-wrap gap-2 items-center">
<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 d="M7 4v16l13 -8z" />
@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>
Start
</button>
@endif
</div>
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 d="M7 4v16l13 -8z" />
</svg>
Start
</button>
@endif
@script
<script>
$wire.$on('startEvent', () => {

View file

@ -17,7 +17,7 @@
</div>
@if ($resource instanceof \App\Models\Application)
@can('update', $resource)
<div class="w-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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-full sm:w-96">
<div class="w-96">
<x-forms.checkbox disabled label="Is this based on the Git repository?"
id="isBasedOnGit"></x-forms.checkbox>
</div>

View file

@ -9,198 +9,120 @@
<h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data">
<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() }}
<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() }}
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
</a>
<a class="shrink-0 {{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.logs', $parameters) }}">
<button>Logs</button>
</a>
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
<a class="{{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.command', $parameters) }}">
<button>Terminal</button>
</a>
@endcan
<div class="shrink-0">
<x-services.links :service="$service" />
</div>
<x-services.links :service="$service" />
</nav>
@if ($service->isDeployable)
<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>
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>
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"
<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 d="M7 4v16l13 -8z" />
<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>
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"
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 d="M7 4v16l13 -8z" />
<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>
Deploy
</button>
@endif
</div>
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"
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>
@else
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">

View file

@ -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-full sm:w-96">
<div class="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."

View file

@ -20,7 +20,7 @@
placeholder="My super WordPress site" />
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
</div>
<div class="w-full sm:w-96">
<div class="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>

View file

@ -40,7 +40,7 @@
@endif
@if (!$isService)
@can('update', $resource)
<div class="w-full sm:w-96">
<div class="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-full sm:w-96">
<div class="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>

View file

@ -31,7 +31,7 @@
</x-callout>
@endif
<h3>Advanced</h3>
<div class="pb-6 w-full sm:w-96">
<div class="pb-6 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"

View file

@ -44,7 +44,7 @@
@endif
</div>
<div class="flex flex-col gap-2">
<div class="w-full sm:w-96">
<div class="w-96">
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
label="Enable Sentinel" />
@if ($server->isSentinelEnabled())

View file

@ -274,7 +274,7 @@ class="w-full input opacity-50 cursor-not-allowed"
<div class="w-full">
@if (!$server->isLocalhost())
<div class="w-full sm:w-96">
<div class="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."

View file

@ -271,7 +271,7 @@ class=""
<div>You need to register a GitHub App before using this source.</div>
@endif
<div class="flex w-full flex-col gap-2 pt-4 sm:w-96">
<div class="flex flex-col gap-2 pt-4 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 "

View file

@ -1,71 +0,0 @@
<?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');
});

View file

@ -1,79 +0,0 @@
<?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]');
});

View file

@ -1,113 +0,0 @@
<?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');
});

View file

@ -1,100 +0,0 @@
<?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();
});

View file

@ -1,218 +0,0 @@
<?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);
});

View file

@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.473"
"version": "4.0.0-beta.472"
},
"nightly": {
"version": "4.0.0"
@ -10,7 +10,7 @@
"version": "1.0.13"
},
"realtime": {
"version": "1.0.13"
"version": "1.0.12"
},
"sentinel": {
"version": "0.0.21"