mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 12:14:28 +00:00
Merge remote-tracking branch 'origin/next' into jean/port-exposes-improvement
This commit is contained in:
commit
07f381b88c
766 files changed
+43286
-12686
No files matched your search
+12
-2
@@ -14,7 +14,10 @@
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-mono: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
--font-geist-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
|
||||
--color-base: #101010;
|
||||
--color-warning: #fcd452;
|
||||
@@ -50,6 +53,13 @@
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.terminal-mobile-key {
|
||||
@apply min-h-10 rounded-md border border-white/10 bg-white/10 px-2 py-2 text-sm font-semibold text-white shadow-inner active:bg-white/25;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
*,
|
||||
@@ -96,7 +106,7 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden;
|
||||
@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;
|
||||
}
|
||||
|
||||
.coolify-monaco-editor {
|
||||
|
||||
@@ -70,3 +70,18 @@
|
||||
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-mono-variable.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Sans';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-sans-variable.woff2') format('woff2');
|
||||
}
|
||||
@@ -145,6 +145,10 @@
|
||||
@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;
|
||||
}
|
||||
@@ -177,7 +181,7 @@
|
||||
@apply flex gap-3 items-center px-2 py-1 w-full text-sm dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 rounded-sm truncate min-w-0;
|
||||
}
|
||||
@utility menu-item-icon {
|
||||
@apply flex-shrink-0 w-6 h-6 dark:hover:text-white;
|
||||
@apply shrink-0 size-4 dark:hover:text-white;
|
||||
}
|
||||
|
||||
@utility menu-item-label {
|
||||
@@ -197,7 +201,7 @@
|
||||
}
|
||||
|
||||
@utility sub-menu-item-icon {
|
||||
@apply flex-shrink-0 w-4 h-4 dark:hover:text-white;
|
||||
@apply shrink-0 size-4 dark:hover:text-white;
|
||||
}
|
||||
|
||||
@utility heading-item-active {
|
||||
@@ -339,3 +343,16 @@
|
||||
@utility log-info {
|
||||
@apply bg-blue-500/10 dark:bg-blue-500/15;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.sidebar-collapsed .menu-item {
|
||||
justify-content: center;
|
||||
width: var(--button-h, 2rem);
|
||||
height: var(--button-h, 2rem);
|
||||
min-height: var(--button-h, 2rem);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
gap: 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,13 @@
|
||||
import { initializeTerminalComponent } from './terminal.js';
|
||||
|
||||
// Livewire 3.5.19+ re-applies `x-cloak` to morphed elements during wire:navigate
|
||||
// (via replaceHtmlAttributes). With `[x-cloak]{display:none}` on the app wrapper,
|
||||
// this blanks the whole page on every navigation until Alpine re-processes it.
|
||||
// Strip leftover x-cloak after each navigation; the initial-load FOUC guard stays.
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
document.querySelectorAll('[x-cloak]').forEach((el) => el.removeAttribute('x-cloak'));
|
||||
});
|
||||
|
||||
['livewire:navigated', 'alpine:init'].forEach((event) => {
|
||||
document.addEventListener(event, () => {
|
||||
// tree-shaking
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const MAX_TERMINAL_SESSION_SECONDS = 8 * 60 * 60;
|
||||
export const TERMINAL_SESSION_WARNING_SECONDS = 30 * 60;
|
||||
export const TERMINAL_SESSION_DANGER_SECONDS = 5 * 60;
|
||||
|
||||
export function formatTerminalSessionRemainingTime(seconds) {
|
||||
const remainingSeconds = Math.max(0, Math.ceil(seconds));
|
||||
|
||||
if (remainingSeconds === 0) {
|
||||
return 'expired';
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(remainingSeconds / 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const secondsPart = remainingSeconds % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes}m ${String(secondsPart).padStart(2, '0')}s`;
|
||||
}
|
||||
|
||||
return `${hours}h ${String(minutes).padStart(2, '0')}m ${String(secondsPart).padStart(2, '0')}s`;
|
||||
}
|
||||
+208
-25
@@ -1,5 +1,11 @@
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import {
|
||||
MAX_TERMINAL_SESSION_SECONDS,
|
||||
TERMINAL_SESSION_DANGER_SECONDS,
|
||||
TERMINAL_SESSION_WARNING_SECONDS,
|
||||
formatTerminalSessionRemainingTime,
|
||||
} from './terminal-session-timer.js';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
|
||||
const terminalDebugEnabled = import.meta.env.DEV;
|
||||
@@ -42,12 +48,20 @@ export function initializeTerminalComponent() {
|
||||
maxHeartbeatMisses: 3,
|
||||
// Command buffering for race condition prevention
|
||||
pendingCommand: null,
|
||||
// Last successfully sent SSH command — replayed after a transient reconnect
|
||||
// so the PTY respawns automatically. Cleared on intentional terminations
|
||||
// (pty-exited, unprocessable).
|
||||
lastSentCommand: null,
|
||||
// Resize handling
|
||||
resizeObserver: null,
|
||||
resizeTimeout: null,
|
||||
// Visibility handling - prevent disconnects when tab loses focus
|
||||
isDocumentVisible: true,
|
||||
wasConnectedBeforeHidden: false,
|
||||
mobileToolbarCollapsed: false,
|
||||
terminalSessionStartedAt: null,
|
||||
terminalSessionRemainingSeconds: null,
|
||||
terminalSessionCountdownInterval: null,
|
||||
|
||||
init() {
|
||||
this.setupTerminal();
|
||||
@@ -75,8 +89,6 @@ export function initializeTerminalComponent() {
|
||||
focusWhenReady();
|
||||
});
|
||||
|
||||
this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000);
|
||||
|
||||
this.$watch('terminalActive', (active) => {
|
||||
if (!active && this.keepAliveInterval) {
|
||||
clearInterval(this.keepAliveInterval);
|
||||
@@ -133,6 +145,7 @@ export function initializeTerminalComponent() {
|
||||
this.clearAllTimers();
|
||||
this.connectionState = 'disconnected';
|
||||
this.pendingCommand = null;
|
||||
this.resetTerminalSessionCountdown();
|
||||
if (this.socket) {
|
||||
this.socket.close(1000, 'Client cleanup');
|
||||
}
|
||||
@@ -150,24 +163,93 @@ export function initializeTerminalComponent() {
|
||||
},
|
||||
|
||||
clearAllTimers() {
|
||||
[this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId, this.resizeTimeout]
|
||||
.forEach(timer => timer && clearInterval(timer));
|
||||
if (this.keepAliveInterval) {
|
||||
clearInterval(this.keepAliveInterval);
|
||||
}
|
||||
[this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId, this.resizeTimeout]
|
||||
.forEach(timer => timer && clearTimeout(timer));
|
||||
if (this.terminalSessionCountdownInterval) {
|
||||
clearInterval(this.terminalSessionCountdownInterval);
|
||||
}
|
||||
this.keepAliveInterval = null;
|
||||
this.reconnectInterval = null;
|
||||
this.connectionTimeoutId = null;
|
||||
this.pingTimeoutId = null;
|
||||
this.resizeTimeout = null;
|
||||
this.terminalSessionCountdownInterval = null;
|
||||
},
|
||||
|
||||
resetTerminalSessionCountdown() {
|
||||
if (this.terminalSessionCountdownInterval) {
|
||||
clearInterval(this.terminalSessionCountdownInterval);
|
||||
}
|
||||
|
||||
this.terminalSessionStartedAt = null;
|
||||
this.terminalSessionRemainingSeconds = null;
|
||||
this.terminalSessionCountdownInterval = null;
|
||||
},
|
||||
|
||||
startTerminalSessionCountdown() {
|
||||
this.resetTerminalSessionCountdown();
|
||||
this.terminalSessionStartedAt = Date.now();
|
||||
this.updateTerminalSessionCountdown();
|
||||
this.terminalSessionCountdownInterval = setInterval(() => {
|
||||
this.updateTerminalSessionCountdown();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
updateTerminalSessionCountdown() {
|
||||
if (!this.terminalSessionStartedAt) {
|
||||
this.terminalSessionRemainingSeconds = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedSeconds = (Date.now() - this.terminalSessionStartedAt) / 1000;
|
||||
this.terminalSessionRemainingSeconds = Math.max(0, MAX_TERMINAL_SESSION_SECONDS - elapsedSeconds);
|
||||
},
|
||||
|
||||
terminalSessionRemainingLabel() {
|
||||
if (this.terminalSessionRemainingSeconds === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `Session expires in ${formatTerminalSessionRemainingTime(this.terminalSessionRemainingSeconds)}`;
|
||||
},
|
||||
|
||||
terminalSessionTimerClass() {
|
||||
if (this.terminalSessionRemainingSeconds === null) {
|
||||
return 'text-neutral-300 bg-black/70 border-white/10';
|
||||
}
|
||||
|
||||
if (this.terminalSessionRemainingSeconds <= TERMINAL_SESSION_DANGER_SECONDS) {
|
||||
return 'text-red-200 bg-red-950/80 border-red-500/40';
|
||||
}
|
||||
|
||||
if (this.terminalSessionRemainingSeconds <= TERMINAL_SESSION_WARNING_SECONDS) {
|
||||
return 'text-yellow-200 bg-yellow-950/80 border-yellow-500/40';
|
||||
}
|
||||
|
||||
return 'text-neutral-300 bg-black/70 border-white/10';
|
||||
},
|
||||
|
||||
resetTerminal() {
|
||||
if (this.term) {
|
||||
this.$wire.dispatch('error', 'Terminal websocket connection lost.');
|
||||
this.term.reset();
|
||||
this.term.clear();
|
||||
this.$wire.dispatch('error', 'Terminal websocket connection lost. Reconnecting...');
|
||||
// Preserve scrollback so the user keeps the context of their previous
|
||||
// session. Print a visible marker so they know where the disconnect
|
||||
// happened. Old PTY shell state cannot be restored — this is purely
|
||||
// a visual carry-over.
|
||||
try {
|
||||
const stamp = new Date().toLocaleTimeString();
|
||||
this.term.write(`\r\n\x1b[33m── Connection lost at ${stamp}, reconnecting... ──\x1b[0m\r\n`);
|
||||
} catch (_) {
|
||||
// ignore — terminal not ready to receive writes
|
||||
}
|
||||
this.pendingWrites = 0;
|
||||
this.paused = false;
|
||||
this.commandBuffer = '';
|
||||
this.pendingCommand = null;
|
||||
this.resetTerminalSessionCountdown();
|
||||
|
||||
// Notify parent component that terminal disconnected
|
||||
this.$wire.dispatch('terminalDisconnected');
|
||||
@@ -186,7 +268,7 @@ export function initializeTerminalComponent() {
|
||||
this.term = new Terminal({
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
|
||||
fontFamily: '"Geist Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace, "Powerline Extra Symbols"',
|
||||
cursorBlink: true,
|
||||
rendererType: 'canvas',
|
||||
convertEol: true,
|
||||
@@ -276,10 +358,22 @@ export function initializeTerminalComponent() {
|
||||
this.connectionTimeoutId = null;
|
||||
}
|
||||
|
||||
// Flush any buffered command from before WebSocket was ready
|
||||
// Flush any buffered command from before WebSocket was ready, otherwise
|
||||
// replay the last command so a transient reconnect respawns the PTY
|
||||
// automatically without requiring the user to click Connect again.
|
||||
if (this.pendingCommand) {
|
||||
this.sendMessage(this.pendingCommand);
|
||||
this.pendingCommand = null;
|
||||
} else if (this.lastSentCommand) {
|
||||
logTerminal('log', '[Terminal] Replaying last command after reconnect.');
|
||||
this.sendMessage(this.lastSentCommand);
|
||||
}
|
||||
|
||||
// (Re)start application-level keepalive on every successful connect.
|
||||
// Server-side WebSocket protocol pings are the primary heartbeat; this
|
||||
// adds a JSON-level ping in case the server-side is older or restarting.
|
||||
if (!this.keepAliveInterval) {
|
||||
this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000);
|
||||
}
|
||||
|
||||
// Start ping timeout monitoring
|
||||
@@ -303,6 +397,7 @@ export function initializeTerminalComponent() {
|
||||
|
||||
this.connectionState = 'disconnected';
|
||||
this.clearAllTimers();
|
||||
this.resetTerminalSessionCountdown();
|
||||
|
||||
// Only reset terminal and reconnect if it wasn't a clean close
|
||||
if (event.code !== 1000) {
|
||||
@@ -354,6 +449,9 @@ export function initializeTerminalComponent() {
|
||||
sendMessage(message) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
if (message && message.command) {
|
||||
this.lastSentCommand = message;
|
||||
}
|
||||
} else {
|
||||
logTerminal('warn', '[Terminal] WebSocket not ready, message not sent:', message);
|
||||
}
|
||||
@@ -368,8 +466,6 @@ export function initializeTerminalComponent() {
|
||||
},
|
||||
|
||||
handleSocketMessage(event) {
|
||||
logTerminal('log', '[Terminal] Received WebSocket message:', event.data);
|
||||
|
||||
// Handle pong responses
|
||||
if (event.data === 'pong') {
|
||||
this.heartbeatMissed = 0;
|
||||
@@ -387,9 +483,18 @@ export function initializeTerminalComponent() {
|
||||
this.term.open(document.getElementById('terminal'));
|
||||
this.term._initialized = true;
|
||||
} else {
|
||||
this.term.reset();
|
||||
// Already initialized — this is a reconnect or a follow-up command.
|
||||
// Preserve scrollback so the user keeps context. Write a visible
|
||||
// separator so the new shell prompt is easy to spot.
|
||||
try {
|
||||
const stamp = new Date().toLocaleTimeString();
|
||||
this.term.write(`\r\n\x1b[32m── Reconnected at ${stamp} ──\x1b[0m\r\n`);
|
||||
} catch (_) {
|
||||
// ignore — fall through; xterm will render the new prompt anyway
|
||||
}
|
||||
}
|
||||
this.terminalActive = true;
|
||||
this.startTerminalSessionCountdown();
|
||||
this.term.focus();
|
||||
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm');
|
||||
|
||||
@@ -415,14 +520,20 @@ export function initializeTerminalComponent() {
|
||||
} else if (event.data === 'unprocessable') {
|
||||
if (this.term) this.term.reset();
|
||||
this.terminalActive = false;
|
||||
this.lastSentCommand = null;
|
||||
this.resetTerminalSessionCountdown();
|
||||
this.message = '(sorry, something went wrong, please try again)';
|
||||
|
||||
// Notify parent component that terminal connection failed
|
||||
this.$wire.dispatch('terminalDisconnected');
|
||||
} else if (event.data === 'pty-exited') {
|
||||
this.fullscreen = false;
|
||||
this.mobileToolbarCollapsed = false;
|
||||
this.terminalActive = false;
|
||||
this.resetTerminalSessionCountdown();
|
||||
this.term.reset();
|
||||
this.commandBuffer = '';
|
||||
this.lastSentCommand = null;
|
||||
|
||||
// Notify parent component that terminal disconnected
|
||||
this.$wire.dispatch('terminalDisconnected');
|
||||
@@ -433,6 +544,7 @@ export function initializeTerminalComponent() {
|
||||
logTerminal('error', '[Terminal] Backend rejected terminal startup:', event.data);
|
||||
this.$wire.dispatch('error', event.data);
|
||||
this.terminalActive = false;
|
||||
this.resetTerminalSessionCountdown();
|
||||
} else {
|
||||
try {
|
||||
this.pendingWrites++;
|
||||
@@ -493,12 +605,65 @@ export function initializeTerminalComponent() {
|
||||
});
|
||||
},
|
||||
|
||||
keepAlive() {
|
||||
// Skip keepalive when document is hidden to prevent unnecessary disconnects
|
||||
if (!this.isDocumentVisible) {
|
||||
|
||||
sendTerminalInput(data) {
|
||||
if (!this.term || !this.terminalActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.term.focus();
|
||||
this.sendMessage({ message: data });
|
||||
},
|
||||
|
||||
sendTerminalControl(sequence) {
|
||||
const terminalSequences = {
|
||||
arrowUp: '\x1b[A',
|
||||
arrowDown: '\x1b[B',
|
||||
arrowRight: '\x1b[C',
|
||||
arrowLeft: '\x1b[D',
|
||||
tab: '\t',
|
||||
escape: '\x1b',
|
||||
ctrlC: '\x03'
|
||||
};
|
||||
|
||||
if (terminalSequences[sequence]) {
|
||||
this.sendTerminalInput(terminalSequences[sequence]);
|
||||
}
|
||||
},
|
||||
|
||||
async pasteFromClipboard() {
|
||||
if (!navigator.clipboard?.readText) {
|
||||
this.$wire.dispatch('error', 'Clipboard paste is not available in this browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
this.sendTerminalInput(text);
|
||||
}
|
||||
} catch (error) {
|
||||
logTerminal('warn', '[Terminal] Clipboard paste failed:', error);
|
||||
this.$wire.dispatch('error', 'Clipboard paste permission was denied.');
|
||||
}
|
||||
},
|
||||
|
||||
async copyTerminalSelection() {
|
||||
const selection = this.term?.getSelection();
|
||||
if (!selection) {
|
||||
this.$wire.dispatch('error', 'Select terminal text before copying.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(selection);
|
||||
} catch (error) {
|
||||
logTerminal('warn', '[Terminal] Clipboard copy failed:', error);
|
||||
this.$wire.dispatch('error', 'Clipboard copy permission was denied.');
|
||||
}
|
||||
},
|
||||
|
||||
keepAlive() {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMessage({ ping: true });
|
||||
} else if (this.connectionState === 'disconnected') {
|
||||
@@ -524,10 +689,23 @@ export function initializeTerminalComponent() {
|
||||
logTerminal('log', '[Terminal] Tab visible, resuming connection management');
|
||||
|
||||
if (this.wasConnectedBeforeHidden && this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
// Send immediate ping to verify connection is still alive
|
||||
// Connection may be half-open after Cloudflare/proxy idle drop while hidden.
|
||||
// Probe with a short timeout (5s) instead of the default 35s — force a
|
||||
// reconnect quickly if no pong arrives so the user is not stuck typing
|
||||
// into a dead socket.
|
||||
this.heartbeatMissed = 0;
|
||||
this.sendMessage({ ping: true });
|
||||
this.resetPingTimeout();
|
||||
if (this.pingTimeoutId) {
|
||||
clearTimeout(this.pingTimeoutId);
|
||||
}
|
||||
this.pingTimeoutId = setTimeout(() => {
|
||||
logTerminal('warn', '[Terminal] Visibility-resume ping timed out, forcing reconnect.');
|
||||
try {
|
||||
this.socket.close(4000, 'Visibility-resume timeout');
|
||||
} catch (_) {
|
||||
// ignore — close handler will run on its own
|
||||
}
|
||||
}, 5000);
|
||||
} else if (this.wasConnectedBeforeHidden && this.connectionState !== 'connected') {
|
||||
// Was connected before but now disconnected - attempt reconnection
|
||||
this.reconnectAttempts = 0;
|
||||
@@ -576,15 +754,20 @@ export function initializeTerminalComponent() {
|
||||
// Force a refresh of the fit addon dimensions
|
||||
this.fitAddon.fit();
|
||||
|
||||
// Get fresh dimensions after fit
|
||||
const wrapperHeight = this.$refs.terminalWrapper.clientHeight;
|
||||
const wrapperWidth = this.$refs.terminalWrapper.clientWidth;
|
||||
// Get fresh dimensions from the terminal element itself. The mobile
|
||||
// toolbar can live beside the terminal in normal flow, so wrapper dimensions
|
||||
// would include controls that should not be counted as terminal rows.
|
||||
const terminalElement = document.getElementById('terminal');
|
||||
const terminalHeight = terminalElement?.clientHeight || this.$refs.terminalWrapper.clientHeight;
|
||||
const terminalWidth = terminalElement?.clientWidth || this.$refs.terminalWrapper.clientWidth;
|
||||
|
||||
// Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom)
|
||||
const horizontalPadding = 16; // 8px * 2 (left + right)
|
||||
const verticalPadding = 8; // 4px * 2 (top + bottom)
|
||||
const height = wrapperHeight - verticalPadding;
|
||||
const width = wrapperWidth - horizontalPadding;
|
||||
// Account for terminal container padding. In fullscreen mobile mode,
|
||||
// the fixed toolbar sits over the terminal container, so reserve its height
|
||||
// when calculating rows to keep the prompt above the controls.
|
||||
const horizontalPadding = 16; // px-2 = 8px * 2 (left + right)
|
||||
const verticalPadding = 8; // py-1 = 4px * 2 (top + bottom)
|
||||
const height = terminalHeight - verticalPadding;
|
||||
const width = terminalWidth - horizontalPadding;
|
||||
|
||||
// Check if dimensions are valid
|
||||
if (height <= 0 || width <= 0) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
MAX_TERMINAL_SESSION_SECONDS,
|
||||
formatTerminalSessionRemainingTime,
|
||||
} from './terminal-session-timer.js';
|
||||
|
||||
test('formatTerminalSessionRemainingTime formats the eight hour terminal limit countdown', () => {
|
||||
assert.equal(MAX_TERMINAL_SESSION_SECONDS, 8 * 60 * 60);
|
||||
assert.equal(formatTerminalSessionRemainingTime(MAX_TERMINAL_SESSION_SECONDS), '8h 00m 00s');
|
||||
assert.equal(formatTerminalSessionRemainingTime((7 * 60 * 60) + (59 * 60) + 59), '7h 59m 59s');
|
||||
assert.equal(formatTerminalSessionRemainingTime(65 * 60), '1h 05m 00s');
|
||||
assert.equal(formatTerminalSessionRemainingTime(59), '0m 59s');
|
||||
assert.equal(formatTerminalSessionRemainingTime(0), 'expired');
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
@props([
|
||||
'database',
|
||||
'label',
|
||||
'dbUrl' => null,
|
||||
'dbUrlPublic' => null,
|
||||
'supportsSsl' => true,
|
||||
'enableSsl' => false,
|
||||
'sslMode' => null,
|
||||
'sslModeOptions' => null,
|
||||
'sslModeHelper' => null,
|
||||
'certificateValidUntil' => null,
|
||||
'isExited' => false,
|
||||
'showPublicUrlPlaceholder' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$urlHelper = 'If you change the user/password/port, this could be different. This is with the default values.';
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input :label="$label . ' URL (internal)'" :helper="$urlHelper" type="password" readonly
|
||||
wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||
@if ($dbUrlPublic)
|
||||
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" type="password" readonly
|
||||
wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||
@elseif ($showPublicUrlPlaceholder)
|
||||
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" readonly
|
||||
value="Starting the database will generate this." canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
|
||||
@if ($supportsSsl)
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64" wire:key="enable_ssl">
|
||||
@if ($isExited)
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this setting." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($sslModeOptions && $enableSsl)
|
||||
<div class="mx-2">
|
||||
@if ($isExited)
|
||||
<x-forms.select id="sslMode" label="SSL Mode" wire:model.live="sslMode"
|
||||
instantSave="instantSaveSSL" :helper="$sslModeHelper" canGate="update"
|
||||
:canResource="$database">
|
||||
@foreach ($sslModeOptions as $value => $option)
|
||||
<option value="{{ $value }}" title="{{ $option['title'] ?? '' }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this setting." canGate="update"
|
||||
:canResource="$database">
|
||||
@foreach ($sslModeOptions as $value => $option)
|
||||
<option value="{{ $value }}" title="{{ $option['title'] ?? '' }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,111 @@
|
||||
@props([
|
||||
'diff' => null,
|
||||
'compact' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$changes = collect(data_get($diff, 'changes', []))->values()->all();
|
||||
$count = count($changes);
|
||||
$requiresBuild = collect($changes)->contains(fn ($change) => data_get($change, 'impact') === 'build');
|
||||
@endphp
|
||||
|
||||
@if ($count > 0)
|
||||
<div @class([
|
||||
'text-xs' => $compact,
|
||||
'text-sm' => ! $compact,
|
||||
])>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2 font-semibold text-black dark:text-white">
|
||||
<span>{{ $count }} configuration {{ $count === 1 ? 'change' : 'changes' }}</span>
|
||||
<span @class([
|
||||
'rounded-sm px-1.5 py-0.5 text-[0.65rem] font-semibold uppercase leading-none',
|
||||
'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300' => $requiresBuild,
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' => ! $requiresBuild,
|
||||
])>
|
||||
{{ $requiresBuild ? 'Rebuild required' : 'Redeploy required' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@unless ($compact)
|
||||
<div class="space-y-4">
|
||||
@foreach (collect($changes)->groupBy('section_label') as $sectionLabel => $sectionChanges)
|
||||
<div>
|
||||
<div class="mb-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-neutral-600 dark:text-neutral-400">
|
||||
{{ $sectionLabel }}
|
||||
</div>
|
||||
<div class="rounded-sm border border-neutral-300 dark:border-coolgray-200">
|
||||
<div class="grid grid-cols-[12rem_1fr_1.5rem_1fr] items-center gap-2 bg-neutral-100 px-3 py-1.5 text-[0.65rem] font-semibold uppercase tracking-wide text-neutral-500 dark:bg-coolgray-200 dark:text-neutral-400">
|
||||
<div>Field</div>
|
||||
<div>From</div>
|
||||
<div></div>
|
||||
<div>To</div>
|
||||
</div>
|
||||
<div class="divide-y divide-neutral-300 dark:divide-coolgray-200">
|
||||
@foreach ($sectionChanges as $change)
|
||||
@php
|
||||
$changeKey = (string) data_get($change, 'key');
|
||||
$expandable = data_get($change, 'expandable', false);
|
||||
$oldDisplay = (string) data_get($change, 'old_display_value');
|
||||
$newDisplay = (string) data_get($change, 'new_display_value');
|
||||
$oldFull = data_get($change, 'old_full_value') ?? $oldDisplay;
|
||||
$newFull = data_get($change, 'new_full_value') ?? $newDisplay;
|
||||
$label = (string) data_get($change, 'label');
|
||||
$labelTruncated = mb_strlen($label) > 20;
|
||||
$rowExpandable = $expandable || $labelTruncated;
|
||||
@endphp
|
||||
<div class="grid grid-cols-[12rem_1fr_1.5rem_1fr] items-start gap-2 px-3 py-1.5 text-neutral-700 dark:text-neutral-300">
|
||||
<div class="min-w-0 shrink-0 font-medium text-black dark:text-white">
|
||||
@if ($rowExpandable)
|
||||
<div class="break-words"
|
||||
:class="expandedRows['{{ $changeKey }}'] ? '' : 'truncate'"
|
||||
x-text="expandedRows['{{ $changeKey }}'] ? @js($label) : @js((string) str($label)->limit(20))"></div>
|
||||
@else
|
||||
{{ $label }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0 text-red-700 dark:text-red-400/80">
|
||||
@if ($expandable)
|
||||
<div class="break-words"
|
||||
:class="expandedRows['{{ $changeKey }}'] ? 'whitespace-pre-wrap' : 'truncate'"
|
||||
x-text="expandedRows['{{ $changeKey }}'] ? @js($oldFull) : @js($oldDisplay)"></div>
|
||||
@else
|
||||
<div class="truncate">{{ $oldDisplay }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-center text-neutral-500 dark:text-neutral-400">→</div>
|
||||
<div class="flex min-w-0 items-start gap-1 text-green-700 dark:text-green-500">
|
||||
<div class="min-w-0 flex-1">
|
||||
@if ($expandable)
|
||||
<div class="break-words"
|
||||
:class="expandedRows['{{ $changeKey }}'] ? 'whitespace-pre-wrap' : 'truncate'"
|
||||
x-text="expandedRows['{{ $changeKey }}'] ? @js($newFull) : @js($newDisplay)"></div>
|
||||
@else
|
||||
<div class="truncate">{{ $newDisplay }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($rowExpandable)
|
||||
<button type="button"
|
||||
x-on:click="expandedRows['{{ $changeKey }}'] = ! expandedRows['{{ $changeKey }}']"
|
||||
:aria-expanded="!! expandedRows['{{ $changeKey }}']"
|
||||
title="Toggle full value"
|
||||
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center text-neutral-400 transition hover:text-black dark:hover:text-white">
|
||||
<svg x-show="! expandedRows['{{ $changeKey }}']" class="h-3.5 w-3.5"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M13.28 7.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 0 0 1.06 1.06ZM2 17.25v-4.5a.75.75 0 0 1 1.5 0v2.69l3.22-3.22a.75.75 0 0 1 1.06 1.06L4.56 16.5h2.69a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<svg x-show="expandedRows['{{ $changeKey }}']" x-cloak class="h-3.5 w-3.5"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3.28 2.22a.75.75 0 0 0-1.06 1.06L5.44 6.5H2.75a.75.75 0 0 0 0 1.5h4.5A.75.75 0 0 0 8 7.25v-4.5a.75.75 0 0 0-1.5 0v2.69L3.28 2.22ZM13.5 2.75a.75.75 0 0 0-1.5 0v4.5c0 .414.336.75.75.75h4.5a.75.75 0 0 0 0-1.5h-2.69l3.22-3.22a.75.75 0 0 0-1.06-1.06L13.5 5.44V2.75ZM3.28 17.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 1 0 1.06 1.06ZM12 12.75c0-.414.336-.75.75-.75h4.5a.75.75 0 0 1 0 1.5h-2.69l3.22 3.22a.75.75 0 1 1-1.06 1.06l-3.22-3.22v2.69a.75.75 0 0 1-1.5 0v-4.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endunless
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,6 @@
|
||||
<span {{ $attributes->merge(['class' => 'inline-flex items-center']) }}>
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-medium leading-normal rounded-full bg-warning/15 text-warning border border-warning/30">
|
||||
Deprecated
|
||||
</span>
|
||||
</span>
|
||||
@@ -1,7 +1,44 @@
|
||||
<div x-data="{
|
||||
dropdownOpen: false
|
||||
}" class="relative" @click.outside="dropdownOpen = false">
|
||||
<button @click="dropdownOpen=true"
|
||||
dropdownOpen: false,
|
||||
panelStyles: '',
|
||||
open() {
|
||||
this.dropdownOpen = true;
|
||||
this.updatePanelPosition();
|
||||
},
|
||||
close() {
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
updatePanelPosition() {
|
||||
if (window.innerWidth >= 768) {
|
||||
this.panelStyles = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const triggerRect = this.$refs.trigger.getBoundingClientRect();
|
||||
const panelRect = this.$refs.panel.getBoundingClientRect();
|
||||
const viewportPadding = 8;
|
||||
let left = triggerRect.left;
|
||||
|
||||
if ((left + panelRect.width + viewportPadding) > window.innerWidth) {
|
||||
left = window.innerWidth - panelRect.width - viewportPadding;
|
||||
}
|
||||
|
||||
left = Math.max(viewportPadding, left);
|
||||
|
||||
let top = triggerRect.bottom + 4;
|
||||
const maxTop = window.innerHeight - panelRect.height - viewportPadding;
|
||||
|
||||
if (top > maxTop) {
|
||||
top = Math.max(viewportPadding, triggerRect.top - panelRect.height - viewportPadding);
|
||||
}
|
||||
|
||||
this.panelStyles = `position: fixed; left: ${left}px; top: ${top}px;`;
|
||||
});
|
||||
}
|
||||
}" class="relative" @click.outside="close()" x-on:resize.window="if (dropdownOpen) updatePanelPosition()">
|
||||
<button x-ref="trigger" @click="dropdownOpen ? close() : open()"
|
||||
class="inline-flex items-center justify-start pr-8 transition-colors focus:outline-hidden disabled:opacity-50 disabled:pointer-events-none">
|
||||
<span class="flex flex-col items-start h-full leading-none">
|
||||
{{ $title }}
|
||||
@@ -13,11 +50,11 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="dropdownOpen" @click.away="dropdownOpen=false" x-transition:enter="ease-out duration-200"
|
||||
<div x-ref="panel" x-show="dropdownOpen" @click.away="close()" x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="-translate-y-2" x-transition:enter-end="translate-y-0"
|
||||
class="absolute top-0 z-50 mt-6 min-w-max" x-cloak>
|
||||
:style="panelStyles" class="absolute top-full z-50 mt-1 min-w-max max-w-[calc(100vw-1rem)] md:top-0 md:mt-6" x-cloak>
|
||||
<div
|
||||
class="p-1 mt-1 bg-white border rounded-sm shadow-sm dark:bg-coolgray-200 dark:border-coolgray-300 border-neutral-300">
|
||||
class="border border-neutral-300 bg-white p-1 shadow-sm dark:border-coolgray-300 dark:bg-coolgray-200">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
])
|
||||
|
||||
<div @class([
|
||||
'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit',
|
||||
'form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2',
|
||||
'w-full' => $fullWidth,
|
||||
'dark:hover:bg-coolgray-100 cursor-pointer' => !$disabled,
|
||||
])>
|
||||
<label @class(['flex gap-4 items-center px-0 min-w-fit label w-full'])>
|
||||
<span class="flex grow gap-2">
|
||||
<label @class(['label flex w-full max-w-full min-w-0 items-center gap-4 px-0'])>
|
||||
<span class="flex min-w-0 grow gap-2 break-words">
|
||||
@if ($label)
|
||||
@if ($disabled)
|
||||
<span class="opacity-60">{!! $label !!}</span>
|
||||
@@ -29,16 +29,16 @@
|
||||
@endif
|
||||
</span>
|
||||
@if ($instantSave)
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
wire:loading.attr="disabled"
|
||||
wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
wire:model={{ $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@else
|
||||
@if ($domValue)
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@else
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
|
||||
wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
@props(['text'])
|
||||
@props(['text', 'label' => null])
|
||||
|
||||
<div class="relative" x-data="{ copied: false, isSecure: window.isSecureContext }">
|
||||
<input type="text" value="{{ $text }}" readonly class="input">
|
||||
<div class="w-full" x-data="{ copied: false, isSecure: window.isSecureContext }">
|
||||
@if ($label)
|
||||
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}</label>
|
||||
@endif
|
||||
<div class="relative">
|
||||
<input type="text" value="{{ $text }}" class="input pr-10"
|
||||
@keydown.prevent @paste.prevent @cut.prevent @drop.prevent
|
||||
@focus="$event.target.select()">
|
||||
<button
|
||||
x-show="isSecure"
|
||||
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
|
||||
@@ -15,4 +21,5 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,15 +10,22 @@
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<div class="relative" x-data="{
|
||||
<div class="relative" @success.window="type = '{{ $type }}'" x-data="{
|
||||
type: '{{ $type }}',
|
||||
showDropdown: false,
|
||||
suggestions: [],
|
||||
selectedIndex: 0,
|
||||
cursorPosition: 0,
|
||||
currentScope: null,
|
||||
availableScopes: ['team', 'project', 'environment'],
|
||||
availableVars: @js($availableVars),
|
||||
get availableScopes() {
|
||||
// Only include scopes that have at least one variable
|
||||
const allScopes = ['team', 'project', 'environment', 'server'];
|
||||
return allScopes.filter(scope => {
|
||||
const vars = this.availableVars[scope];
|
||||
return vars && vars.length > 0;
|
||||
});
|
||||
},
|
||||
scopeUrls: @js($scopeUrls),
|
||||
|
||||
handleInput() {
|
||||
@@ -54,6 +61,11 @@
|
||||
|
||||
if (content === '') {
|
||||
this.currentScope = null;
|
||||
// Only show dropdown if there are available scopes with variables
|
||||
if (this.availableScopes.length === 0) {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
this.suggestions = this.availableScopes.map(scope => ({
|
||||
type: 'scope',
|
||||
value: scope,
|
||||
@@ -184,43 +196,51 @@
|
||||
}"
|
||||
@click.outside="showDropdown = false">
|
||||
|
||||
@if ($type === 'password' && $allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white z-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<input
|
||||
x-ref="input"
|
||||
@input="handleInput()"
|
||||
@keydown="handleKeydown($event)"
|
||||
@click="handleInput()"
|
||||
autocomplete="{{ $autocomplete }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@required($required)
|
||||
@readonly($readonly)
|
||||
@if ($modelBinding !== 'null')
|
||||
wire:model="{{ $modelBinding }}"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@endif
|
||||
wire:loading.attr="disabled"
|
||||
@if ($type === 'password')
|
||||
:type="type"
|
||||
@else
|
||||
@disabled($disabled)
|
||||
@if ($type !== 'password')
|
||||
type="{{ $type }}"
|
||||
@endif
|
||||
@disabled($disabled)
|
||||
@if ($htmlId !== 'null') id="{{ $htmlId }}" @endif
|
||||
name="{{ $name }}"
|
||||
placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) autofocus @endif>
|
||||
|
||||
@if ($type === 'password' && $allowToPeak)
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
{{-- Dropdown for suggestions --}}
|
||||
<div x-show="showDropdown"
|
||||
x-transition
|
||||
|
||||
@@ -13,10 +13,21 @@
|
||||
</label>
|
||||
@endif
|
||||
@if ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }">
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
@if ($allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType; type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white">
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
{{-- Eye icon (shown when password is hidden) --}}
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -32,16 +43,8 @@
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
|
||||
</div>
|
||||
@else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div wire:key="{{ rand() }}" class="coolify-monaco-editor flex-1">
|
||||
<div wire:key="{{ random_int(0, PHP_INT_MAX) }}" class="coolify-monaco-editor flex-1">
|
||||
<div x-ref="monacoRef" x-data="{
|
||||
monacoVersion: '0.52.2',
|
||||
monacoContent: @entangle($id),
|
||||
|
||||
@@ -30,19 +30,7 @@
|
||||
readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" />
|
||||
@else
|
||||
@if ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }">
|
||||
@if ($allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType"
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@@ -58,6 +46,26 @@
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
@if ($allowToPeak)
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
|
||||
aria-label="Toggle password visibility">
|
||||
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@else
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div {{ $attributes->merge(['class' => 'group']) }}>
|
||||
<div x-data="{ open: false }" @click.stop="open = !open" @click.outside="open = false"
|
||||
{{ $attributes->merge(['class' => 'group']) }}>
|
||||
<div class="info-helper">
|
||||
@isset($icon)
|
||||
{{ $icon }}
|
||||
@@ -10,7 +11,7 @@
|
||||
@endisset
|
||||
|
||||
</div>
|
||||
<div class="info-helper-popup">
|
||||
<div class="info-helper-popup" :class="{ 'block': open }">
|
||||
<div class="p-4">
|
||||
{!! $helper !!}
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,11 @@
|
||||
}"
|
||||
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
|
||||
class="relative w-auto h-auto">
|
||||
@if ($customButton)
|
||||
@if (isset($trigger))
|
||||
<div @click="modalOpen=true">
|
||||
{{ $trigger }}
|
||||
</div>
|
||||
@elseif ($customButton)
|
||||
@if ($buttonFullWidth)
|
||||
<x-forms.button @click="modalOpen=true" class="w-full">
|
||||
{{ $customButton }}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
|
||||
<nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
|
||||
:class="collapsed ? 'px-2 lg:px-[0.7rem] sidebar-collapsed' : 'px-2 lg:px-[0.7rem]'"
|
||||
@mouseover="
|
||||
if (!collapsed) return;
|
||||
const el = $event.target.closest('.menu-item');
|
||||
if (!el) { tooltip.show = false; return; }
|
||||
const text = el.getAttribute('title') || el.getAttribute('aria-label') || '';
|
||||
if (!text) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
tooltip.text = text;
|
||||
tooltip.x = rect.right + 8;
|
||||
tooltip.y = rect.top + rect.height / 2;
|
||||
tooltip.show = true;
|
||||
"
|
||||
@mouseleave="tooltip.show = false"
|
||||
x-data="{
|
||||
tooltip: { text: '', x: 0, y: 0, show: false },
|
||||
switchWidth() {
|
||||
if (this.full === 'full') {
|
||||
localStorage.setItem('pageWidth', 'center');
|
||||
@@ -34,23 +49,32 @@
|
||||
localStorage.setItem('theme', type);
|
||||
this.queryTheme();
|
||||
},
|
||||
cycleTheme() {
|
||||
const themes = ['light', 'system', 'dark'];
|
||||
const currentIndex = themes.indexOf(this.theme || localStorage.getItem('theme') || 'dark');
|
||||
this.setTheme(themes[(currentIndex + 1) % themes.length]);
|
||||
},
|
||||
queryTheme() {
|
||||
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const userSettings = localStorage.getItem('theme') || 'dark';
|
||||
localStorage.setItem('theme', userSettings);
|
||||
let isDark = false;
|
||||
if (userSettings === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
this.theme = 'dark';
|
||||
isDark = true;
|
||||
} else if (userSettings === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
this.theme = 'light';
|
||||
} else if (darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.add('dark');
|
||||
isDark = true;
|
||||
} else if (!darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
},
|
||||
checkZoom() {
|
||||
if (this.zoom === null) {
|
||||
@@ -77,15 +101,25 @@
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<div class="flex pt-4 pb-4 pl-2 pr-3 items-start gap-3"
|
||||
:class="collapsed ? 'lg:flex-col lg:items-center lg:pl-0 lg:pr-0 lg:gap-3 lg:pt-7' : 'lg:pt-6'">
|
||||
<div class="flex min-w-0 flex-1 flex-col" :class="collapsed && 'lg:hidden'">
|
||||
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-tight dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<x-version />
|
||||
</div>
|
||||
<div>
|
||||
<div class="hidden flex-col items-center w-full gap-1"
|
||||
:class="collapsed && 'lg:flex'">
|
||||
<a href="/" {{ wireNavigate() }}
|
||||
class="hover:opacity-80 transition-opacity"
|
||||
title="Coolify">
|
||||
<img src="/coolify-logo.svg" alt="Coolify" class="w-6 h-6" />
|
||||
</a>
|
||||
<x-version class="text-[10px]" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1" :class="collapsed && 'lg:hidden'">
|
||||
<!-- Search button that triggers global search modal -->
|
||||
<button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)"
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
|
||||
class="flex h-8 w-full items-center justify-between gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -95,9 +129,8 @@
|
||||
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<livewire:settings-dropdown />
|
||||
</div>
|
||||
<div class="px-2 pt-2 pb-7">
|
||||
<div class="px-2 pt-2 pb-7 overflow-hidden" :class="collapsed && 'lg:px-0 lg:pt-0 lg:pb-4 lg:min-h-8 lg:flex lg:justify-center'">
|
||||
<livewire:switch-team />
|
||||
</div>
|
||||
<ul role="list" class="flex flex-col flex-1 gap-y-7">
|
||||
@@ -112,7 +145,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Dashboard</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -127,7 +160,7 @@
|
||||
<path d="M4 12l8 4l8 -4" />
|
||||
<path d="M4 16l8 4l8 -4" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Projects</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Projects</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -145,7 +178,7 @@
|
||||
<path d="M7 16v.01" />
|
||||
<path d="M20 15l-2 3h3l-2 3" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Servers</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Servers</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -157,7 +190,7 @@
|
||||
<path fill="currentColor"
|
||||
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Sources</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Sources</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -170,7 +203,7 @@
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 4L3 8v12l6-3l6 3l6-4V4l-6 3l-6-3zm-2 8.001V12m4 .001V12m3-2l2 2m2 2l-2-2m0 0l2-2m-2 2l-2 2" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Destinations</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Destinations</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -185,7 +218,7 @@
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</g>
|
||||
</svg>
|
||||
<span class="menu-item-label">S3 Storages</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">S3 Storages</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -200,7 +233,7 @@
|
||||
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9" />
|
||||
</g>
|
||||
</svg>
|
||||
<span class="menu-item-label">Shared Variables</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Shared Variables</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -212,7 +245,7 @@
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Notifications</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Notifications</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -224,7 +257,7 @@
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Keys & Tokens</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Keys & Tokens</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -239,7 +272,7 @@
|
||||
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
|
||||
</g>
|
||||
</svg>
|
||||
<span class="menu-item-label">Tags</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Tags</span>
|
||||
</a>
|
||||
</li>
|
||||
@can('canAccessTerminal')
|
||||
@@ -254,7 +287,7 @@
|
||||
<path d="M5 7l5 5l-5 5" />
|
||||
<path d="M12 19l7 0" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Terminal</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Terminal</span>
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
@@ -270,7 +303,7 @@
|
||||
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
|
||||
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Profile</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -288,7 +321,7 @@
|
||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Teams</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Teams</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (isCloud() && auth()->user()->isAdmin())
|
||||
@@ -301,7 +334,7 @@
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Subscription</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Subscription</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@@ -319,7 +352,7 @@
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Settings</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@@ -333,12 +366,76 @@
|
||||
<path fill="currentColor"
|
||||
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Admin</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Admin</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
<div class="flex-1"></div>
|
||||
<li>
|
||||
<livewire:settings-dropdown trigger="changelog-sidebar" />
|
||||
</li>
|
||||
<li>
|
||||
<div class="menu-item" title="Theme" aria-label="Theme switcher" :class="collapsed && 'lg:hidden'">
|
||||
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Theme</span>
|
||||
<div class="ml-auto flex items-center gap-0.5 rounded-sm bg-neutral-100 p-0.5 dark:bg-coolgray-200">
|
||||
<button type="button" @click.stop="setTheme('light')" title="Light" aria-label="Use light theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'light' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click.stop="setTheme('system')" title="System default" aria-label="Use system theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'system' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click.stop="setTheme('dark')" title="Dark" aria-label="Use dark theme"
|
||||
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
|
||||
:class="theme === 'dark' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click.stop="cycleTheme()"
|
||||
:title="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change.`"
|
||||
:aria-label="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change theme.`"
|
||||
class="menu-item hidden"
|
||||
:class="collapsed && 'lg:flex'">
|
||||
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
@if (isInstanceAdmin() && !isCloud())
|
||||
@persist('upgrade')
|
||||
<li>
|
||||
@@ -368,7 +465,7 @@
|
||||
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
|
||||
</g>
|
||||
</svg>
|
||||
<span class="menu-item-label">Sponsor us</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Sponsor us</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@@ -384,7 +481,7 @@
|
||||
<path fill="currentColor"
|
||||
d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
|
||||
</svg>
|
||||
<span class="menu-item-label">Feedback</span>
|
||||
<span class="menu-item-label" :class="collapsed && 'lg:hidden'">Feedback</span>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:help />
|
||||
@@ -393,16 +490,22 @@
|
||||
<li>
|
||||
<form action="/logout" method="POST">
|
||||
@csrf
|
||||
<button title="Logout" type="submit" class="gap-2 mb-6 menu-item">
|
||||
<svg class="menu-item-icon mr-1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<button title="Logout" type="submit" class="mb-6 menu-item">
|
||||
<svg class="menu-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22m7-6v-3h-8v-2h8V8l5 4z" />
|
||||
</svg>
|
||||
<span>Logout</span>
|
||||
<span class="text-left menu-item-label" :class="collapsed && 'lg:hidden'">Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div x-show="collapsed && tooltip.show"
|
||||
x-cloak
|
||||
x-transition.opacity.duration.100ms
|
||||
:style="`left: ${tooltip.x}px; top: ${tooltip.y}px;`"
|
||||
class="fixed z-[100] -translate-y-1/2 px-2 py-1 text-xs font-medium rounded-md bg-neutral-900 dark:bg-coolgray-300 text-white whitespace-nowrap pointer-events-none shadow-lg border border-neutral-700 dark:border-coolgray-200"
|
||||
x-text="tooltip.text"></div>
|
||||
</nav>
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1>Notifications</h1>
|
||||
<div class="subtitle">Get notified about your infrastructure.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-3.5 min-h-10">
|
||||
<nav class="flex items-center gap-6 min-h-10 whitespace-nowrap">
|
||||
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.email') }}">
|
||||
<button>Email</button>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="pb-6">
|
||||
<h1>Profile</h1>
|
||||
<div class="subtitle">Your user profile settings.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10">
|
||||
<a class="{{ request()->routeIs('profile') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('profile') }}">
|
||||
General
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('profile.appearance') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('profile.appearance') }}">
|
||||
Appearance
|
||||
</a>
|
||||
<div class="flex-1"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,9 +29,47 @@
|
||||
$currentProjectUuid = data_get($resource, 'environment.project.uuid');
|
||||
$currentEnvironmentUuid = data_get($resource, 'environment.uuid');
|
||||
$currentResourceUuid = data_get($resource, 'uuid');
|
||||
$resourceUuid = data_get($resource, 'uuid');
|
||||
$resourceType = $resource->getMorphClass();
|
||||
$isApplication = $resourceType === 'App\Models\Application';
|
||||
$isService = $resourceType === 'App\Models\Service';
|
||||
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
|
||||
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') &&
|
||||
($resource->relationLoaded('additional_servers') ? $resource->additional_servers->count() > 0 : ($resource->additional_servers_count ?? 0) > 0);
|
||||
$serverName = $hasMultipleServers ? null : data_get($resource, 'destination.server.name');
|
||||
$routeParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
];
|
||||
if ($isApplication) {
|
||||
$routeParams['application_uuid'] = $resourceUuid;
|
||||
} elseif ($isService) {
|
||||
$routeParams['service_uuid'] = $resourceUuid;
|
||||
} else {
|
||||
$routeParams['database_uuid'] = $resourceUuid;
|
||||
}
|
||||
@endphp
|
||||
<nav class="flex pt-2 pb-10">
|
||||
<ol class="flex flex-wrap items-center gap-y-1">
|
||||
<nav class="pt-2 pb-4 md:pb-10">
|
||||
<div class="flex min-w-0 flex-col gap-1 md:hidden">
|
||||
<div class="flex min-w-0 items-center text-xs text-neutral-400">
|
||||
<a class="min-w-0 truncate text-neutral-300 hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ $isApplication
|
||||
? route('project.application.configuration', $routeParams)
|
||||
: ($isService
|
||||
? route('project.service.configuration', $routeParams)
|
||||
: route('project.database.configuration', $routeParams)) }}"
|
||||
title="{{ data_get($resource, 'name') }}{{ $serverName ? ' ('.$serverName.')' : '' }}">
|
||||
{{ data_get($resource, 'name') }}
|
||||
</a>
|
||||
</div>
|
||||
@if ($resource->getMorphClass() == 'App\Models\Service')
|
||||
<x-status.services :service="$resource" />
|
||||
@else
|
||||
<x-status.index :resource="$resource" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<ol class="hidden flex-wrap items-center gap-y-1 md:flex">
|
||||
<!-- Project Level -->
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false, closeTimeout: null, toggle() { this.projectOpen = !this.projectOpen }, open() { clearTimeout(this.closeTimeout); this.projectOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.projectOpen = false }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
|
||||
@@ -204,27 +242,6 @@
|
||||
</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
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
@endif
|
||||
@if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
|
||||
<a class="sub-menu-item {{ $activeMenu === 'swarm' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.swarm', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Swarm (experimental)</span>
|
||||
href="{{ route('server.swarm', ['server_uuid' => $server->uuid]) }}"><span class="menu-item-label">Swarm</span>
|
||||
</a>
|
||||
@endif
|
||||
@if (!$server->isLocalhost())
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
</a>
|
||||
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', $parameters) }}"><span class="menu-item-label">General</span></a>
|
||||
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index.advanced', $parameters) }}"><span class="menu-item-label">Advanced</span></a>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.database.backups', $parameters) }}"><span class="menu-item-label">Backups</span></a>
|
||||
|
||||
@@ -3,35 +3,37 @@
|
||||
'lastDeploymentLink' => null,
|
||||
'resource' => null,
|
||||
])
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('degraded'))
|
||||
<x-status.degraded :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting'))
|
||||
<x-status.restarting :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@else
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited'))
|
||||
<div class="flex items-center pl-2">
|
||||
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
|
||||
({{ $resource->restart_count }}x restarts)
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('degraded'))
|
||||
<x-status.degraded :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting'))
|
||||
<x-status.restarting :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@else
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited'))
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
|
||||
({{ $resource->restart_count }}x restarts)
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -1,30 +1,32 @@
|
||||
@php
|
||||
$displayStatus = formatContainerStatus($complexStatus);
|
||||
@endphp
|
||||
@if (str($displayStatus)->lower()->contains('running'))
|
||||
<x-status.running :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('starting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('restarting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('degraded'))
|
||||
<x-status.degraded :status="$displayStatus" />
|
||||
@else
|
||||
<x-status.stopped :status="$displayStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
@if (str($displayStatus)->lower()->contains('running'))
|
||||
<x-status.running :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('starting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('restarting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('degraded'))
|
||||
<x-status.degraded :status="$displayStatus" />
|
||||
@else
|
||||
<x-status.stopped :status="$displayStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<x-emails.layout>
|
||||
Your Coolify API token ({{ $tokenName }}) expires on {{ $expiresAt }}.
|
||||
|
||||
Rotate this token before it expires. API calls using this token will start failing once the expiration time is reached.
|
||||
|
||||
Manage your API tokens [here]({{ $manageUrl }}).
|
||||
</x-emails.layout>
|
||||
@@ -1,5 +1,5 @@
|
||||
<x-emails.layout>
|
||||
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
|
||||
|
||||
Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).
|
||||
Please update your subscription to enable the server again [here](https://app.coolify.io/subscription).
|
||||
</x-emails.layout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<x-layout-simple>
|
||||
<section class="bg-gray-50 dark:bg-base">
|
||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||
Coolify
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="p-6 rounded-lg border border-neutral-500/20 bg-white/5">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Team Invitation</h2>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400 mb-2">
|
||||
You have been invited to join:
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{{ $team->name }}
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400 mb-1">
|
||||
Role: <span class="font-medium text-gray-900 dark:text-white">{{ ucfirst($invitation->role) }}</span>
|
||||
</p>
|
||||
|
||||
@if ($alreadyMember)
|
||||
<div class="mt-4 p-3 bg-warning/10 border border-warning rounded-lg">
|
||||
<p class="text-sm text-warning">You are already a member of this team.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('team.invitation.accept', $invitation->uuid) }}" class="mt-6">
|
||||
@csrf
|
||||
<x-forms.button class="w-full justify-center py-3 box-boarding" type="submit" isHighlighted>
|
||||
{{ $alreadyMember ? 'Dismiss Invitation' : 'Accept Invitation' }}
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layout-simple>
|
||||
@@ -7,18 +7,29 @@
|
||||
<!-- Global search component - included once to prevent keyboard shortcut duplication -->
|
||||
<livewire:global-search />
|
||||
@auth
|
||||
<livewire:deployments-indicator />
|
||||
<div x-data="{
|
||||
open: false,
|
||||
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
|
||||
pageWidth: localStorage.getItem('pageWidth') || 'full',
|
||||
sidebarReady: false,
|
||||
init() {
|
||||
this.pageWidth = localStorage.getItem('pageWidth');
|
||||
if (!this.pageWidth) {
|
||||
this.pageWidth = 'full';
|
||||
localStorage.setItem('pageWidth', 'full');
|
||||
if (!localStorage.getItem('pageWidth')) {
|
||||
localStorage.setItem('pageWidth', this.pageWidth);
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.sidebarReady = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.collapsed = !this.collapsed;
|
||||
localStorage.setItem('sidebarCollapsed', this.collapsed);
|
||||
}
|
||||
}" x-cloak class="mx-auto dark:text-inherit text-black"
|
||||
:class="pageWidth === 'full' ? '' : 'max-w-7xl'">
|
||||
<livewire:deployments-indicator />
|
||||
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
|
||||
<div class="fixed inset-y-0 right-0 h-full flex">
|
||||
@@ -40,10 +51,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col min-w-0">
|
||||
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col min-w-0"
|
||||
:class="[collapsed ? 'lg:w-16' : 'lg:w-56', sidebarReady ? 'transition-[width] duration-200' : '']">
|
||||
<div class="flex flex-col overflow-y-auto grow gap-y-5 scrollbar min-w-0">
|
||||
<x-navbar />
|
||||
</div>
|
||||
<button type="button" @click="toggleSidebar()"
|
||||
:title="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||
class="absolute top-8 -right-3 z-50 hidden lg:flex items-center justify-center w-6 h-6 rounded-full border bg-white dark:bg-coolgray-100 dark:border-coolgray-200 border-neutral-300 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-neutral-600 dark:text-neutral-300 transition-transform"
|
||||
:class="collapsed ? '' : 'rotate-180'"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -62,10 +83,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main class="lg:pl-56">
|
||||
<div class="p-4 sm:px-6 lg:px-8 lg:py-6">
|
||||
<main class="p-6" :class="[collapsed ? 'lg:pl-[6rem]' : 'lg:pl-[16rem]', sidebarReady ? 'transition-[padding] duration-200' : '']">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
@@ -172,7 +172,8 @@
|
||||
}
|
||||
@auth
|
||||
window.Pusher = Pusher;
|
||||
window.Echo = new Echo({
|
||||
const EchoConstructor = typeof Echo === 'function' ? Echo : Echo.default;
|
||||
window.Echo = new EchoConstructor({
|
||||
broadcaster: 'pusher',
|
||||
cluster: "{{ config('constants.pusher.host') }}" || window.location.hostname,
|
||||
key: "{{ config('constants.pusher.app_key') }}" || 'coolify',
|
||||
@@ -203,30 +204,6 @@
|
||||
let checkHealthInterval = null;
|
||||
let checkIfIamDeadInterval = null;
|
||||
|
||||
function changePasswordFieldType(event) {
|
||||
let element = event.target
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (element.className === "relative") {
|
||||
break;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
element = element.children[1];
|
||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
if (element.disabled) return;
|
||||
element.classList.add('truncate');
|
||||
this.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
if (element.disabled) return;
|
||||
element.classList.remove('truncate');
|
||||
this.type = 'password';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
|
||||
}
|
||||
@@ -326,4 +303,4 @@
|
||||
</body>
|
||||
@show
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -34,10 +34,10 @@
|
||||
}
|
||||
}" x-init="// Initial scroll
|
||||
$nextTick(() => scrollToBottom());
|
||||
|
||||
|
||||
// Add scroll event listener
|
||||
$el.addEventListener('scroll', () => handleScroll());
|
||||
|
||||
|
||||
// Set up mutation observer to watch for content changes
|
||||
observer = new MutationObserver(() => {
|
||||
$nextTick(() => scrollToBottom());
|
||||
@@ -52,7 +52,7 @@
|
||||
'flex-1 min-h-0' => $fullHeight,
|
||||
'max-h-96' => !$fullHeight,
|
||||
])>
|
||||
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
|
||||
<pre class="font-logs whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
|
||||
</div>
|
||||
@else
|
||||
@if ($showWaiting)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<x-modal-input buttonTitle="Add" title="New Project">
|
||||
<x-slot:content>
|
||||
<button
|
||||
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@@ -81,7 +81,7 @@
|
||||
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<button
|
||||
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
|
||||
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div wire:poll.3000ms x-data="{
|
||||
expanded: @entangle('expanded'),
|
||||
reduceOpacity: @js($this->shouldReduceOpacity)
|
||||
}" class="fixed bottom-0 z-60 mb-4 left-0 lg:left-56 ml-4">
|
||||
}" class="fixed bottom-0 left-0 z-60 mb-4 ml-4 transition-[left] duration-200"
|
||||
:class="collapsed ? 'lg:left-16' : 'lg:left-56'">
|
||||
@if ($this->deploymentCount > 0)
|
||||
<div class="relative transition-opacity duration-200"
|
||||
:class="{ 'opacity-100': expanded || !reduceOpacity, 'opacity-60 hover:opacity-100': reduceOpacity && !expanded }">
|
||||
|
||||
@@ -14,31 +14,30 @@
|
||||
</div>
|
||||
<div class="subtitle">Network endpoints to deploy your resources.</div>
|
||||
<div class="grid gap-4 lg:grid-cols-2 -mt-1">
|
||||
@forelse ($servers as $server)
|
||||
@forelse ($server->destinations() as $destination)
|
||||
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">{{ $destination->name }}</div>
|
||||
<div class="box-description">Server: {{ $destination->server->name }}</div>
|
||||
@forelse ($destinations as $destination)
|
||||
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">{{ $destination->name }}</div>
|
||||
<div class="box-description">Server: {{ $destination->server->name }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">
|
||||
{{ $destination->name }}
|
||||
<x-deprecated-badge />
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">{{ $destination->name }}</div>
|
||||
<div class="box-description">server: {{ $destination->server->name }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@empty
|
||||
<div>No destinations found.</div>
|
||||
@endforelse
|
||||
<div class="box-description">Server: {{ $destination->server->name }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@empty
|
||||
<div>No servers found.</div>
|
||||
<div>No destinations found.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,9 @@
|
||||
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
|
||||
<div class="subtitle ">A simple Docker network.</div>
|
||||
@else
|
||||
<div class="subtitle ">A swarm Docker network. WIP</div>
|
||||
<div class="subtitle flex items-center gap-2">A swarm Docker network.
|
||||
<x-deprecated-badge />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$destination" id="name" label="Name" />
|
||||
|
||||
@@ -835,6 +835,20 @@
|
||||
<div class="font-medium text-neutral-900 dark:text-white truncate"
|
||||
x-text="item.name">
|
||||
</div>
|
||||
<template x-if="item.amd_only">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 shrink-0"
|
||||
title="This service only supports AMD64/x86_64 architecture">
|
||||
AMD only
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="item.arm_only">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 shrink-0"
|
||||
title="This service only supports ARM64/aarch64 architecture">
|
||||
ARM only
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0"
|
||||
x-text="item.quickcommand"
|
||||
@@ -1123,4 +1137,4 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
@endif
|
||||
</div>
|
||||
@if (!isCloud())
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSave()" id="useInstanceEmailSettings"
|
||||
label="Use system wide (transactional) email settings" />
|
||||
</div>
|
||||
@@ -72,7 +72,7 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col w-full gap-2 xl:flex-row">
|
||||
<x-forms.input canGate="update" :canResource="$settings" required id="smtpHost" placeholder="smtp.mailgun.org" label="Host" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" required id="smtpPort" placeholder="587" label="Port" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" required id="smtpPort" type="number" placeholder="587" label="Port" />
|
||||
<x-forms.select canGate="update" :canResource="$settings" required id="smtpEncryption" label="Encryption">
|
||||
<option value="starttls">StartTLS</option>
|
||||
<option value="tls">TLS/SSL</option>
|
||||
@@ -82,7 +82,7 @@
|
||||
<div class="flex flex-col w-full gap-2 xl:flex-row">
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpUsername" label="SMTP Username" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpPassword" type="password" label="SMTP Password" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpTimeout" helper="Timeout value for sending emails."
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpTimeout" type="number" helper="Timeout value for sending emails."
|
||||
label="Timeout" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<h3 class="text-lg font-medium mb-3">Deployments</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentSuccessTelegramNotifications"
|
||||
label="Deployment Success" />
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
id="telegramNotificationsDeploymentSuccessThreadId" />
|
||||
</div>
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="deploymentFailureTelegramNotifications"
|
||||
label="Deployment Failure" />
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
id="telegramNotificationsDeploymentFailureThreadId" />
|
||||
</div>
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="statusChangeTelegramNotifications"
|
||||
label="Container Status Changes"
|
||||
helper="Send a notification when a container status changes. It will send a notification for Stopped and Restarted events of a container." />
|
||||
@@ -71,7 +71,7 @@
|
||||
<h3 class="text-lg font-medium mb-3">Backups</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupSuccessTelegramNotifications"
|
||||
label="Backup Success" />
|
||||
</div>
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="backupFailureTelegramNotifications"
|
||||
label="Backup Failure" />
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
<h3 class="text-lg font-medium mb-3">Scheduled Tasks</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskSuccessTelegramNotifications"
|
||||
label="Scheduled Task Success" />
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="scheduledTaskFailureTelegramNotifications"
|
||||
label="Scheduled Task Failure" />
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@
|
||||
<h3 class="text-lg font-medium mb-3">Server</h3>
|
||||
<div class="flex flex-col gap-1.5 pl-1">
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupSuccessTelegramNotifications"
|
||||
label="Docker Cleanup Success" />
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="dockerCleanupFailureTelegramNotifications"
|
||||
label="Docker Cleanup Failure" />
|
||||
</div>
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverDiskUsageTelegramNotifications"
|
||||
label="Server Disk Usage" />
|
||||
</div>
|
||||
@@ -144,7 +144,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverReachableTelegramNotifications"
|
||||
label="Server Reachable" />
|
||||
</div>
|
||||
@@ -153,7 +153,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverUnreachableTelegramNotifications"
|
||||
label="Server Unreachable" />
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchTelegramNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedTelegramNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Appearance | Coolify
|
||||
</x-slot>
|
||||
<x-profile.navbar />
|
||||
|
||||
<div x-data="{
|
||||
theme: localStorage.getItem('theme') || 'dark',
|
||||
pageWidth: localStorage.getItem('pageWidth') || 'full',
|
||||
zoom: localStorage.getItem('zoom') || '100',
|
||||
init() {
|
||||
localStorage.setItem('theme', this.theme);
|
||||
localStorage.setItem('pageWidth', this.pageWidth);
|
||||
localStorage.setItem('zoom', this.zoom);
|
||||
this.applyTheme();
|
||||
},
|
||||
setTheme(type) {
|
||||
this.theme = type;
|
||||
localStorage.setItem('theme', type);
|
||||
this.applyTheme();
|
||||
},
|
||||
applyTheme() {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = this.theme === 'dark' || (this.theme === 'system' && prefersDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
},
|
||||
setWidth(width) {
|
||||
this.pageWidth = width;
|
||||
localStorage.setItem('pageWidth', width);
|
||||
window.location.reload();
|
||||
},
|
||||
setZoom(value) {
|
||||
this.zoom = value;
|
||||
localStorage.setItem('zoom', value);
|
||||
window.location.reload();
|
||||
},
|
||||
}" class="flex max-w-2xl flex-col">
|
||||
<section class="space-y-1.5">
|
||||
<h2>Appearance</h2>
|
||||
<div>Choose how Coolify looks in this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setTheme('light')" aria-label="Use light theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'light' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
<button type="button" @click="setTheme('system')" aria-label="Use system theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'system' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>System</span>
|
||||
</button>
|
||||
<button type="button" @click="setTheme('dark')" aria-label="Use dark theme"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-left text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="theme === 'dark' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-1.5">
|
||||
<h2>Width</h2>
|
||||
<div>Choose the maximum page width for this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setWidth('center')" aria-label="Use centered width"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="pageWidth === 'center' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
Center
|
||||
</button>
|
||||
<button type="button" @click="setWidth('full')" aria-label="Use full width"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="pageWidth === 'full' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
Full
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-1.5">
|
||||
<h2>Zoom</h2>
|
||||
<div>Choose interface density for this browser.</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button type="button" @click="setZoom('100')" aria-label="Use 100 percent zoom"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="zoom === '100' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
100%
|
||||
</button>
|
||||
<button type="button" @click="setZoom('90')" aria-label="Use 90 percent zoom"
|
||||
class="flex items-center gap-2 rounded-sm border border-neutral-300 bg-white px-2 py-1 text-sm hover:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-coollabs dark:border-coolgray-300 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 dark:focus-visible:ring-warning"
|
||||
:class="zoom === '90' && 'border-coollabs text-coollabs dark:border-warning dark:text-warning'">
|
||||
<svg class="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
|
||||
</svg>
|
||||
90%
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,8 +2,7 @@
|
||||
<x-slot:title>
|
||||
Profile | Coolify
|
||||
</x-slot>
|
||||
<h1>Profile</h1>
|
||||
<div class="subtitle -mt-2">Your user profile settings.</div>
|
||||
<x-profile.navbar />
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>General</h2>
|
||||
|
||||
@@ -5,19 +5,7 @@
|
||||
</div>
|
||||
<div>Advanced configuration for your application.</div>
|
||||
<div class="flex flex-col gap-1 pt-4">
|
||||
<h3>General</h3>
|
||||
@if ($application->git_based())
|
||||
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
|
||||
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox
|
||||
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
|
||||
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
|
||||
:canResource="$application" />
|
||||
<x-forms.checkbox
|
||||
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
|
||||
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
|
||||
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
|
||||
@endif
|
||||
<h3>Build</h3>
|
||||
<x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave
|
||||
id="disableBuildCache" label="Disable Build Cache" canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox
|
||||
@@ -29,6 +17,55 @@
|
||||
instantSave id="includeSourceCommitInBuild" label="Include Source Commit in Build" canGate="update"
|
||||
:canResource="$application" />
|
||||
|
||||
<h3 class="pt-4">Container</h3>
|
||||
<x-forms.checkbox
|
||||
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
|
||||
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
|
||||
:canResource="$application" />
|
||||
@if ($isConsistentContainerNameEnabled === false)
|
||||
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
|
||||
<x-forms.input
|
||||
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
|
||||
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
|
||||
:canResource="$application" />
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if ($application->git_based())
|
||||
<h3 class="pt-4">Deployment</h3>
|
||||
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
|
||||
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox
|
||||
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
|
||||
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
|
||||
:canResource="$application" />
|
||||
<x-forms.checkbox
|
||||
helper="When enabled, anyone can trigger PR deployments. When disabled, fork PRs are blocked and only repository owners, members, and collaborators can trigger PR deployments."
|
||||
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
|
||||
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
|
||||
|
||||
<h3 class="pt-4">Git</h3>
|
||||
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
|
||||
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
|
||||
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
|
||||
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
|
||||
canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<h3 class="pt-4">Docker Compose</h3>
|
||||
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
|
||||
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
|
||||
canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" 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>."
|
||||
canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
|
||||
<h3 class="pt-4">Proxy</h3>
|
||||
@if ($application->settings->is_container_label_readonly_enabled)
|
||||
<x-forms.checkbox
|
||||
helper="Your application will be available only on https if your domain starts with https://..."
|
||||
@@ -49,45 +86,24 @@
|
||||
helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled
|
||||
instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<h3>Docker Compose</h3>
|
||||
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
|
||||
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
|
||||
canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
<h3 class="pt-4">Container Names</h3>
|
||||
<x-forms.checkbox
|
||||
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
|
||||
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
|
||||
:canResource="$application" />
|
||||
@if ($isConsistentContainerNameEnabled === false)
|
||||
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
|
||||
<x-forms.input
|
||||
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
|
||||
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
|
||||
:canResource="$application" />
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<h3 class="pt-4">Network</h3>
|
||||
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" 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>."
|
||||
canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
<h3 class="pt-4">Operations</h3>
|
||||
<form class="flex items-end gap-2" wire:submit.prevent='saveStopGracePeriod'>
|
||||
<x-forms.input
|
||||
type="number"
|
||||
id="stopGracePeriod"
|
||||
label="Stop Grace Period (seconds)"
|
||||
placeholder="{{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }}"
|
||||
helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: {{ MIN_STOP_GRACE_PERIOD_SECONDS }}-{{ MAX_STOP_GRACE_PERIOD_SECONDS }} seconds (1 hour)."
|
||||
min="{{ MIN_STOP_GRACE_PERIOD_SECONDS }}"
|
||||
max="{{ MAX_STOP_GRACE_PERIOD_SECONDS }}"
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
/>
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
<h3 class="pt-4">Logs</h3>
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave id="isLogDrainEnabled" label="Drain Logs" canGate="update" :canResource="$application" />
|
||||
@if ($application->git_based())
|
||||
<h3>Git</h3>
|
||||
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
|
||||
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
|
||||
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
|
||||
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
|
||||
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
|
||||
canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Advanced</span></a>
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm Configuration</span></a>
|
||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm</span>
|
||||
</a>
|
||||
@endif
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Environment Variables</span></a>
|
||||
@@ -26,27 +27,13 @@
|
||||
@endif
|
||||
<a class="sub-menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Servers</span>
|
||||
@if ($application->server_status == false)
|
||||
<span title="One or more servers are unreachable or misconfigured.">
|
||||
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</span>
|
||||
@elseif ($application->additional_servers()->exists() && str($application->status)->contains('degraded'))
|
||||
<span title="Application is in degraded state across multiple servers.">
|
||||
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</span>
|
||||
@endif
|
||||
<livewire:project.application.server-status-badge :application="$application" />
|
||||
</a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
<a @class(['sub-menu-item', 'menu-item-active' => str($currentRoute)->startsWith('project.application.scheduled-tasks')]) {{ wireNavigate() }}
|
||||
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Scheduled Tasks</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
|
||||
@if ($application->git_based())
|
||||
@if ($application->git_based() || $application->build_pack === 'dockerimage')
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Preview Deployments</span></a>
|
||||
@endif
|
||||
@@ -84,6 +71,8 @@
|
||||
<livewire:project.shared.destination :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.scheduled-tasks.show')
|
||||
<livewire:project.shared.scheduled-task.all :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.scheduled-tasks')
|
||||
<livewire:project.shared.scheduled-task.show />
|
||||
@elseif ($currentRoute === 'project.application.webhooks')
|
||||
<livewire:project.shared.webhooks :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.preview-deployments')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<div class="flex h-[calc(100vh-10rem)] min-h-[50rem] flex-col overflow-hidden">
|
||||
<x-slot:title>
|
||||
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
|
||||
</x-slot>
|
||||
@@ -9,6 +9,13 @@
|
||||
fullscreen: @entangle('fullscreen'),
|
||||
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
|
||||
rafId: null,
|
||||
scrollTimeout: null,
|
||||
scrollDebounce: null,
|
||||
isScrolling: false,
|
||||
destroyed: false,
|
||||
morphUpdatedCleanup: null,
|
||||
deploymentFinishedCleanup: null,
|
||||
lastTouchY: 0,
|
||||
showTimestamps: true,
|
||||
searchQuery: '',
|
||||
matchCount: 0,
|
||||
@@ -17,17 +24,76 @@
|
||||
this.fullscreen = !this.fullscreen;
|
||||
},
|
||||
scrollToBottom() {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (this.destroyed) return;
|
||||
const logsContainer = this.$root.querySelector('#logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
requestAnimationFrame(() => { this.isScrolling = false; });
|
||||
}
|
||||
},
|
||||
scheduleScroll() {
|
||||
cancelScrollLoop() {
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
this.scrollTimeout = null;
|
||||
}
|
||||
if (this.scrollDebounce) {
|
||||
clearTimeout(this.scrollDebounce);
|
||||
this.scrollDebounce = null;
|
||||
}
|
||||
},
|
||||
disableFollow() {
|
||||
if (!this.alwaysScroll) return;
|
||||
this.alwaysScroll = false;
|
||||
this.cancelScrollLoop();
|
||||
},
|
||||
handleWheel(event) {
|
||||
if (this.alwaysScroll && event.deltaY < 0) {
|
||||
this.disableFollow();
|
||||
}
|
||||
},
|
||||
handleTouchStart(event) {
|
||||
this.lastTouchY = event.touches[0].clientY;
|
||||
},
|
||||
handleTouchMove(event) {
|
||||
if (!this.alwaysScroll) return;
|
||||
const currentY = event.touches[0].clientY;
|
||||
if (currentY > this.lastTouchY) {
|
||||
this.disableFollow();
|
||||
}
|
||||
this.lastTouchY = currentY;
|
||||
},
|
||||
handleKeyScroll(event) {
|
||||
if (!this.alwaysScroll) return;
|
||||
const upKeys = ['ArrowUp', 'PageUp', 'Home'];
|
||||
if (upKeys.includes(event.key)) {
|
||||
this.disableFollow();
|
||||
}
|
||||
},
|
||||
handleScroll(event) {
|
||||
if (this.isScrolling || this.destroyed) return;
|
||||
const el = event.target;
|
||||
clearTimeout(this.scrollDebounce);
|
||||
this.scrollDebounce = setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (!this.alwaysScroll && distanceFromBottom <= 10) {
|
||||
this.alwaysScroll = true;
|
||||
this.scheduleScroll();
|
||||
}
|
||||
}, 150);
|
||||
},
|
||||
scheduleScroll() {
|
||||
if (!this.alwaysScroll || this.destroyed) return;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
if (!this.alwaysScroll || this.destroyed) return;
|
||||
this.scrollToBottom();
|
||||
if (this.alwaysScroll) {
|
||||
setTimeout(() => this.scheduleScroll(), 250);
|
||||
if (this.alwaysScroll && !this.destroyed) {
|
||||
this.scrollTimeout = setTimeout(() => this.scheduleScroll(), 250);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -36,10 +102,7 @@
|
||||
if (this.alwaysScroll) {
|
||||
this.scheduleScroll();
|
||||
} else {
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
this.cancelScrollLoop();
|
||||
}
|
||||
},
|
||||
hasActiveLogSelection() {
|
||||
@@ -107,9 +170,9 @@
|
||||
|
||||
this.matchCount = query ? count : 0;
|
||||
},
|
||||
downloadLogs() {
|
||||
collectVisibleLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
if (!logs) return '';
|
||||
const visibleLines = logs.querySelectorAll('[data-log-line]:not(.hidden)');
|
||||
let content = '';
|
||||
visibleLines.forEach(line => {
|
||||
@@ -118,6 +181,17 @@
|
||||
content += text + String.fromCharCode(10);
|
||||
}
|
||||
});
|
||||
return content;
|
||||
},
|
||||
copyLogs() {
|
||||
const content = this.collectVisibleLogs();
|
||||
if (!content) return;
|
||||
navigator.clipboard.writeText(content);
|
||||
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
||||
},
|
||||
downloadLogs() {
|
||||
const content = this.collectVisibleLogs();
|
||||
if (!content) return;
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -130,10 +204,7 @@
|
||||
stopScroll() {
|
||||
this.scrollToBottom();
|
||||
this.alwaysScroll = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
this.cancelScrollLoop();
|
||||
},
|
||||
init() {
|
||||
// Watch search query changes
|
||||
@@ -141,21 +212,26 @@
|
||||
this.applySearch();
|
||||
});
|
||||
|
||||
// Apply search after Livewire updates
|
||||
Livewire.hook('morph.updated', ({ el }) => {
|
||||
if (el.id === 'logs') {
|
||||
this.$nextTick(() => {
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Apply search after Livewire updates.
|
||||
// Livewire.hook() returns an unregister fn; keep it for destroy().
|
||||
this.morphUpdatedCleanup = Livewire.hook('morph.updated', ({ el }) => {
|
||||
if (this.destroyed) return;
|
||||
if (el.id !== 'logs' || !this.$root.contains(el)) return;
|
||||
this.$nextTick(() => {
|
||||
if (this.destroyed) return;
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Stop auto-scroll when deployment finishes
|
||||
Livewire.on('deploymentFinished', () => {
|
||||
// Stop auto-scroll when deployment finishes.
|
||||
// Livewire.on() returns an unregister fn; keep it for destroy().
|
||||
this.deploymentFinishedCleanup = Livewire.on('deploymentFinished', () => {
|
||||
if (this.destroyed) return;
|
||||
setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
this.stopScroll();
|
||||
}, 500);
|
||||
});
|
||||
@@ -164,14 +240,32 @@
|
||||
if (this.alwaysScroll) {
|
||||
this.scheduleScroll();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
// Runs when Alpine tears the component down (wire:navigate away).
|
||||
this.destroyed = true;
|
||||
this.alwaysScroll = false;
|
||||
this.cancelScrollLoop();
|
||||
if (this.scrollDebounce) {
|
||||
clearTimeout(this.scrollDebounce);
|
||||
this.scrollDebounce = null;
|
||||
}
|
||||
if (typeof this.morphUpdatedCleanup === 'function') {
|
||||
this.morphUpdatedCleanup();
|
||||
this.morphUpdatedCleanup = null;
|
||||
}
|
||||
if (typeof this.deploymentFinishedCleanup === 'function') {
|
||||
this.deploymentFinishedCleanup();
|
||||
this.deploymentFinishedCleanup = null;
|
||||
}
|
||||
}
|
||||
}">
|
||||
}" class="flex flex-1 min-h-0 flex-col overflow-hidden">
|
||||
<livewire:project.application.deployment-navbar
|
||||
:application_deployment_queue="$application_deployment_queue" />
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 relative'">
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 flex flex-1 min-h-0 flex-col overflow-hidden'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
|
||||
class="flex min-h-0 flex-col w-full overflow-hidden bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
|
||||
:class="fullscreen ? 'h-full' : 'flex-1 border border-dotted rounded-sm'">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -210,12 +304,7 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
x-on:click="
|
||||
$wire.copyLogs().then(logs => {
|
||||
navigator.clipboard.writeText(logs);
|
||||
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
||||
});
|
||||
"
|
||||
x-on:click="copyLogs()"
|
||||
title="Copy Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
@@ -327,10 +416,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logsContainer"
|
||||
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
|
||||
<div id="logs" class="flex flex-col font-mono">
|
||||
<div id="logsContainer" @scroll="handleScroll" @wheel="handleWheel"
|
||||
@touchstart="handleTouchStart" @touchmove="handleTouchMove" @keydown="handleKeyScroll" tabindex="0"
|
||||
class="flex min-h-40 flex-1 flex-col overflow-y-auto p-2 px-4 scrollbar">
|
||||
<div id="logs" class="flex flex-col font-logs">
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
@@ -356,11 +445,11 @@
|
||||
])>{{ $lineContent }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
|
||||
<span class="font-logs text-neutral-400 mb-2">No logs yet.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,6 +12,9 @@
|
||||
<div>{{ $application->compose_parsing_version }}</div>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$application" />
|
||||
</x-modal-input>
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<x-forms.button canGate="update" :canResource="$application" wire:target='initLoadingCompose'
|
||||
x-on:click="$wire.dispatch('loadCompose', false)">
|
||||
@@ -32,6 +35,7 @@
|
||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="buildPack" label="Build Pack"
|
||||
required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
@@ -60,7 +64,7 @@
|
||||
@if (!isDatabaseImage(data_get($service, 'image')))
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
|
||||
label="Domains for {{ $serviceName }}"
|
||||
id="parsedServiceDomains.{{ str($serviceName)->replace('-', '_')->replace('.', '_') }}.domain"
|
||||
x-bind:disabled="shouldDisable()"></x-forms.input>
|
||||
@@ -114,7 +118,7 @@
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@can('update', $application)
|
||||
<x-forms.button wire:click="getWildcardDomain">Generate Domain
|
||||
@@ -226,20 +230,24 @@
|
||||
id="customDockerRunOptions" label="Custom Docker Options" x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($buildPack === 'nixpacks')
|
||||
@if ($buildPack === 'nixpacks' || $buildPack === 'railpack')
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
|
||||
id="installCommand" label="Install Command" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
|
||||
id="buildCommand" label="Build Command" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
|
||||
id="startCommand" label="Start Command" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
|
||||
automatically.
|
||||
@if ($buildPack === 'nixpacks')
|
||||
<div class="pt-1 text-xs">
|
||||
|
||||
<span class="font-medium">Nixpacks</span>
|
||||
will detect the required configuration automatically.
|
||||
<a class="underline" href="https://coolify.io/docs/applications/">Framework
|
||||
Specific Docs</a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@endif
|
||||
@@ -276,7 +284,7 @@
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($baseDirectory . $dockerComposeLocation, '/') }}</span>"
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
|
||||
label="Preserve Repository During Deployment"
|
||||
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
||||
@@ -382,7 +390,7 @@
|
||||
x-bind:disabled="!canUpdate" />
|
||||
|
||||
@if ($buildPack !== 'dockercompose')
|
||||
<div class="pt-2 w-96">
|
||||
<div class="pt-2 w-full sm:w-96">
|
||||
<x-forms.checkbox
|
||||
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
instantSave id="isBuildServerEnabled" label="Use a Build Server?"
|
||||
@@ -422,7 +430,7 @@
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="isContainerLabelEscapeEnabled" instantSave
|
||||
@@ -528,7 +536,7 @@
|
||||
|
||||
<h3 class="pt-8">HTTP Basic Authentication</h3>
|
||||
<div>
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||
label="Enable" id="isHttpBasicAuthEnabled" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@@ -550,7 +558,7 @@
|
||||
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"
|
||||
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="isContainerLabelReadonlyEnabled" instantSave
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.deployment.index', $parameters) }}">
|
||||
Deployments
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.logs', $parameters) }}">
|
||||
<div class="flex items-center gap-1">
|
||||
Logs
|
||||
@@ -23,100 +24,160 @@
|
||||
</a>
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.command', $parameters) }}">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
<x-applications.links :application="$application" />
|
||||
<div class="shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
|
||||
<div>Please load a Compose file.</div>
|
||||
@else
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div>
|
||||
<x-applications.advanced :application="$application" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Redeploy
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Update Service
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='restart'>
|
||||
Restart
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Deploy
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
|
||||
@if ($application->status === 'running')
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='force_deploy_without_cache'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@else
|
||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div>
|
||||
<x-applications.advanced :application="$application" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,20 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($application->build_pack === 'dockerimage')
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<h3>Manual Preview Deployment</h3>
|
||||
<form wire:submit.prevent="addDockerImagePreview" class="flex flex-col gap-2 xl:flex-row xl:items-end">
|
||||
<x-forms.input id="manualPullRequestId" label="Pull Request Id"
|
||||
helper="Used as the preview identifier for naming, domains, logs, and cleanup." />
|
||||
<x-forms.input id="manualDockerTag" label="Docker Tag"
|
||||
helper="The image tag to deploy for this preview, for example pr_1234." />
|
||||
@can('deploy', $application)
|
||||
<x-forms.button type="submit">Deploy Preview</x-forms.button>
|
||||
@endcan
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->previews->count() > 0)
|
||||
<h3 class="py-4">Deployments</h3>
|
||||
<div class="flex flex-wrap w-full gap-4">
|
||||
@@ -87,11 +101,13 @@
|
||||
<x-external-link />
|
||||
</a>
|
||||
@endif
|
||||
|
|
||||
<a target="_blank" href="{{ data_get($preview, 'pull_request_html_url') }}">Open
|
||||
PR on Git
|
||||
<x-external-link />
|
||||
</a>
|
||||
@if (filled(data_get($preview, 'pull_request_html_url')))
|
||||
|
|
||||
<a target="_blank" href="{{ data_get($preview, 'pull_request_html_url') }}">Open
|
||||
PR on Git
|
||||
<x-external-link />
|
||||
</a>
|
||||
@endif
|
||||
@if (count($parameters) > 0)
|
||||
|
|
||||
<a {{ wireNavigate() }}
|
||||
@@ -131,6 +147,10 @@
|
||||
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
|
||||
<x-forms.input label="Domain" helper="One domain per preview."
|
||||
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
|
||||
@if ($application->build_pack === 'dockerimage')
|
||||
<x-forms.input label="Docker Tag" helper="The image tag used for this preview deployment."
|
||||
id="previewDockerTags.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
|
||||
@endif
|
||||
@can('update', $application)
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
|
||||
@@ -157,7 +177,8 @@
|
||||
Force deploy (without
|
||||
cache)
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
|
||||
<x-forms.button
|
||||
wire:click="deploy({{ data_get($preview, 'pull_request_id') }}, null, false, '{{ data_get($preview, 'docker_registry_image_tag') }}')">
|
||||
@if (data_get($preview, 'status') === 'exited')
|
||||
<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"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<span>
|
||||
@if ($application->server_status === false)
|
||||
<span title="One or more servers are unreachable or misconfigured.">
|
||||
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</span>
|
||||
@elseif ($application->additional_servers()->exists() && str($application->status)->contains('degraded'))
|
||||
<span title="Application is in degraded state across multiple servers.">
|
||||
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
@@ -2,6 +2,7 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Swarm Configuration</h2>
|
||||
<x-deprecated-badge />
|
||||
@can('update', $application)
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
@@ -13,6 +14,9 @@
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
</div>
|
||||
<x-callout type="warning" title="Deprecated" class="my-4">
|
||||
{{ config('deprecations.swarm') }}
|
||||
</x-callout>
|
||||
<div class="flex flex-col gap-2 py-4">
|
||||
<div class="flex flex-col items-end gap-2 xl:flex-row">
|
||||
<x-forms.input id="swarmReplicas" label="Replicas" required canGate="update" :canResource="$application" />
|
||||
|
||||
@@ -81,10 +81,10 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Frequency" id="frequency" />
|
||||
<x-forms.input label="Frequency" id="frequency" required />
|
||||
<x-forms.input label="Timezone" id="timezone" disabled
|
||||
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" />
|
||||
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds." />
|
||||
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
|
||||
<x-forms.input label="Timeout" id="timeout" type="number" min="60" helper="The timeout of the backup job in seconds." required />
|
||||
</div>
|
||||
|
||||
<h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3>
|
||||
@@ -101,13 +101,13 @@
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
|
||||
type="number" min="0"
|
||||
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." />
|
||||
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
|
||||
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysLocally" type="number"
|
||||
min="0"
|
||||
helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." />
|
||||
helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required />
|
||||
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageLocally"
|
||||
type="number" min="0"
|
||||
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." />
|
||||
type="number" min="0" step="any"
|
||||
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,13 +117,13 @@
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
|
||||
type="number" min="0"
|
||||
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." />
|
||||
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
|
||||
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysS3" type="number"
|
||||
min="0"
|
||||
helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." />
|
||||
helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required />
|
||||
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageS3"
|
||||
type="number" min="0"
|
||||
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." />
|
||||
type="number" min="0" step="any"
|
||||
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -41,20 +44,8 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="Clickhouse URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||
@if ($dbUrlPublic)
|
||||
<x-forms.input label="Clickhouse URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.input label="Clickhouse URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
readonly value="Starting the database will generate this." canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<livewire:project.database.clickhouse.status-info :database="$database" />
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<div class="flex items-center">
|
||||
@@ -76,11 +67,12 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available" canGate="update"
|
||||
:canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="w-64">
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
@endcan
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Healthcheck</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Resource Limits</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
@@ -57,6 +59,8 @@
|
||||
<livewire:project.shared.destination :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.persistent-storage')
|
||||
<livewire:project.service.storage :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.healthcheck')
|
||||
<livewire:project.database.health :database="$database" />
|
||||
@elseif ($currentRoute === 'project.database.import-backup')
|
||||
<livewire:project.database.import :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.webhooks')
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -37,60 +40,8 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="Dragonfly URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||
|
||||
@if ($dbUrlPublic)
|
||||
<x-forms.input label="Dragonfly URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.input label="Dragonfly URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
readonly value="Starting the database will generate this." canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||
instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||
instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.dragonfly.status-info :database="$database" />
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
@@ -113,14 +64,15 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available" canGate="update"
|
||||
:canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="w-64">
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
canGate="update" :canResource="$database" />
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" canGate="update"
|
||||
:canResource="$database" />
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
</x-slide-over>
|
||||
<div class="navbar-main">
|
||||
<nav
|
||||
class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
|
||||
<a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.database.logs', $parameters) }}">
|
||||
Logs
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.database.command', $parameters) }}">
|
||||
Terminal
|
||||
</a>
|
||||
@@ -31,7 +31,7 @@
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
|
||||
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.backup.index', $parameters) }}">
|
||||
Backups
|
||||
</a>
|
||||
@@ -39,57 +39,97 @@
|
||||
</nav>
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:button-title>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch">
|
||||
Restart
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Start
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:button-title>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Start
|
||||
</button>
|
||||
@endif
|
||||
Start
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('startEvent', () => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Healthcheck</h2>
|
||||
<x-forms.button canGate="update" :canResource="$database" type="submit">Save</x-forms.button>
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this database.']"
|
||||
warningMessage="If the health check fails, this database will be marked unhealthy. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
||||
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
|
||||
isHighlightedButton>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button canGate="update" :canResource="$database" wire:click="toggleHealthcheck">Disable Healthcheck</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1 pb-4">Define how your resource's health should be checked.</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-callout type="warning" title="Healthcheck disabled">
|
||||
<p>Docker runs no healthcheck probe for this database and Coolify can no longer report a healthy/unhealthy state.</p>
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckInterval"
|
||||
placeholder="15" label="Interval (s)" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckTimeout"
|
||||
placeholder="5" label="Timeout (s)" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckRetries"
|
||||
placeholder="5" label="Retries" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="0" type="number"
|
||||
id="healthCheckStartPeriod" placeholder="5" label="Start Period (s)" required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,228 @@
|
||||
<div x-data="{
|
||||
error: $wire.entangle('error'),
|
||||
filesize: $wire.entangle('filesize'),
|
||||
filename: $wire.entangle('filename'),
|
||||
isUploading: $wire.entangle('isUploading'),
|
||||
progress: $wire.entangle('progress'),
|
||||
s3FileSize: $wire.entangle('s3FileSize'),
|
||||
s3StorageId: $wire.entangle('s3StorageId'),
|
||||
s3Path: $wire.entangle('s3Path'),
|
||||
restoreType: null
|
||||
}">
|
||||
<script type="text/javascript" src="{{ URL::asset('js/dropzone.js') }}"></script>
|
||||
@script
|
||||
<script data-navigate-once>
|
||||
Dropzone.options.myDropzone = {
|
||||
chunking: true,
|
||||
method: "POST",
|
||||
maxFilesize: 1000000000,
|
||||
chunkSize: 10000000,
|
||||
createImageThumbnails: false,
|
||||
disablePreviews: true,
|
||||
parallelChunkUploads: false,
|
||||
init: function () {
|
||||
let button = this.element.querySelector('button');
|
||||
button.innerText = 'Select or drop a backup file here.'
|
||||
this.on('sending', function (file, xhr, formData) {
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
formData.append("_token", token);
|
||||
});
|
||||
this.on("addedfile", file => {
|
||||
$wire.isUploading = true;
|
||||
$wire.customLocation = '';
|
||||
});
|
||||
this.on('uploadprogress', function (file, progress, bytesSent) {
|
||||
$wire.progress = progress;
|
||||
});
|
||||
this.on('complete', function (file) {
|
||||
$wire.filename = file.name;
|
||||
$wire.filesize = Number(file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
$wire.isUploading = false;
|
||||
});
|
||||
this.on('error', function (file, message) {
|
||||
$wire.error = true;
|
||||
$wire.$dispatch('error', message.error)
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endscript
|
||||
<div class="pt-2 rounded-sm alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>This is a destructive action, existing data will be replaced!</span>
|
||||
</div>
|
||||
{{-- Restore Command Configuration --}}
|
||||
@if ($resourceDbType === 'standalone-postgresql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="6" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='postgresqlRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
|
||||
<div class="flex flex-col gap-1 pt-1">
|
||||
<span class="text-xs">You can add "--clean" to drop objects before creating them, avoiding
|
||||
conflicts.</span>
|
||||
<span class="text-xs">You can add "--verbose" to log more things.</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mysql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mariadb')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText' canGate="update" :canResource="$this->resource"></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand' canGate="update" :canResource="$this->resource"></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll' canGate="update" :canResource="$this->resource"></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Restore Type Selection Boxes --}}
|
||||
<h3 class="pt-6">Choose Restore Method</h3>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<div @click="restoreType = 'file'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 'file' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from File</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Upload a backup file or specify a file path on the server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($availableS3Storages) > 0)
|
||||
<div @click="restoreType = 's3'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 's3' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from S3</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Download and restore a backup from S3 storage</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- File Restore Section --}}
|
||||
@can('update', $this->resource)
|
||||
<div x-show="restoreType === 'file'" class="pt-6">
|
||||
<h3>Backup File</h3>
|
||||
<form class="flex gap-2 items-end pt-2">
|
||||
<x-forms.input label="Location of the backup file on the server" placeholder="e.g. /home/user/backup.sql.gz"
|
||||
wire:model='customLocation' x-model="$wire.customLocation" canGate="update" :canResource="$this->resource"></x-forms.input>
|
||||
<x-forms.button class="w-full" wire:click='checkFile' x-bind:disabled="!$wire.customLocation" canGate="update" :canResource="$this->resource">Check File</x-forms.button>
|
||||
</form>
|
||||
<div class="pt-2 text-center text-xl font-bold">
|
||||
Or
|
||||
</div>
|
||||
<form action="{{ route('upload.backup', ['databaseUuid' => $resourceUuid]) }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
<div x-show="isUploading">
|
||||
<progress max="100" x-bind:value="progress" class="progress progress-warning"></progress>
|
||||
</div>
|
||||
|
||||
<div x-show="filename && !error" class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: <span x-text="filename ?? 'N/A'"></span><span x-show="filesize" x-text="' / ' + filesize"></span></div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from File?" buttonTitle="Restore from File"
|
||||
submitAction="runImport" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from File
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Copy backup file to database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
{{-- S3 Restore Section --}}
|
||||
@if (count($availableS3Storages) > 0)
|
||||
@can('update', $this->resource)
|
||||
<div x-show="restoreType === 's3'" class="pt-6">
|
||||
<h3>Restore from S3</h3>
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<x-forms.select label="S3 Storage" wire:model.live="s3StorageId" canGate="update" :canResource="$this->resource">
|
||||
<option value="">Select S3 Storage</option>
|
||||
@foreach ($availableS3Storages as $storage)
|
||||
<option value="{{ $storage['id'] }}">{{ $storage['name'] }}
|
||||
@if ($storage['description'])
|
||||
- {{ $storage['description'] }}
|
||||
@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
|
||||
<x-forms.input label="S3 File Path (within bucket)"
|
||||
helper="Path to the backup file in your S3 bucket, e.g., /backups/database-2025-01-15.gz"
|
||||
placeholder="/backups/database-backup.gz" wire:model.blur='s3Path'
|
||||
wire:keydown.enter='checkS3File' canGate="update" :canResource="$this->resource"></x-forms.input>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button class="w-full" wire:click='checkS3File' x-bind:disabled="!s3StorageId || !s3Path" canGate="update" :canResource="$this->resource">
|
||||
Check File
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($s3FileSize)
|
||||
<div class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: {{ $s3Path }} {{ formatBytes($s3FileSize ?? 0) }}</div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from S3?" buttonTitle="Restore from S3"
|
||||
submitAction="restoreFromS3" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from S3
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Download backup from S3 storage</li>
|
||||
<li>Copy file into database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
{{-- Slide-over for activity monitor (all restore operations) --}}
|
||||
<x-slide-over @databaserestore.window="slideOverOpen = true" closeWithX fullScreen>
|
||||
<x-slot:title>Database Restore Output</x-slot:title>
|
||||
<x-slot:content>
|
||||
<div wire:ignore>
|
||||
<livewire:activity-monitor wire:key="database-restore-{{ $resourceUuid }}" header="Logs" fullHeight />
|
||||
</div>
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
</div>
|
||||
@@ -1,237 +1,10 @@
|
||||
<div x-data="{
|
||||
error: $wire.entangle('error'),
|
||||
filesize: $wire.entangle('filesize'),
|
||||
filename: $wire.entangle('filename'),
|
||||
isUploading: $wire.entangle('isUploading'),
|
||||
progress: $wire.entangle('progress'),
|
||||
s3FileSize: $wire.entangle('s3FileSize'),
|
||||
s3StorageId: $wire.entangle('s3StorageId'),
|
||||
s3Path: $wire.entangle('s3Path'),
|
||||
restoreType: null
|
||||
}">
|
||||
<script type="text/javascript" src="{{ URL::asset('js/dropzone.js') }}"></script>
|
||||
@script
|
||||
<script data-navigate-once>
|
||||
Dropzone.options.myDropzone = {
|
||||
chunking: true,
|
||||
method: "POST",
|
||||
maxFilesize: 1000000000,
|
||||
chunkSize: 10000000,
|
||||
createImageThumbnails: false,
|
||||
disablePreviews: true,
|
||||
parallelChunkUploads: false,
|
||||
init: function () {
|
||||
let button = this.element.querySelector('button');
|
||||
button.innerText = 'Select or drop a backup file here.'
|
||||
this.on('sending', function (file, xhr, formData) {
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
formData.append("_token", token);
|
||||
});
|
||||
this.on("addedfile", file => {
|
||||
$wire.isUploading = true;
|
||||
$wire.customLocation = '';
|
||||
});
|
||||
this.on('uploadprogress', function (file, progress, bytesSent) {
|
||||
$wire.progress = progress;
|
||||
});
|
||||
this.on('complete', function (file) {
|
||||
$wire.filename = file.name;
|
||||
$wire.filesize = Number(file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
$wire.isUploading = false;
|
||||
});
|
||||
this.on('error', function (file, message) {
|
||||
$wire.error = true;
|
||||
$wire.$dispatch('error', message.error)
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endscript
|
||||
<div>
|
||||
<h2>Import Backup</h2>
|
||||
@if ($unsupported)
|
||||
<div>Database restore is not supported.</div>
|
||||
@elseif (str($resourceStatus)->startsWith('running'))
|
||||
<livewire:project.database.import-form wire:key="database-import-form-{{ $resourceUuid }}" />
|
||||
@else
|
||||
<div class="pt-2 rounded-sm alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>This is a destructive action, existing data will be replaced!</span>
|
||||
</div>
|
||||
@if (str($resourceStatus)->startsWith('running'))
|
||||
{{-- Restore Command Configuration --}}
|
||||
@if ($resourceDbType === 'standalone-postgresql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="6" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='postgresqlRestoreCommand'></x-forms.input>
|
||||
<div class="flex flex-col gap-1 pt-1">
|
||||
<span class="text-xs">You can add "--clean" to drop objects before creating them, avoiding
|
||||
conflicts.</span>
|
||||
<span class="text-xs">You can add "--verbose" to log more things.</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mysql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resourceDbType === 'standalone-mariadb')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Restore Type Selection Boxes --}}
|
||||
<h3 class="pt-6">Choose Restore Method</h3>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<div @click="restoreType = 'file'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 'file' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from File</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Upload a backup file or specify a file path on the server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($availableS3Storages) > 0)
|
||||
<div @click="restoreType = 's3'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 's3' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from S3</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Download and restore a backup from S3 storage</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- File Restore Section --}}
|
||||
@can('update', $this->resource)
|
||||
<div x-show="restoreType === 'file'" class="pt-6">
|
||||
<h3>Backup File</h3>
|
||||
<form class="flex gap-2 items-end pt-2">
|
||||
<x-forms.input label="Location of the backup file on the server" placeholder="e.g. /home/user/backup.sql.gz"
|
||||
wire:model='customLocation' x-model="$wire.customLocation"></x-forms.input>
|
||||
<x-forms.button class="w-full" wire:click='checkFile' x-bind:disabled="!$wire.customLocation">Check File</x-forms.button>
|
||||
</form>
|
||||
<div class="pt-2 text-center text-xl font-bold">
|
||||
Or
|
||||
</div>
|
||||
<form action="/upload/backup/{{ $resourceUuid }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
<div x-show="isUploading">
|
||||
<progress max="100" x-bind:value="progress" class="progress progress-warning"></progress>
|
||||
</div>
|
||||
|
||||
<div x-show="filename && !error" class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: <span x-text="filename ?? 'N/A'"></span><span x-show="filesize" x-text="' / ' + filesize"></span></div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from File?" buttonTitle="Restore from File"
|
||||
submitAction="runImport" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from File
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Copy backup file to database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
{{-- S3 Restore Section --}}
|
||||
@if (count($availableS3Storages) > 0)
|
||||
@can('update', $this->resource)
|
||||
<div x-show="restoreType === 's3'" class="pt-6">
|
||||
<h3>Restore from S3</h3>
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<x-forms.select label="S3 Storage" wire:model.live="s3StorageId">
|
||||
<option value="">Select S3 Storage</option>
|
||||
@foreach ($availableS3Storages as $storage)
|
||||
<option value="{{ $storage['id'] }}">{{ $storage['name'] }}
|
||||
@if ($storage['description'])
|
||||
- {{ $storage['description'] }}
|
||||
@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
|
||||
<x-forms.input label="S3 File Path (within bucket)"
|
||||
helper="Path to the backup file in your S3 bucket, e.g., /backups/database-2025-01-15.gz"
|
||||
placeholder="/backups/database-backup.gz" wire:model.blur='s3Path'
|
||||
wire:keydown.enter='checkS3File'></x-forms.input>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button class="w-full" wire:click='checkS3File' x-bind:disabled="!s3StorageId || !s3Path">
|
||||
Check File
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($s3FileSize)
|
||||
<div class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: {{ $s3Path }} {{ formatBytes($s3FileSize ?? 0) }}</div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from S3?" buttonTitle="Restore from S3"
|
||||
submitAction="restoreFromS3" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from S3
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Download backup from S3 storage</li>
|
||||
<li>Copy file into database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
{{-- Slide-over for activity monitor (all restore operations) --}}
|
||||
<x-slide-over @databaserestore.window="slideOverOpen = true" closeWithX fullScreen>
|
||||
<x-slot:title>Database Restore Output</x-slot:title>
|
||||
<x-slot:content>
|
||||
<div wire:ignore>
|
||||
<livewire:activity-monitor wire:key="database-restore-{{ $resourceUuid }}" header="Logs" fullHeight />
|
||||
</div>
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
@else
|
||||
<div>Database must be running to restore a backup.</div>
|
||||
@endif
|
||||
<div>Database must be running to restore a backup.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -38,59 +41,8 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="KeyDB URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||
@if ($dbUrlPublic)
|
||||
<x-forms.input label="KeyDB URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.input label="KeyDB URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
readonly value="Starting the database will generate this." canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($database->enable_ssl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||
instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enable_ssl" label="Enable SSL" wire:model.live="enable_ssl"
|
||||
instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.keydb.status-info :database="$database" />
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
@@ -113,14 +65,15 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available" canGate="update"
|
||||
:canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}" id="publicPort" label="Public Port"
|
||||
canGate="update" :canResource="$database" />
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
<x-forms.textarea
|
||||
helper="<a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/Snapchat/KeyDB/unstable/keydb.conf'>KeyDB Default Configuration</a>"
|
||||
label="Custom KeyDB Configuration" rows="10" id="keydbConf" canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
<div class="w-64">
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -61,59 +64,9 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="MariaDB URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url" canGate="update" :canResource="$database" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="MariaDB URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url_public" canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
||||
:canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.mariadb.status-info :database="$database" />
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
@@ -137,10 +90,12 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}"
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea label="Custom MariaDB Configuration" rows="10" id="mariadbConf"
|
||||
canGate="update" :canResource="$database" />
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -50,85 +53,10 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="Mongo URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url" canGate="update" :canResource="$database" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Mongo URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url_public" canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<livewire:project.database.mongodb.status-info :database="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
||||
:canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($enableSsl)
|
||||
<div class="mx-2">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.select id="sslMode" label="SSL Mode" wire:model.live="sslMode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for MongoDB connections" canGate="update"
|
||||
:canResource="$database">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL"
|
||||
disabled helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
@@ -151,10 +79,12 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}"
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea label="Custom MongoDB Configuration" rows="10" id="mongoConf"
|
||||
canGate="update" :canResource="$database" />
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -56,81 +59,9 @@
|
||||
<x-forms.input placeholder="3000:5432" id="portsMappings" label="Ports Mappings"
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433" canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="MySQL URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="MySQL URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($enableSsl)
|
||||
<div class="mx-2">
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.select id="sslMode" label="SSL Mode" wire:model.live="sslMode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for MySQL connections" canGate="update" :canResource="$database">
|
||||
<option value="PREFERRED" title="Prefer secure connections">Prefer (secure)</option>
|
||||
<option value="REQUIRED" title="Require secure connections">Require (secure)</option>
|
||||
<option value="VERIFY_CA" title="Verify CA certificate">Verify CA (secure)</option>
|
||||
<option value="VERIFY_IDENTITY" title="Verify full certificate">Verify Full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL"
|
||||
disabled helper="Database should be stopped to change this settings.">
|
||||
<option value="PREFERRED" title="Prefer secure connections">Prefer (secure)</option>
|
||||
<option value="REQUIRED" title="Require secure connections">Require (secure)</option>
|
||||
<option value="VERIFY_CA" title="Verify CA certificate">Verify CA (secure)</option>
|
||||
<option value="VERIFY_IDENTITY" title="Verify full certificate">Verify Full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.mysql.status-info :database="$database" />
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
@@ -153,10 +84,12 @@
|
||||
</div>
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available" canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}"
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea label="Custom Mysql Configuration" rows="10" id="mysqlConf" canGate="update" :canResource="$database" />
|
||||
<h3 class="pt-4">Advanced</h3>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -68,112 +71,38 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
|
||||
<x-forms.input label="Postgres URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Postgres URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
<livewire:project.database.postgresql.status-info :database="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates" buttonTitle="Regenerate SSL Certificates"
|
||||
:actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]" submitAction="regenerateSslCertificate" :confirmWithText="false"
|
||||
:confirmWithPassword="false" />
|
||||
<h3>Proxy</h3>
|
||||
<x-loading wire:loading wire:target="instantSave" />
|
||||
@if (data_get($database, 'is_public'))
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 w-64">
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}" id="publicPort"
|
||||
label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64" wire:key='enable_ssl'>
|
||||
@if ($database->isExited())
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($enableSsl)
|
||||
<div class="mx-2">
|
||||
@if ($database->isExited())
|
||||
<x-forms.select id="sslMode" label="SSL Mode" wire:model.live="sslMode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for PostgreSQL connections" canGate="update"
|
||||
:canResource="$database">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-ca" title="Verify CA certificate">verify-ca (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings.">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
<option value="verify-ca" title="Verify CA certificate">verify-ca (secure)</option>
|
||||
<option value="verify-full" title="Verify full certificate">verify-full (secure)
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<h3>Proxy</h3>
|
||||
<x-loading wire:loading wire:target="instantSave" />
|
||||
@if (data_get($database, 'is_public'))
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$database"
|
||||
container="{{ data_get($database, 'uuid') }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button disabled="{{ !data_get($database, 'is_public') }}"
|
||||
@click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-64">
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}" id="publicPort"
|
||||
label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="postgresConf"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="postgresConf"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<x-forms.button type="submit" canGate="update" :canResource="$database">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$database" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" canGate="update" :canResource="$database" />
|
||||
@@ -60,56 +63,8 @@
|
||||
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold dark:text-warning'>Example</span>3000:5432,3002:5433"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input label="Redis URL (internal)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" />
|
||||
@if ($dbUrlPublic)
|
||||
<x-forms.input label="Redis URL (public)"
|
||||
helper="If you change the user/password/port, this could be different. This is with the default values."
|
||||
type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h3>SSL Configuration</h3>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<x-modal-confirmation title="Regenerate SSL Certificates"
|
||||
buttonTitle="Regenerate SSL Certificates" :actions="[
|
||||
'The SSL certificate of this database will be regenerated.',
|
||||
'You must restart the database after regenerating the certificate to start using the new certificate.',
|
||||
]"
|
||||
submitAction="regenerateSslCertificate" :confirmWithText="false" :confirmWithPassword="false" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($enableSsl && $certificateValidUntil)
|
||||
<span class="text-sm">Valid until:
|
||||
@if (now()->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired</span>
|
||||
@elseif(now()->addDays(30)->gt($certificateValidUntil))
|
||||
<span class="text-red-500">{{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expiring
|
||||
soon</span>
|
||||
@else
|
||||
<span>{{ $certificateValidUntil->format('d.m.Y H:i:s') }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64" wire:key='enable_ssl'>
|
||||
@if (str($database->status)->contains('exited'))
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
||||
:canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." canGate="update"
|
||||
:canResource="$database" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.database.redis.status-info :database="$database" />
|
||||
<div>
|
||||
<div class="flex flex-col py-2 w-64">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
@@ -132,10 +87,12 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input type="number" placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input type="number" placeholder="3600" disabled="{{ $isPublic }}" id="publicPortTimeout"
|
||||
label="Proxy Timeout (seconds)" helper="Timeout for the public TCP proxy connection in seconds. Default: 3600 (1 hour)." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.textarea placeholder="# maxmemory 256mb
|
||||
# maxmemory-policy allkeys-lru
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<div>
|
||||
<x-database-status-info :database="$database" :label="$label" :db-url="$dbUrl" :db-url-public="$dbUrlPublic"
|
||||
:enable-ssl="$enableSsl" :ssl-mode="$sslMode" :certificate-valid-until="$certificateValidUntil"
|
||||
:supports-ssl="$supportsSsl" :ssl-mode-options="$sslModeOptions" :ssl-mode-helper="$sslModeHelper"
|
||||
:show-public-url-placeholder="$showPublicUrlPlaceholder" :is-exited="$isExited" />
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<x-slot:title>
|
||||
Projects | Coolify
|
||||
</x-slot>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<h1>Projects</h1>
|
||||
@can('createAnyResource')
|
||||
<x-modal-input buttonTitle="+ Add" title="New Project">
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<x-forms.input id="branch" required label="Branch" />
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
<livewire:source.github.create />
|
||||
</x-modal-input>
|
||||
@if ($repositories->count() > 0)
|
||||
<a target="_blank" class="flex hover:no-underline" href="{{ getInstallationPath($github_app) }}">
|
||||
<x-forms.button>
|
||||
Change Repositories on GitHub
|
||||
<x-external-link />
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click.prevent="loadRepositories({{ $github_app->id }})">
|
||||
Refresh Repository List
|
||||
</x-forms.button>
|
||||
<a target="_blank" class="inline-flex items-center self-center gap-1 text-sm hover:underline dark:text-neutral-400"
|
||||
href="{{ getInstallationPath($github_app) }}">
|
||||
Change Repositories on GitHub
|
||||
<x-external-link />
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@@ -51,7 +53,10 @@
|
||||
@endforeach
|
||||
</x-forms.datalist>
|
||||
</div>
|
||||
<x-forms.button wire:click.prevent="loadBranches"> Load Repository </x-forms.button>
|
||||
<x-forms.button :showLoadingIndicator="false" wire:click.prevent="loadBranches" wire:target="loadBranches, selected_repository_id">
|
||||
Load Repository
|
||||
<x-loading-on-button wire:loading.delay wire:target="loadBranches, selected_repository_id" />
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@else
|
||||
<div>No repositories found. Check your GitHub App configuration.</div>
|
||||
@@ -78,6 +83,7 @@
|
||||
</x-forms.select>
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
@endif
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
||||
@@ -173,6 +173,34 @@
|
||||
</template>
|
||||
</x-slot:logo>
|
||||
</x-resource-view>
|
||||
<template x-if="service.amd_only">
|
||||
<div class="absolute top-2 right-10 group">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 cursor-pointer">
|
||||
AMD only
|
||||
</span>
|
||||
<div class="info-helper-popup right-0 w-sm">
|
||||
<div class="p-4">
|
||||
This service only supports AMD64/x86_64 architecture. It will not work
|
||||
on ARM-based servers (e.g., Apple Silicon, Raspberry Pi, AWS Graviton).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="service.arm_only">
|
||||
<div class="absolute top-2 right-10 group">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 cursor-pointer">
|
||||
ARM only
|
||||
</span>
|
||||
<div class="info-helper-popup right-0 w-sm">
|
||||
<div class="p-4">
|
||||
This service only supports ARM64/aarch64 architecture. It will not work
|
||||
on AMD64/x86_64-based servers.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="shouldShowDocIcon(service)">
|
||||
<a :href="getDocLink(service) || coolifyDocsUrl(service.name)" target="_blank"
|
||||
@click.stop @mouseenter="resolveDocLink(service)"
|
||||
@@ -424,6 +452,7 @@
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="font-bold dark:group-hover:text-white">
|
||||
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
|
||||
<x-deprecated-badge />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
href="{{ route('project.service.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}"><span class="menu-item-label">Environment Variables</span></a>
|
||||
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.storages', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}"><span class="menu-item-label">Persistent Storages</span></a>
|
||||
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
<a @class(['sub-menu-item', 'menu-item-active' => str($currentRoute)->startsWith('project.service.scheduled-tasks')]) {{ wireNavigate() }}
|
||||
href="{{ route('project.service.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}"><span class="menu-item-label">Scheduled Tasks</span></a>
|
||||
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
|
||||
@@ -43,134 +43,12 @@
|
||||
@endif
|
||||
|
||||
@foreach ($applications as $application)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => str(
|
||||
$application->status)->contains(['exited']),
|
||||
'border-l border-dashed border-success' => str(
|
||||
$application->status)->contains(['running']),
|
||||
'border-l border-dashed border-warning' => str(
|
||||
$application->status)->contains(['starting']),
|
||||
'flex gap-2 box-without-bg-without-border dark:bg-coolgray-100 bg-white dark:hover:text-neutral-300 group',
|
||||
])>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="pb-2">
|
||||
@if ($application->human_name)
|
||||
{{ Str::headline($application->human_name) }}
|
||||
@else
|
||||
{{ Str::headline($application->name) }}
|
||||
@endif
|
||||
<span class="text-xs">({{ $application->image }})</span>
|
||||
</div>
|
||||
@if ($application->configuration_required)
|
||||
<span class="text-xs text-error">(configuration required)</span>
|
||||
@endif
|
||||
@if ($application->description)
|
||||
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
|
||||
@endif
|
||||
@if ($application->fqdn)
|
||||
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
||||
@can('update', $service)
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<span class="cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4 dark:text-warning text-coollabs"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path
|
||||
d="m12 15l8.385-8.415a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zm4-10l3 3" />
|
||||
<path d="M9 7.07A7 7 0 0 0 10 21a7 7 0 0 0 6.929-6" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
</x-slot:content>
|
||||
<livewire:project.service.edit-domain
|
||||
applicationId="{{ $application->id }}"
|
||||
wire:key="edit-domain-{{ $application->id }}" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
</span>
|
||||
@endif
|
||||
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $application->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@if (str($application->status)->contains('running'))
|
||||
@can('update', $service)
|
||||
<x-modal-confirmation title="Confirm Service Application Restart?"
|
||||
buttonTitle="Restart"
|
||||
submitAction="restartApplication({{ $application->id }})" :actions="[
|
||||
'The selected service application will be unavailable during the restart.',
|
||||
'If the service application is currently in use data could be lost.',
|
||||
]"
|
||||
:confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Restart Service Container" />
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.service.resource-card :service="$service" :resource="$application"
|
||||
:parameters="$parameters" wire:key="service-application-card-{{ $application->id }}" />
|
||||
@endforeach
|
||||
@foreach ($databases as $database)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => str($database->status)->contains(
|
||||
['exited']),
|
||||
'border-l border-dashed border-success' => str($database->status)->contains(
|
||||
['running']),
|
||||
'border-l border-dashed border-warning' => str($database->status)->contains(
|
||||
['restarting']),
|
||||
'flex gap-2 box-without-bg-without-border dark:bg-coolgray-100 bg-white dark:hover:text-neutral-300 group',
|
||||
])>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="pb-2">
|
||||
@if ($database->human_name)
|
||||
{{ Str::headline($database->human_name) }}
|
||||
@else
|
||||
{{ Str::headline($database->name) }}
|
||||
@endif
|
||||
<span class="text-xs">({{ $database->image }})</span>
|
||||
</div>
|
||||
@if ($database->configuration_required)
|
||||
<span class="text-xs text-error">(configuration required)</span>
|
||||
@endif
|
||||
@if ($database->description)
|
||||
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
|
||||
@endif
|
||||
<div class="text-xs">{{ formatContainerStatus($database->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.database.backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}">
|
||||
Backups
|
||||
</a>
|
||||
@endif
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@if (str($database->status)->contains('running'))
|
||||
@can('update', $service)
|
||||
<x-modal-confirmation title="Confirm Service Database Restart?"
|
||||
buttonTitle="Restart" submitAction="restartDatabase({{ $database->id }})"
|
||||
:actions="[
|
||||
'This service database will be unavailable during the restart.',
|
||||
'If the service database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Restart Database" />
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.service.resource-card :service="$service" :resource="$database"
|
||||
:parameters="$parameters" wire:key="service-database-card-{{ $database->id }}" />
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif ($currentRoute === 'project.service.environment-variables')
|
||||
@@ -189,6 +67,8 @@
|
||||
@endforeach
|
||||
@elseif ($currentRoute === 'project.service.scheduled-tasks.show')
|
||||
<livewire:project.shared.scheduled-task.all :resource="$service" />
|
||||
@elseif ($currentRoute === 'project.service.scheduled-tasks')
|
||||
<livewire:project.shared.scheduled-task.show />
|
||||
@elseif ($currentRoute === 'project.service.webhooks')
|
||||
<livewire:project.shared.webhooks :resource="$service" />
|
||||
@elseif ($currentRoute === 'project.service.resource-operations')
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<div class="w-full">
|
||||
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
|
||||
@if($requiredPort)
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
<x-callout type="info" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
<strong>Example:</strong> https://app.coolify.io:{{ $requiredPort }},https://www.app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
|
||||
id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<div>
|
||||
<div class="flex flex-col gap-4 p-4 bg-white border dark:bg-base dark:border-coolgray-300 border-neutral-200">
|
||||
@if ($isReadOnly)
|
||||
@if ($fileStorage->is_too_large)
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
File on server exceeds 5 MB and cannot be edited from the UI. Edit it directly on the server.
|
||||
</div>
|
||||
@elseif ($isReadOnly)
|
||||
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
|
||||
@if ($fileStorage->is_directory)
|
||||
This directory is mounted as read-only and cannot be modified from the UI.
|
||||
@@ -17,7 +21,7 @@
|
||||
</div>
|
||||
@if ($resource instanceof \App\Models\Application)
|
||||
@can('update', $resource)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's path for preview deployments (e.g. ./scripts becomes ./scripts-pr-1). Disable this for volumes that contain shared config or scripts from your repository."></x-forms.checkbox>
|
||||
@@ -44,7 +48,7 @@
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
|
||||
shortConfirmationLabel="Filepath" />
|
||||
@else
|
||||
@if (!$fileStorage->is_binary)
|
||||
@if (!$fileStorage->is_binary && !$fileStorage->is_too_large)
|
||||
<x-modal-confirmation :ignoreWire="false" title="Confirm File Conversion to Directory?"
|
||||
buttonTitle="Convert to directory" submitAction="convertToDirectory" :actions="[
|
||||
'The selected file will be permanently deleted and an empty directory will be created in its place.',
|
||||
@@ -67,7 +71,7 @@
|
||||
@if (!$fileStorage->is_directory)
|
||||
@can('update', $resource)
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
@@ -76,13 +80,13 @@
|
||||
label="{{ $fileStorage->is_based_on_git ? 'Content (refreshed after a successful deployment)' : 'Content' }}"
|
||||
helper="The content shown may be outdated. Click 'Load from server' to fetch the latest version."
|
||||
rows="20" id="content"
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary)
|
||||
readonly="{{ $fileStorage->is_based_on_git || $fileStorage->is_binary || $fileStorage->is_too_large }}"></x-forms.textarea>
|
||||
@if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary && !$fileStorage->is_too_large)
|
||||
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
|
||||
@endif
|
||||
@else
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
@@ -103,7 +107,7 @@
|
||||
</div>
|
||||
@endcan
|
||||
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox disabled label="Is this based on the Git repository?"
|
||||
id="isBasedOnGit"></x-forms.checkbox>
|
||||
</div>
|
||||
|
||||
@@ -9,120 +9,198 @@
|
||||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.service.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.service.command', $parameters) }}">
|
||||
<button>Terminal</button>
|
||||
</a>
|
||||
@endcan
|
||||
<x-services.links :service="$service" />
|
||||
<div class="shrink-0">
|
||||
<x-services.links :service="$service" />
|
||||
</div>
|
||||
</nav>
|
||||
@if ($service->isDeployable)
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<div class="order-first flex flex-wrap items-center gap-2 sm:order-last">
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (str($service->status)->contains('running'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
Pull Latest Images & Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
</svg>
|
||||
<span class="menu-item-label">Back</span>
|
||||
</a>
|
||||
<a class="sub-menu-item menu-item-active" href="#"><span class="menu-item-label">General</span></a>
|
||||
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', $parameters) }}"><span class="menu-item-label">General</span></a>
|
||||
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index.advanced', $parameters) }}"><span class="menu-item-label">Advanced</span></a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-full">
|
||||
@@ -23,63 +26,9 @@
|
||||
{{ data_get_str($service, 'name')->limit(10) }} >
|
||||
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify
|
||||
</x-slot>
|
||||
<form wire:submit='submitApplication'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($serviceApplication->human_name)
|
||||
<h2>{{ Str::headline($serviceApplication->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($serviceApplication->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$serviceApplication" type="submit">Save</x-forms.button>
|
||||
@can('update', $serviceApplication)
|
||||
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
|
||||
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
@can('delete', $serviceApplication)
|
||||
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="deleteApplication" :actions="['The selected service application container will be stopped and permanently deleted.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Description"
|
||||
id="description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
@if ($serviceApplication->required_fqdn)
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" required placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication"
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="py-2 pt-4">Advanced</h3>
|
||||
<div class="w-96 flex flex-col gap-1">
|
||||
@if ($currentRoute === 'project.service.index.advanced')
|
||||
<h2>Advanced</h2>
|
||||
<div class="w-full sm:w-96 flex flex-col gap-1 pt-4">
|
||||
@if (str($serviceApplication->image)->contains('pocketbase'))
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
@@ -99,77 +48,134 @@
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveApplicationAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
<form wire:submit='submitApplication'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($serviceApplication->human_name)
|
||||
<h2>{{ Str::headline($serviceApplication->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($serviceApplication->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$serviceApplication" type="submit">Save</x-forms.button>
|
||||
@can('update', $serviceApplication)
|
||||
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
|
||||
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
@can('delete', $serviceApplication)
|
||||
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="deleteApplication" :actions="['The selected service application container will be stopped and permanently deleted.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
<x-callout type="info" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> https://app.coolify.io:{{ $requiredPort }},https://www.app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>Only one service will be accessible at this domain</li>
|
||||
<li>The routing behavior will be unpredictable</li>
|
||||
<li>You may experience service disruptions</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Description"
|
||||
id="description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
@if ($serviceApplication->required_fqdn)
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" required placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication"
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>Only one service will be accessible at this domain</li>
|
||||
<li>The routing behavior will be unpredictable</li>
|
||||
<li>You may experience service disruptions</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
@elseif ($resourceType === 'database')
|
||||
<x-slot:title>
|
||||
@@ -178,6 +184,17 @@
|
||||
</x-slot>
|
||||
@if ($currentRoute === 'project.service.database.import')
|
||||
<livewire:project.database.import :resource="$serviceDatabase" :key="'import-' . $serviceDatabase->uuid" />
|
||||
@elseif ($currentRoute === 'project.service.index.advanced')
|
||||
<h2>Advanced</h2>
|
||||
<div class="w-full sm:w-96 flex flex-col gap-1 pt-4">
|
||||
<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."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase"
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
@else
|
||||
<form wire:submit='submitDatabase'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@@ -233,7 +250,7 @@
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave id="isPublic"
|
||||
label="Make it publicly available" />
|
||||
</div>
|
||||
<x-forms.input canGate="update" :canResource="$serviceDatabase" placeholder="5432"
|
||||
<x-forms.input type="number" canGate="update" :canResource="$serviceDatabase" placeholder="5432"
|
||||
disabled="{{ $serviceDatabase->is_public }}" id="publicPort" label="Public Port" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Database IP:PORT (public)"
|
||||
@@ -242,16 +259,6 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<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."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase"
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => str($resource->status)->contains(['exited']),
|
||||
'border-l border-dashed border-success' => str($resource->status)->contains(['running']),
|
||||
'border-l border-dashed border-warning' => str($resource->status)->contains(['starting', 'restarting']),
|
||||
'flex gap-2 box-without-bg-without-border dark:bg-coolgray-100 bg-white dark:hover:text-neutral-300 group',
|
||||
])>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="pb-2">
|
||||
@if ($resource->human_name)
|
||||
{{ Str::headline($resource->human_name) }}
|
||||
@else
|
||||
{{ Str::headline($resource->name) }}
|
||||
@endif
|
||||
<span class="text-xs">({{ $resource->image }})</span>
|
||||
</div>
|
||||
@if ($resource->configuration_required)
|
||||
<span class="text-xs text-error">(configuration required)</span>
|
||||
@endif
|
||||
@if ($resource->description)
|
||||
<span class="text-xs">{{ Str::limit($resource->description, 60) }}</span>
|
||||
@endif
|
||||
@if ($isApplication && $resource->fqdn)
|
||||
<span class="flex gap-1 text-xs">{{ Str::limit($resource->fqdn, 60) }}
|
||||
@can('update', $service)
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<span class="cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4 dark:text-warning text-coollabs"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path d="m12 15l8.385-8.415a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zm4-10l3 3" />
|
||||
<path d="M9 7.07A7 7 0 0 0 10 21a7 7 0 0 0 6.929-6" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</x-slot:content>
|
||||
<livewire:project.service.edit-domain applicationId="{{ $resource->id }}"
|
||||
wire:key="edit-domain-{{ $resource->id }}" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
</span>
|
||||
@endif
|
||||
<div @class(['pt-2' => $isApplication, 'text-xs'])>{{ formatContainerStatus($resource->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.database.backups', [...$parameters, 'stack_service_uuid' => $resource->uuid]) }}">
|
||||
Backups
|
||||
</a>
|
||||
@endif
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $resource->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@if (str($resource->status)->contains('running'))
|
||||
@can('update', $service)
|
||||
<x-modal-confirmation :title="$isApplication ? 'Confirm Service Application Restart?' : 'Confirm Service Database Restart?'"
|
||||
buttonTitle="Restart" submitAction="restart" :actions="$isApplication
|
||||
? [
|
||||
'The selected service application will be unavailable during the restart.',
|
||||
'If the service application is currently in use data could be lost.',
|
||||
]
|
||||
: [
|
||||
'This service database will be unavailable during the restart.',
|
||||
'If the service database is currently in use data could be lost.',
|
||||
]"
|
||||
:confirmWithText="false" :confirmWithPassword="false"
|
||||
:step2ButtonText="$isApplication ? 'Restart Service Container' : 'Restart Database'" />
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,6 +12,9 @@
|
||||
<livewire:project.service.edit-compose serviceId="{{ $service->id }}" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
<x-modal-input title="Resource Details" buttonTitle="Details">
|
||||
<livewire:project.shared.resource-details :resource="$service" />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
<div>Configuration</div>
|
||||
</div>
|
||||
@@ -20,7 +23,10 @@
|
||||
placeholder="My super WordPress site" />
|
||||
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<div>
|
||||
<h3>Network</h3>
|
||||
</div>
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork"
|
||||
label="Connect To Predefined Network"
|
||||
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
||||
@@ -46,4 +52,4 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</form>
|
||||
@@ -1,22 +1,77 @@
|
||||
<div>
|
||||
@if ($isConfigurationChanged && !is_null($resource->config_hash) && !$resource->isExited())
|
||||
<x-popup-small>
|
||||
<x-slot:title>
|
||||
The latest configuration has not been applied
|
||||
</x-slot:title>
|
||||
<x-slot:icon>
|
||||
<svg class="hidden w-10 h-10 dark:text-warning lg:block" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</x-slot:icon>
|
||||
<x-slot:description>
|
||||
<span>Please redeploy to apply the new configuration.</span>
|
||||
</x-slot:description>
|
||||
<x-slot:button-text @click="disableSponsorship()">
|
||||
Disable This Popup
|
||||
</x-slot:button-text>
|
||||
</x-popup-small>
|
||||
<div x-data="{ configurationDiffModalOpen: false, expandedRows: {} }">
|
||||
<x-popup-small>
|
||||
<x-slot:title>
|
||||
The latest configuration has not been applied
|
||||
</x-slot:title>
|
||||
<x-slot:icon>
|
||||
<svg class="hidden w-10 h-10 dark:text-warning lg:block" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>
|
||||
</x-slot:icon>
|
||||
<x-slot:description>
|
||||
<span>
|
||||
@if (data_get($configurationDiff, 'count'))
|
||||
{{ data_get($configurationDiff, 'count') }} unapplied configuration
|
||||
{{ data_get($configurationDiff, 'count') === 1 ? 'change' : 'changes' }} detected.
|
||||
@if (data_get($configurationDiff, 'requires_build'))
|
||||
A rebuild is required.
|
||||
@else
|
||||
Please redeploy to apply the new configuration.
|
||||
@endif
|
||||
<button type="button" class="ml-1 font-semibold underline text-coollabs dark:text-warning"
|
||||
x-on:click="$wire.refreshConfigurationChanges().then(() => configurationDiffModalOpen = true)"
|
||||
wire:loading.attr="disabled" wire:target="refreshConfigurationChanges">
|
||||
View changes
|
||||
</button>
|
||||
@else
|
||||
Please redeploy to apply the new configuration.
|
||||
@endif
|
||||
</span>
|
||||
</x-slot:description>
|
||||
</x-popup-small>
|
||||
|
||||
@if (data_get($configurationDiff, 'count'))
|
||||
<template x-teleport="body">
|
||||
<div x-show="configurationDiffModalOpen" x-cloak
|
||||
class="fixed inset-0 z-99 flex h-screen w-screen items-center justify-center p-4"
|
||||
@keydown.escape.window="configurationDiffModalOpen = false">
|
||||
<div x-show="configurationDiffModalOpen" x-transition.opacity
|
||||
class="absolute inset-0 h-full w-full bg-black/20 backdrop-blur-xs"
|
||||
@click="configurationDiffModalOpen = false"></div>
|
||||
<div x-show="configurationDiffModalOpen" x-trap.inert.noscroll="configurationDiffModalOpen"
|
||||
x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative flex max-h-[85vh] w-full flex-col rounded-sm border border-neutral-200 bg-white shadow-lg dark:border-coolgray-300 dark:bg-base lg:max-w-4xl">
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-neutral-200 px-6 py-5 dark:border-coolgray-300">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-black dark:text-white">Configuration changes</h3>
|
||||
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
These changes are not applied to the latest deployment yet.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" @click="configurationDiffModalOpen = false"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base">
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto p-6">
|
||||
<x-deployment.configuration-diff :diff="$configurationDiff" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -1,16 +1,23 @@
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'>
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'
|
||||
x-data="{ isMultiline: $wire.entangle('is_multiline') }">
|
||||
<x-forms.input placeholder="NODE_ENV" id="key" label="Name" required />
|
||||
@if ($is_multiline)
|
||||
<x-forms.textarea id="value" label="Value" required />
|
||||
@else
|
||||
<x-forms.env-var-input placeholder="production" id="value" label="Value" required
|
||||
:availableVars="$shared ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')" />
|
||||
@endif
|
||||
<template x-if="isMultiline">
|
||||
<div wire:key="env-value-textarea">
|
||||
<x-forms.textarea id="value" label="Value" required class="font-sans" spellcheck />
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!isMultiline">
|
||||
<div wire:key="env-value-input">
|
||||
<x-forms.env-var-input placeholder="production" id="value" label="Value" required
|
||||
:availableVars="$shared ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')"
|
||||
:serverUuid="data_get($parameters, 'server_uuid')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@if (!$shared && !$is_multiline)
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400 -mt-1">
|
||||
@if (!$shared)
|
||||
<div x-cloak x-show="!isMultiline" wire:key="env-value-tip" class="text-xs text-neutral-500 dark:text-neutral-400 -mt-1">
|
||||
Tip: Type <span class="font-mono dark:text-warning text-coollabs">{{</span> to reference a shared environment
|
||||
variable
|
||||
</div>
|
||||
@@ -33,8 +40,8 @@
|
||||
label="Is Literal?" />
|
||||
@endif
|
||||
|
||||
<x-forms.checkbox id="is_multiline" label="Is Multiline?" />
|
||||
<x-forms.checkbox id="is_multiline" x-model="isMultiline" label="Is Multiline?" />
|
||||
<x-forms.button type="submit" @click="slideOverOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</form>
|
||||
@@ -84,24 +84,24 @@
|
||||
Inline comments with space before # (e.g., <code class="font-mono">KEY=value #comment</code>) are stripped.
|
||||
</x-callout>
|
||||
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" id="variables" wire:model="variables"
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap font-sans" id="variables" wire:model="variables"
|
||||
label="Production Environment Variables"></x-forms.textarea>
|
||||
|
||||
@if ($showPreview)
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" label="Preview Deployments Environment Variables"
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap font-sans" label="Preview Deployments Environment Variables"
|
||||
id="variablesPreview" wire:model="variablesPreview"></x-forms.textarea>
|
||||
@endif
|
||||
|
||||
<x-forms.button type="submit" class="btn btn-primary">Save All Environment Variables</x-forms.button>
|
||||
@else
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" id="variables" wire:model="variables"
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap font-sans" id="variables" wire:model="variables"
|
||||
label="Production Environment Variables" disabled></x-forms.textarea>
|
||||
|
||||
@if ($showPreview)
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap" label="Preview Deployments Environment Variables"
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap font-sans" label="Preview Deployments Environment Variables"
|
||||
id="variablesPreview" wire:model="variablesPreview" disabled></x-forms.textarea>
|
||||
@endif
|
||||
@endcan
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,6 +43,12 @@
|
||||
@endif
|
||||
@else
|
||||
@if ($is_shared)
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
<x-forms.checkbox instantSave id="is_runtime"
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
<x-forms.checkbox instantSave id="is_literal"
|
||||
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
|
||||
label="Is Literal?" />
|
||||
@@ -52,7 +58,7 @@
|
||||
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
||||
@endif
|
||||
@else
|
||||
@if (!$env->is_nixpacks)
|
||||
@if (!$env->is_buildpack_control)
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
@@ -61,7 +67,7 @@
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
@if (!$isMagicVariable)
|
||||
@if (!$env->is_nixpacks)
|
||||
@if (!$env->is_buildpack_control)
|
||||
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
||||
@if ($is_multiline === false)
|
||||
<x-forms.checkbox instantSave id="is_literal"
|
||||
@@ -89,6 +95,12 @@
|
||||
@endif
|
||||
@else
|
||||
@if ($is_shared)
|
||||
<x-forms.checkbox disabled id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
<x-forms.checkbox disabled id="is_runtime"
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
<x-forms.checkbox disabled id="is_literal"
|
||||
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
|
||||
label="Is Literal?" />
|
||||
@@ -129,7 +141,14 @@
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||
<x-forms.input disabled id="key" />
|
||||
<x-forms.input disabled type="password" id="value" />
|
||||
<x-forms.env-var-input
|
||||
disabled
|
||||
type="password"
|
||||
id="value"
|
||||
:availableVars="$isSharedVariable ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')"
|
||||
:serverUuid="data_get($parameters, 'server_uuid')" />
|
||||
@if ($is_shared)
|
||||
<x-forms.input disabled type="password" id="real_value" />
|
||||
@endif
|
||||
@@ -143,10 +162,21 @@
|
||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||
@if ($is_multiline)
|
||||
<x-forms.input :required="$is_redis_credential" isMultiline="{{ $is_multiline }}" id="key" />
|
||||
<x-forms.textarea :required="$is_redis_credential" type="password" id="value" />
|
||||
<div class="flex-1" wire:key="env-show-value-textarea-{{ $env->id }}">
|
||||
<x-forms.textarea :required="$is_redis_credential" type="password" id="value" />
|
||||
</div>
|
||||
@else
|
||||
<x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" id="key" />
|
||||
<x-forms.input :required="$is_redis_credential" type="password" id="value" />
|
||||
<div class="w-full" wire:key="env-show-value-input-{{ $env->id }}">
|
||||
<x-forms.env-var-input
|
||||
:required="$is_redis_credential"
|
||||
type="password"
|
||||
id="value"
|
||||
:availableVars="$isSharedVariable ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')"
|
||||
:serverUuid="data_get($parameters, 'server_uuid')" />
|
||||
</div>
|
||||
@endif
|
||||
@if ($is_shared)
|
||||
<x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" disabled
|
||||
@@ -161,7 +191,14 @@
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||
<x-forms.input disabled id="key" />
|
||||
<x-forms.input disabled type="password" id="value" />
|
||||
<x-forms.env-var-input
|
||||
disabled
|
||||
type="password"
|
||||
id="value"
|
||||
:availableVars="$isSharedVariable ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')"
|
||||
:serverUuid="data_get($parameters, 'server_uuid')" />
|
||||
@if ($is_shared)
|
||||
<x-forms.input disabled type="password" id="real_value" />
|
||||
@endif
|
||||
@@ -184,6 +221,12 @@
|
||||
@endif
|
||||
@else
|
||||
@if ($is_shared)
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
<x-forms.checkbox instantSave id="is_runtime"
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
<x-forms.checkbox instantSave id="is_literal"
|
||||
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
|
||||
label="Is Literal?" />
|
||||
@@ -193,7 +236,7 @@
|
||||
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
||||
@endif
|
||||
@else
|
||||
@if (!$env->is_nixpacks)
|
||||
@if (!$env->is_buildpack_control)
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
@@ -202,7 +245,7 @@
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
@if (!$isMagicVariable)
|
||||
@if (!$env->is_nixpacks)
|
||||
@if (!$env->is_buildpack_control)
|
||||
<x-forms.checkbox instantSave id="is_multiline" label="Is Multiline?" />
|
||||
@if ($is_multiline === false)
|
||||
<x-forms.checkbox instantSave id="is_literal"
|
||||
@@ -258,6 +301,12 @@
|
||||
@endif
|
||||
@else
|
||||
@if ($is_shared)
|
||||
<x-forms.checkbox disabled id="is_buildtime"
|
||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||
label="Available at Buildtime" />
|
||||
<x-forms.checkbox disabled id="is_runtime"
|
||||
helper="Make this variable available in the running container at runtime."
|
||||
label="Available at Runtime" />
|
||||
<x-forms.checkbox disabled id="is_literal"
|
||||
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
|
||||
label="Is Literal?" />
|
||||
@@ -291,4 +340,4 @@
|
||||
@endif
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,6 +28,38 @@
|
||||
}
|
||||
},
|
||||
isScrolling: false,
|
||||
lastTouchY: 0,
|
||||
disableFollow() {
|
||||
if (!this.alwaysScroll) return;
|
||||
this.alwaysScroll = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
},
|
||||
handleWheel(event) {
|
||||
if (this.alwaysScroll && event.deltaY < 0) {
|
||||
this.disableFollow();
|
||||
}
|
||||
},
|
||||
handleTouchStart(event) {
|
||||
this.lastTouchY = event.touches[0].clientY;
|
||||
},
|
||||
handleTouchMove(event) {
|
||||
if (!this.alwaysScroll) return;
|
||||
const currentY = event.touches[0].clientY;
|
||||
if (currentY > this.lastTouchY) {
|
||||
this.disableFollow();
|
||||
}
|
||||
this.lastTouchY = currentY;
|
||||
},
|
||||
handleKeyScroll(event) {
|
||||
if (!this.alwaysScroll) return;
|
||||
const upKeys = ['ArrowUp', 'PageUp', 'Home'];
|
||||
if (upKeys.includes(event.key)) {
|
||||
this.disableFollow();
|
||||
}
|
||||
},
|
||||
scrollToBottom() {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
@@ -57,17 +89,14 @@
|
||||
}
|
||||
},
|
||||
handleScroll(event) {
|
||||
if (!this.alwaysScroll || this.isScrolling) return;
|
||||
if (this.isScrolling) return;
|
||||
clearTimeout(this.scrollDebounce);
|
||||
this.scrollDebounce = setTimeout(() => {
|
||||
const el = event.target;
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (distanceFromBottom > 100) {
|
||||
this.alwaysScroll = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
if (!this.alwaysScroll && distanceFromBottom <= 10) {
|
||||
this.alwaysScroll = true;
|
||||
this.scheduleScroll();
|
||||
}
|
||||
}, 150);
|
||||
},
|
||||
@@ -245,10 +274,13 @@
|
||||
<div>({{ $pull_request }})</div>
|
||||
@endif
|
||||
@if ($streamLogs)
|
||||
<x-loading wire:poll.2000ms='getLogs(true)' />
|
||||
<x-loading />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if ($streamLogs)
|
||||
<div class="sr-only" wire:poll.2000ms="getLogs(true)" aria-hidden="true"></div>
|
||||
@endif
|
||||
<div x-show="expanded" {{ $collapsible ? 'x-collapse' : '' }}
|
||||
:class="fullscreen ? 'fullscreen flex flex-col !overflow-visible' : 'relative w-full {{ $collapsible ? 'py-4' : '' }} mx-auto'"
|
||||
:style="fullscreen ? 'max-height: none !important; height: 100% !important;' : ''">
|
||||
@@ -473,14 +505,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logsContainer" @scroll="handleScroll"
|
||||
<div id="logsContainer" @scroll="handleScroll" @wheel="handleWheel"
|
||||
@touchstart="handleTouchStart" @touchmove="handleTouchMove" @keydown="handleKeyScroll" tabindex="0"
|
||||
class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0 scrollbar"
|
||||
:class="fullscreen ? 'flex-1' : 'max-h-[40rem]'">
|
||||
@if ($outputs)
|
||||
@php
|
||||
$displayLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== '');
|
||||
@endphp
|
||||
<div id="logs" class="font-mono max-w-full cursor-default">
|
||||
<div id="logs" class="font-logs max-w-full cursor-default">
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
@@ -490,20 +523,19 @@
|
||||
// Parse timestamp from log line (ISO 8601 format: 2025-12-04T11:48:39.136764033Z)
|
||||
$timestamp = '';
|
||||
$logContent = $line;
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z?\s(.*)$/', $line, $matches)) {
|
||||
$year = $matches[1];
|
||||
$month = $matches[2];
|
||||
$day = $matches[3];
|
||||
$time = $matches[4];
|
||||
$microseconds = isset($matches[5]) ? substr($matches[5], 0, 6) : '000000';
|
||||
$logContent = $matches[6];
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z?\s(.*)$/', $line, $matches)) {
|
||||
$microseconds = isset($matches[2]) ? substr($matches[2], 0, 6) : '000000';
|
||||
$logContent = $matches[3];
|
||||
|
||||
// Convert month number to abbreviated name
|
||||
$monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
$monthName = $monthNames[(int)$month - 1] ?? $month;
|
||||
|
||||
// Format for display: 2025-Dec-04 09:44:58
|
||||
$timestamp = "{$year}-{$monthName}-{$day} {$time}";
|
||||
// Convert UTC Docker timestamp to server timezone for display
|
||||
$carbonTs = \Carbon\Carbon::parse($matches[1], 'UTC');
|
||||
$serverTz = getServerTimezone($server);
|
||||
try {
|
||||
$carbonTs->setTimezone($serverTz);
|
||||
} catch (\Exception) {
|
||||
// keep UTC
|
||||
}
|
||||
$timestamp = $carbonTs->format('Y-M-d H:i:s');
|
||||
// Include microseconds in key for uniqueness
|
||||
$lineKey = "{$timestamp}.{$microseconds}";
|
||||
}
|
||||
@@ -518,7 +550,7 @@
|
||||
</div>
|
||||
@else
|
||||
<pre id="logs"
|
||||
class="font-mono whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet.</pre>
|
||||
class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet.</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Healthchecks</h2>
|
||||
<h2>Healthcheck</h2>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="w-full max-h-[70vh] overflow-y-auto pr-1 -mt-4">
|
||||
<div class="pb-4 text-sm dark:text-neutral-400">Identifiers for this resource. Read-only</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<h3>Resource</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$resource->name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$resource->uuid ?? ''" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($environment_uuid)
|
||||
<div>
|
||||
<h3>Environment</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$environment_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$environment_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($project_uuid)
|
||||
<div>
|
||||
<h3>Project</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$project_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$project_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($server_uuid)
|
||||
<div>
|
||||
<h3>Server</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<x-forms.copy-button label="Name" :text="$server_name ?? ''" />
|
||||
<x-forms.copy-button label="UUID" :text="$server_uuid" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (! empty($stack_applications) || ! empty($stack_databases))
|
||||
<div>
|
||||
<h3>Stack Sub-Resources</h3>
|
||||
<div class="pt-2 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
@foreach ($stack_applications as $item)
|
||||
<x-forms.copy-button :label="'Application — ' . $item['name']" :text="$item['uuid']" />
|
||||
@endforeach
|
||||
@foreach ($stack_databases as $item)
|
||||
<x-forms.copy-button :label="'Database — ' . $item['name']" :text="$item['uuid']" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,24 +18,20 @@
|
||||
label="CPU Weight" id="limitsCpuShares" />
|
||||
</div>
|
||||
<h3 class="pt-4">Limit Memory</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
|
||||
label="Soft Memory Limit" id="limitsMemoryReservation" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
|
||||
type="number" min="0" max="100" label="Swappiness"
|
||||
id="limitsMemorySwappiness" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
|
||||
label="Maximum Memory Limit" id="limitsMemory" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
|
||||
label="Maximum Swap Limit" id="limitsMemorySwap" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
|
||||
label="Soft Memory Limit" id="limitsMemoryReservation" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="Value between 0-100.<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
|
||||
type="number" min="0" max="100" label="Swappiness"
|
||||
id="limitsMemorySwappiness" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
|
||||
label="Maximum Memory Limit" id="limitsMemory" />
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
|
||||
label="Maximum Swap Limit" id="limitsMemorySwap" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -63,6 +63,9 @@
|
||||
}">
|
||||
<h3 class="pt-4">Clone Resource</h3>
|
||||
<div class="pb-2">Duplicate this resource to another server or network destination.</div>
|
||||
<x-callout type="info" title="Important" class="mb-4">
|
||||
Cloning only duplicates resource configuration (such as environment variables, build settings etc..). It does not include any resource data, such as databases or stored files.
|
||||
</x-callout>
|
||||
|
||||
@can('update', $resource)
|
||||
<div class="space-y-4 pb-8">
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($resource, 'name')->limit(10) }} > Scheduled Tasks | Coolify
|
||||
</x-slot>
|
||||
@if ($type === 'application')
|
||||
<h1>Scheduled Task</h1>
|
||||
<livewire:project.application.heading :application="$resource" />
|
||||
@elseif ($type === 'service')
|
||||
<livewire:project.service.heading :service="$resource" :parameters="$parameters" />
|
||||
@endif
|
||||
|
||||
<form wire:submit="submit" class="w-full">
|
||||
<div class="flex flex-col gap-2 pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<h2>Scheduled Task</h2>
|
||||
<h2>Task {{ $task->name }}</h2>
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@@ -21,6 +11,11 @@
|
||||
Execute Now
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if (!$isEnabled)
|
||||
<x-forms.button wire:click="toggleEnabled" isHighlighted>Enable Task</x-forms.button>
|
||||
@else
|
||||
<x-forms.button wire:click="toggleEnabled">Disable Task</x-forms.button>
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
|
||||
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
|
||||
@@ -28,13 +23,11 @@
|
||||
step2ButtonText="Permanently Delete" />
|
||||
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox instantSave id="isEnabled" label="Enabled" />
|
||||
</div>
|
||||
<h3 class="pt-4">Configuration</h3>
|
||||
<div class="flex gap-2 w-full">
|
||||
<x-forms.input placeholder="Name" id="name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency"
|
||||
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." required />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-36000)." label="Timeout (seconds)" required />
|
||||
@if ($type === 'application')
|
||||
@@ -47,6 +40,7 @@
|
||||
id="container" label="Service name" />
|
||||
@endif
|
||||
</div>
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
</form>
|
||||
|
||||
<div class="pt-4">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
@endif
|
||||
@if (!$isService)
|
||||
@can('update', $resource)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's name for preview deployments (e.g. myvolume becomes myvolume-pr-1). Disable this for volumes that should be shared between the main and preview deployments."></x-forms.checkbox>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
@endif
|
||||
@if (!$isService)
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave canGate="update" :canResource="$resource" label="Add suffix for PR deployments"
|
||||
id="isPreviewSuffixEnabled"
|
||||
helper="When enabled, a -pr-N suffix is added to this volume's name for preview deployments (e.g. myvolume becomes myvolume-pr-1). Disable this for volumes that should be shared between the main and preview deployments."></x-forms.checkbox>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<div id="terminal-container" x-data="terminalData()">
|
||||
@if ($isTerminalConnected)
|
||||
<div class="hidden" aria-hidden="true" wire:poll.keep-alive.30s="keepTerminalPageAlive"></div>
|
||||
@endif
|
||||
@if (!$hasShell)
|
||||
<div class="flex pt-4 items-center justify-center w-full py-4 mx-auto">
|
||||
<div class="p-4 w-full rounded-sm border dark:bg-coolgray-100 dark:border-coolgray-300">
|
||||
@@ -18,10 +21,37 @@
|
||||
</div>
|
||||
@endif
|
||||
<div x-ref="terminalWrapper"
|
||||
:class="fullscreen ? 'fullscreen !bg-black' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
|
||||
:class="fullscreen ? 'fixed inset-0 z-[9999] m-0 h-[100dvh] w-screen max-w-none overflow-hidden rounded-none !bg-black p-0' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
|
||||
<!-- Terminal container -->
|
||||
<div x-show="terminalActive" x-cloak class="mb-2 flex justify-start">
|
||||
<div class="inline-flex rounded-sm border px-2 py-1 text-xs font-medium"
|
||||
:class="terminalSessionTimerClass()" x-text="terminalSessionRemainingLabel()">
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal" wire:ignore
|
||||
:class="fullscreen ? 'px-2 py-1 h-full bg-black' : 'px-2 py-1 rounded-sm bg-black'" x-show="terminalActive">
|
||||
:class="fullscreen ? (mobileToolbarCollapsed ? 'h-[calc(100dvh-3.5rem)] mb-14 px-2 py-1 bg-black' : 'h-[calc(100dvh-6rem)] mb-[6rem] px-2 py-1 bg-black') : 'h-[510px] max-h-[calc(100dvh-10rem)] overflow-hidden px-2 py-1 rounded-sm bg-black'"
|
||||
x-show="terminalActive">
|
||||
</div>
|
||||
<div x-show="terminalActive" x-cloak
|
||||
:class="fullscreen ? 'absolute inset-x-0 bottom-0 z-[9999] px-2 pb-2' : 'relative mt-2'"
|
||||
class="sm:hidden" data-terminal-mobile-toolbar>
|
||||
<div class="mx-auto max-w-3xl rounded-lg border border-white/10 bg-black/90 p-1.5 text-white shadow-lg backdrop-blur">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="px-2 text-[11px] font-medium uppercase tracking-wide text-neutral-400">Terminal keys</span>
|
||||
<button type="button" class="rounded px-2 py-1 text-xs text-neutral-300 hover:bg-white/10 hover:text-white"
|
||||
x-on:click="mobileToolbarCollapsed = !mobileToolbarCollapsed; $nextTick(() => resizeTerminal())"
|
||||
x-text="mobileToolbarCollapsed ? 'Show' : 'Hide'"
|
||||
aria-label="Toggle mobile terminal toolbar"></button>
|
||||
</div>
|
||||
<div x-show="!mobileToolbarCollapsed" class="mt-1 grid grid-cols-6 gap-1">
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('arrowUp')" aria-label="Previous command">↑</button>
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('arrowDown')" aria-label="Next command">↓</button>
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('arrowLeft')" aria-label="Move cursor left">←</button>
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('arrowRight')" aria-label="Move cursor right">→</button>
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('tab')">Tab</button>
|
||||
<button type="button" class="terminal-mobile-key" x-on:click="sendTerminalControl('escape')">Esc</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button title="Minimize" x-show="fullscreen" class="fixed bg-black/40 top-4 right-6 text-white"
|
||||
x-on:click="makeFullscreen"><svg class="w-5 h-5 text-gray-500 hover:text-white bg-black/80"
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Webhooks</h2>
|
||||
<x-helper
|
||||
helper="For more details goto our <a class='underline dark:text-white' href='https://coolify.io/docs/api-reference/api/operations/deploy-by-tag-or-uuid' target='_blank'>docs</a>." />
|
||||
helper="For more details goto our <a class='underline dark:text-white' href='https://coolify.io/docs/api-reference/authorization' target='_blank'>docs</a>." />
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.input readonly
|
||||
helper="See details in our <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/api-reference/api/operations/deploy-by-tag-or-uuid'>documentation</a>."
|
||||
helper="See details in our <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/api-reference/authorization'>documentation</a>."
|
||||
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
|
||||
</div>
|
||||
@if ($resource->type() === 'application')
|
||||
|
||||
@@ -14,13 +14,19 @@
|
||||
<h3>New Token</h3>
|
||||
@can('create', App\Models\PersonalAccessToken::class)
|
||||
<form class="flex flex-col gap-2" wire:submit='addNewToken'>
|
||||
<div class="flex gap-2 items-end w-96">
|
||||
<x-forms.input required id="description" label="Description" />
|
||||
<div class="flex gap-2 items-end w-lg">
|
||||
<x-forms.input class="w-64" required id="description" label="Description" />
|
||||
<x-forms.select id="expiresInDays" label="Expires in" wire:model="expiresInDays">
|
||||
@foreach ($expirationOptions as $days => $label)
|
||||
<option value="{{ $days }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
<option value="">Never</option>
|
||||
</x-forms.select>
|
||||
<x-forms.button type="submit">Create</x-forms.button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
Permissions
|
||||
<x-helper class="px-1" helper="These permissions will be granted to the token." /><span
|
||||
<span
|
||||
class="pr-1">:</span>
|
||||
<div class="flex gap-1 font-bold dark:text-white">
|
||||
@if ($permissions)
|
||||
@@ -31,7 +37,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Token Permissions</h4>
|
||||
<div class="w-64">
|
||||
@if ($canUseRootPermissions)
|
||||
<x-forms.checkbox label="root" wire:model.live="permissions" domValue="root"
|
||||
@@ -71,38 +76,78 @@
|
||||
<div class="pb-4 font-bold dark:text-white"> {{ session('token') }}</div>
|
||||
@endif
|
||||
<h3 class="py-4">Issued Tokens</h3>
|
||||
<div class="grid gap-2 lg:grid-cols-1">
|
||||
@forelse ($tokens as $token)
|
||||
<div wire:key="token-{{ $token->id }}"
|
||||
class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underline">
|
||||
<div>Description: {{ $token->name }}</div>
|
||||
<div>Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}</div>
|
||||
<div class="flex gap-1">
|
||||
@if ($token->abilities)
|
||||
Permissions:
|
||||
@foreach ($token->abilities as $ability)
|
||||
<div class="font-bold dark:text-white">{{ $ability }}</div>
|
||||
@endforeach
|
||||
@endif
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full">
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Description</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Permissions</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Last used</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Created</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Expires</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($tokens as $token)
|
||||
<tr wire:key="token-{{ $token->id }}">
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">{{ $token->name }}</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
@if ($token->abilities)
|
||||
<div class="flex gap-1">
|
||||
@foreach ($token->abilities as $ability)
|
||||
<div class="font-bold dark:text-white">{{ $ability }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ $token->created_at->diffForHumans() }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
@if (! $token->expires_at)
|
||||
Never
|
||||
@elseif ($token->expires_at->isPast())
|
||||
<span class="font-bold dark:text-error">Expired
|
||||
{{ $token->expires_at->format('Y-m-d H:i:s') }}</span>
|
||||
@else
|
||||
{{ $token->expires_at->format('Y-m-d H:i:s') }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm font-medium whitespace-nowrap">
|
||||
@if (auth()->id() === $token->tokenable_id)
|
||||
<x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton
|
||||
buttonTitle="Revoke token"
|
||||
submitAction="revoke({{ data_get($token, 'id') }})" :actions="[
|
||||
'This API Token will be revoked and permanently deleted.',
|
||||
'Any API call made with this token will fail.',
|
||||
]"
|
||||
confirmationText="{{ $token->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
|
||||
shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
|
||||
step2ButtonText="Revoke API Token" />
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap" colspan="6">No API tokens found.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (auth()->id() === $token->tokenable_id)
|
||||
<x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton buttonTitle="Revoke token"
|
||||
submitAction="revoke({{ data_get($token, 'id') }})" :actions="[
|
||||
'This API Token will be revoked and permanently deleted.',
|
||||
'Any API call made with this token will fail.',
|
||||
]"
|
||||
confirmationText="{{ $token->name }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
|
||||
shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
|
||||
step2ButtonText="Revoke API Token" />
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div>
|
||||
<div>No API tokens found.</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
<x-forms.input id="name" label="Name" required />
|
||||
<x-forms.input id="description" label="Description" />
|
||||
</div>
|
||||
<x-forms.textarea realtimeValidation id="value" rows="10"
|
||||
<x-forms.textarea realtimeValidation id="value" rows="10" monospace
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key" required />
|
||||
<x-forms.input id="publicKey" readonly label="Public Key" />
|
||||
<span class="pt-2 pb-4 font-bold dark:text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
required disabled />
|
||||
</div>
|
||||
<div x-cloak x-show="showPrivateKey">
|
||||
<x-forms.textarea canGate="update" :canResource="$private_key" rows="10" id="privateKeyValue" required />
|
||||
<x-forms.textarea canGate="update" :canResource="$private_key" rows="10" id="privateKeyValue" required monospace />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
id="serverDiskUsageCheckFrequency" label="Disk usage check frequency" required
|
||||
helper="Cron expression for disk usage check frequency.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at 11:00 PM." />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="serverDiskUsageNotificationThreshold"
|
||||
type="number" min="1" max="99"
|
||||
label="Server disk usage notification threshold (%)" required
|
||||
helper="If the server disk usage exceeds this threshold, Coolify will send a notification to the team members." />
|
||||
</div>
|
||||
@@ -31,12 +32,15 @@
|
||||
<h3>Builds</h3>
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap pt-4">
|
||||
<x-forms.input canGate="update" :canResource="$server" id="concurrentBuilds"
|
||||
type="number" min="1"
|
||||
label="Number of concurrent builds" required
|
||||
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="dynamicTimeout"
|
||||
type="number" min="1"
|
||||
label="Deployment timeout (seconds)" required
|
||||
helper="You can define the maximum duration for a deployment to run before timing it out." />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="deploymentQueueLimit"
|
||||
type="number" min="1"
|
||||
label="Deployment queue limit" required
|
||||
helper="Maximum number of queued deployments allowed. New deployments will be rejected with a 429 status when the limit is reached." />
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
@if ($server->standaloneDockers->isEmpty() && $server->swarmDockers->isEmpty())
|
||||
<div class="text-sm text-neutral-500">No destinations configured for this server yet.</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($networks->count() > 0)
|
||||
<div class="pt-2">
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
<svg class="h-5 w-5 shrink-0 text-gray-500 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<code class="flex-1 text-sm font-mono text-gray-700 dark:text-gray-300">{{ data_get($result, 'command') }}</code>
|
||||
<code class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300">{{ data_get($result, 'command') }}</code>
|
||||
</div>
|
||||
@php
|
||||
$output = data_get($result, 'output');
|
||||
@@ -108,7 +108,7 @@
|
||||
@endphp
|
||||
<div class="p-4">
|
||||
@if($hasOutput)
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ $output }}</pre>
|
||||
<pre class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ $output }}</pre>
|
||||
@else
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
No output returned - command completed successfully
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</x-callout>
|
||||
@endif
|
||||
<h3>Advanced</h3>
|
||||
<div class="pb-6 w-96">
|
||||
<div class="pb-6 w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
|
||||
id="generateExactLabels"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-96">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
|
||||
label="Enable Sentinel" />
|
||||
@if ($server->isSentinelEnabled())
|
||||
@@ -91,13 +91,14 @@
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
<x-forms.input canGate="update" :canResource="$server" type="number" min="1"
|
||||
id="sentinelMetricsRefreshRateSeconds" label="Metrics rate (seconds)" required
|
||||
helper="Interval used for gathering metrics. Lower values result in more disk space usage." />
|
||||
<x-forms.input canGate="update" :canResource="$server" id="sentinelMetricsHistoryDays"
|
||||
<x-forms.input canGate="update" :canResource="$server" type="number" min="1"
|
||||
id="sentinelMetricsHistoryDays"
|
||||
label="Metrics history (days)" required
|
||||
helper="Number of days to retain metrics data for." />
|
||||
<x-forms.input canGate="update" :canResource="$server"
|
||||
<x-forms.input canGate="update" :canResource="$server" type="number" min="10"
|
||||
id="sentinelPushIntervalSeconds" label="Push interval (seconds)" required
|
||||
helper="Interval at which metrics data is sent to the collector." />
|
||||
</div>
|
||||
|
||||
Loaded 100 of 766 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user