Merge pull request #625 from coollabsio/next

v3.10.5
This commit is contained in:
Andras Bacsai 2022-09-26 11:23:01 +02:00 committed by GitHub
commit bf6b799dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
146 changed files with 5952 additions and 5009 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ local-serve
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
logs logs
others/certificates

View File

@ -20,6 +20,7 @@
"@fastify/cors": "8.1.0", "@fastify/cors": "8.1.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2", "@fastify/jwt": "6.3.2",
"@fastify/multipart": "7.2.0",
"@fastify/static": "6.5.0", "@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.0.2",
@ -49,6 +50,7 @@
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"pump": "^3.0.0",
"ssh-config": "4.1.6", "ssh-config": "4.1.6",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"

View File

@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "Certificate" (
"id" TEXT NOT NULL PRIMARY KEY,
"key" TEXT NOT NULL,
"cert" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"teamId" TEXT,
CONSTRAINT "Certificate_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);

View File

@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -8,6 +8,16 @@ datasource db {
url = env("COOLIFY_DATABASE_URL") url = env("COOLIFY_DATABASE_URL")
} }
model Certificate {
id String @id @default(cuid())
key String
cert String
team Team? @relation(fields: [teamId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamId String?
}
model Setting { model Setting {
id String @id @default(cuid()) id String @id @default(cuid())
fqdn String? @unique fqdn String? @unique
@ -70,6 +80,7 @@ model Team {
gitLabApps GitlabApp[] gitLabApps GitlabApp[]
service Service[] service Service[]
users User[] users User[]
certificate Certificate[]
} }
model TeamInvitation { model TeamInvitation {
@ -161,6 +172,7 @@ model ApplicationSettings {
isBot Boolean @default(false) isBot Boolean @default(false)
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])

View File

@ -3,6 +3,7 @@ import cors from '@fastify/cors';
import serve from '@fastify/static'; import serve from '@fastify/static';
import env from '@fastify/env'; import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common'; import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
@ -31,6 +32,7 @@ prisma.setting.findFirst().then(async (settings) => {
logger: settings?.isAPIDebuggingEnabled || false, logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true trustProxy: true
}); });
const schema = { const schema = {
type: 'object', type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'], required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
@ -88,13 +90,13 @@ prisma.setting.findFirst().then(async (settings) => {
return reply.status(200).sendFile('index.html'); return reply.status(200).sendFile('index.html');
}); });
} }
fastify.register(multipart, { limits: { fileSize: 100000 } });
fastify.register(autoLoad, { fastify.register(autoLoad, {
dir: join(__dirname, 'plugins') dir: join(__dirname, 'plugins')
}); });
fastify.register(autoLoad, { fastify.register(autoLoad, {
dir: join(__dirname, 'routes') dir: join(__dirname, 'routes')
}); });
fastify.register(cookie) fastify.register(cookie)
fastify.register(cors); fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => { fastify.addHook('onRequest', async (request, reply) => {
@ -145,11 +147,16 @@ prisma.setting.findFirst().then(async (settings) => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10) }, isDev ? 6000 : 60000 * 10)
// checkProxies // checkProxies and checkFluentBit
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
}, 10000) }, 10000)
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
}, 2000)
// cleanupPrismaEngines // cleanupPrismaEngines
// setInterval(async () => { // setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines") // scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")

View File

@ -154,7 +154,7 @@ import * as buildpacks from '../lib/buildPacks';
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory; baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation; dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile; denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({ const commit = await importers[gitSource.type]({

View File

@ -1,8 +1,9 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import axios from 'axios'; import axios from 'axios';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() { async function autoUpdater() {
try { try {
const currentVersion = version; const currentVersion = version;
@ -39,6 +40,68 @@ async function autoUpdater() {
} }
} catch (error) { } } catch (error) { }
} }
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() { async function checkProxies() {
try { try {
const { default: isReachable } = await import('is-port-reachable'); const { default: isReachable } = await import('is-port-reachable');
@ -189,7 +252,8 @@ async function cleanupStorage() {
(async () => { (async () => {
let status = { let status = {
cleanupStorage: false, cleanupStorage: false,
autoUpdater: false autoUpdater: false,
copySSLCertificates: false,
} }
if (parentPort) { if (parentPort) {
parentPort.on('message', async (message) => { parentPort.on('message', async (message) => {
@ -215,6 +279,18 @@ async function cleanupStorage() {
await checkProxies(); await checkProxies();
return; return;
} }
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') { if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) { if (!status.cleanupStorage) {
status.autoUpdater = true status.autoUpdater = true

View File

@ -342,13 +342,13 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.2-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions;
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
if (buildPack === 'heroku') { if (buildPack === 'heroku') {
payload.baseImage = 'heroku/buildpacks:20'; payload.baseImage = 'heroku/buildpacks:20';
payload.baseImages = herokuVersions; payload.baseImages = herokuVersions;
} }
return payload; return payload;
} }
@ -384,7 +384,7 @@ export const setDefaultConfiguration = async (data: any) => {
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) { if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`; if (baseDirectory.endsWith('/') && baseDirectory !== '/') baseDirectory = baseDirectory.slice(0, -1);
} }
if (dockerFileLocation) { if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;

View File

@ -14,12 +14,8 @@ export default async function (data) {
dockerFileLocation dockerFileLocation
} = data } = data
try { try {
const file = `${workdir}${dockerFileLocation}`; const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`; data.workdir = `${workdir}${baseDirectory}`;
if (baseDirectory) {
dockerFileOut = `${workdir}${baseDirectory}`;
workdir = `${workdir}${baseDirectory}`;
}
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
.toString() .toString()
.trim() .trim()
@ -28,7 +24,6 @@ export default async function (data) {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
// TODO: fix secrets
if ( if (
(pullmergeRequestId && secret.isPRMRSecret) || (pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret) (!pullmergeRequestId && !secret.isPRMRSecret)
@ -45,7 +40,7 @@ export default async function (data) {
}); });
} }
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {
throw error; throw error;

View File

@ -2,38 +2,16 @@ import { executeDockerCmd, prisma } from "../common"
import { saveBuildLog } from "./common"; import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> { export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir } = data const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory } = data
try { try {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
const { stdout } = await executeDockerCmd({ await executeDockerCmd({
debug,
dockerId, dockerId,
command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20` command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder heroku/buildpacks:20`
}) })
if (debug) {
const array = stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
} catch (error) { } catch (error) {
const array = error.stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
throw error; throw error;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,8 @@ Bree.extend(TSBree);
const options: any = { const options: any = {
defaultExtension: 'js', defaultExtension: 'js',
// logger: new Cabin(), logger: new Cabin(),
logger: false, // logger: false,
workerMessageHandler: async ({ name, message }) => { workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication' && message?.deploying) { if (name === 'deployApplication' && message?.deploying) {
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) { if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {

View File

@ -20,7 +20,7 @@ export const includeServices: any = {
glitchTip: true, glitchTip: true,
searxng: true, searxng: true,
weblate: true, weblate: true,
taiga: true taiga: true,
}; };
export async function configureServiceType({ export async function configureServiceType({
id, id,

View File

@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import { ServiceStartStop } from '../../routes/api/v1/services/types'; import { ServiceStartStop } from '../../routes/api/v1/services/types';
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
import { defaultServiceConfigurations } from '../services'; import { defaultServiceConfigurations } from '../services';
import { OnlyId } from '../../types';
export async function startService(request: FastifyRequest<ServiceStartStop>) { export async function startService(request: FastifyRequest<ServiceStartStop>) {
try { try {
@ -69,6 +70,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'taiga') { if (type === 'taiga') {
return await startTaigaService(request) return await startTaigaService(request)
} }
if (type === 'grafana') {
return await startGrafanaService(request)
}
if (type === 'trilium') {
return await startTriliumService(request)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch (error) { } catch (error) {
throw { status: 500, message: error?.message || error } throw { status: 500, message: error?.message || error }
@ -314,7 +322,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
destinationDocker, destinationDocker,
persistentStorage, persistentStorage,
exposePort, exposePort,
minio: { rootUser, rootUserPassword }, minio: { rootUser, rootUserPassword, apiFqdn },
serviceSecret serviceSecret
} = service; } = service;
@ -333,7 +341,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`, image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`], volumes: [`${id}-minio-data:/data`],
environmentVariables: { environmentVariables: {
MINIO_SERVER_URL: fqdn, MINIO_SERVER_URL: apiFqdn,
MINIO_DOMAIN: getDomain(fqdn), MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_ROOT_PASSWORD: rootUserPassword,
@ -900,8 +908,8 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const { const {
meiliSearch: { masterKey } meiliSearch: { masterKey }
} = service; } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = const { type, version, destinationDockerId, destinationDocker,
service; serviceSecret, exposePort, persistentStorage } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('meilisearch'); const port = getServiceMainPort('meilisearch');
@ -2640,3 +2648,132 @@ async function startTaigaService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
async function startGrafanaService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('grafana');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
grafana: {
image: `${image}:${version}`,
volumes: [`${id}-grafana:/var/lib/grafana`],
environmentVariables: {}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.grafana.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.grafana.image,
volumes: config.grafana.volumes,
environment: config.grafana.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('grafana'),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function startTriliumService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('trilium');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
trilium: {
image: `${image}:${version}`,
volumes: [`${id}-trilium:/home/node/trilium-data`],
environmentVariables: {}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.trilium.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.trilium.image,
volumes: config.trilium.volumes,
environment: config.trilium.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('trilium'),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const {
destinationDockerId,
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id} migrate`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could cleanup logs.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -172,8 +172,8 @@ export const supportedServiceTypesAndVersions = [
fancyName: 'Appwrite', fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite', baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '1.0','0.15.3'], versions: ['latest', '1.0', '0.15.3'],
recommendedVersion: '0.15.3', recommendedVersion: '1.0',
ports: { ports: {
main: 80 main: 80
} }
@ -233,4 +233,26 @@ export const supportedServiceTypesAndVersions = [
// main: 80 // main: 80
// } // }
// }, // },
{
name: 'grafana',
fancyName: 'Grafana Dashboard',
baseImage: 'grafana/grafana',
images: [],
versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'],
recommendedVersion: 'latest',
ports: {
main: 3000
}
},
{
name: 'trilium',
fancyName: 'Trilium Notes',
baseImage: 'zadam/trilium',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
]; ];

View File

@ -321,15 +321,10 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) { export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body
// const isDouble = await checkDoubleBranch(branch, projectId);
// if (isDouble && autodeploy) {
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
// throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
// }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
return reply.code(201).send(); return reply.code(201).send();
@ -787,64 +782,74 @@ export async function saveConnectedDatabase(request, reply) {
export async function getSecrets(request: FastifyRequest<OnlyId>) { export async function getSecrets(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
let secrets = await prisma.secret.findMany({ let secrets = await prisma.secret.findMany({
where: { applicationId: id }, where: { applicationId: id, isPRMRSecret: false },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'asc' }
}); });
let previewSecrets = await prisma.secret.findMany({
where: { applicationId: id, isPRMRSecret: true },
orderBy: { createdAt: 'asc' }
});
secrets = secrets.map((secret) => { secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value); secret.value = decrypt(secret.value);
return secret; return secret;
}); });
secrets = secrets.filter((secret) => !secret.isPRMRSecret).sort((a, b) => { previewSecrets = previewSecrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return {
previewSecrets: previewSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
secrets: secrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name); return ('' + a.name).localeCompare(b.name);
}) })
return {
secrets
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function updatePreviewSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try {
const { id } = request.params
const { name, value } = request.body
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret: true },
data: { value: encrypt(value.trim()) }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function updateSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try {
const { id } = request.params
const { name, value, isBuildSecret = undefined } = request.body
await prisma.secret.updateMany({
where: { applicationId: id, name },
data: { value: encrypt(value.trim()), isBuildSecret }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) { export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body const { name, value, isBuildSecret = false } = request.body
if (isNew) {
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
if (found) {
throw { status: 500, message: `Secret ${name} already exists.` }
} else {
value = encrypt(value.trim());
await prisma.secret.create({ await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
}); });
}
} else {
if (value) {
value = encrypt(value.trim());
}
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) {
if (!value && isPRMRSecret) {
await prisma.secret.deleteMany({
where: { applicationId: id, name, isPRMRSecret }
});
} else {
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
}
} else {
await prisma.secret.create({ await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: true, application: { connect: { id } } }
}); });
}
}
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@ -30,6 +30,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request)); fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));
fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply)); fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply));
fastify.put<SaveSecret>('/:id/secrets', async (request, reply) => await updateSecret(request, reply));
fastify.put<SaveSecret>('/:id/secrets/preview', async (request, reply) => await updatePreviewSecret(request, reply));
fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request)); fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request));
fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request)); fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request));

View File

@ -26,7 +26,7 @@ export interface SaveApplication extends OnlyId {
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean }; Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
@ -65,7 +65,7 @@ export interface SaveSecret extends OnlyId {
name: string, name: string,
value: string, value: string,
isBuildSecret: boolean, isBuildSecret: boolean,
isPRMRSecret: boolean, previewSecret: boolean,
isNew: boolean isNew: boolean
} }
} }

View File

@ -1,6 +1,9 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types'; import { GetCurrentUser } from './types';
import pump from 'pump'
import fs from 'fs'
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
export interface Update { export interface Update {
Body: { latestVersion: string } Body: { latestVersion: string }
@ -23,9 +26,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await getCurrentUser(request, fastify)); }, async (request) => await getCurrentUser(request, fastify));
fastify.get('/undead', { fastify.get('/undead', async function () {
onRequest: [fastify.authenticate]
}, async function () {
return { message: 'nope' }; return { message: 'nope' };
}); });

View File

@ -30,7 +30,7 @@ import {
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { startService, stopService } from '../../../../lib/services/handlers'; import { migrateAppwriteDB, startService, stopService } from '../../../../lib/services/handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -76,6 +76,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply)); fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply));
fastify.post<OnlyId>('/:id/appwrite/migrate', async (request, reply) => await migrateAppwriteDB(request, reply));
}; };
export default root; export default root;

View File

@ -1,8 +1,9 @@
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types'; import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) { export async function listAllSettings(request: FastifyRequest) {
@ -16,8 +17,16 @@ export async function listAllSettings(request: FastifyRequest) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt }) unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
} }
} }
const certificates = await prisma.certificate.findMany({ where: { team: { id: teamId } } })
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt })
}
return { return {
settings, settings,
certificates: cns,
sshKeys: unencryptedKeys sshKeys: unencryptedKeys
} }
} catch ({ status, message }) { } catch ({ status, message }) {
@ -118,7 +127,7 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) { export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try { try {
const { id } = request.body; const { id } = request.body;
await prisma.sshKey.delete({ where: { id } }) await prisma.sshKey.delete({ where: { id } })
@ -127,3 +136,14 @@ export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply:
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`)
await prisma.certificate.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,6 +1,9 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers'; import { X509Certificate } from 'node:crypto';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@ -15,7 +18,42 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request)); fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply)); fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply)); fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post('/upload', async (request) => {
try {
const teamId = request.user.teamId;
const certificates = await prisma.certificate.findMany({})
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push(x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''))
}
const parts = await request.files()
let key = null
let cert = null
for await (const part of parts) {
const name = part.fieldname
if (name === 'key') key = (await part.toBuffer()).toString()
if (name === 'cert') cert = (await part.toBuffer()).toString()
}
const x509 = new X509Certificate(cert);
const cn = x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', '')
if (cns.includes(cn)) {
throw {
message: `A certificate with ${cn} common name already exists.`
}
}
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
await prisma.applicationSettings.updateMany({ where: { application: { AND: [{ fqdn: { endsWith: cn } }, { fqdn: { startsWith: 'https' } }] } }, data: { isCustomSSL: true } })
return { message: 'Certificated uploaded' }
} catch ({ status, message }) {
return errorHandler({ status, message });
}
});
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
// fastify.get('/certificates', async (request) => await getCertificates(request))
}; };
export default root; export default root;

View File

@ -42,3 +42,8 @@ export interface DeleteSSHKey {
id: string id: string
} }
} }
export interface OnlyIdInBody {
Body: {
id: string
}
}

View File

@ -6,7 +6,7 @@ import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types"; import { OnlyId } from "../../../types";
function configureMiddleware( function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL },
traefik traefik
) { ) {
if (isHttps) { if (isHttps) {
@ -55,7 +55,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@ -66,7 +66,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@ -99,7 +99,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@ -178,7 +178,19 @@ function configureMiddleware(
export async function traefikConfiguration(request, reply) { export async function traefikConfiguration(request, reply) {
try { try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } })
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = { const traefik = {
tls: {
certificates: parsedCertificates
},
http: { http: {
routers: {}, routers: {},
services: {}, services: {},
@ -224,7 +236,7 @@ export async function traefikConfiguration(request, reply) {
port, port,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts } settings: { previews, dualCerts, isCustomSSL }
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
@ -244,7 +256,8 @@ export async function traefikConfiguration(request, reply) {
isRunning, isRunning,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
if (previews) { if (previews) {
@ -267,7 +280,8 @@ export async function traefikConfiguration(request, reply) {
nakedDomain, nakedDomain,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
} }
@ -534,7 +548,19 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) { export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
try { try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } })
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = { const traefik = {
tls: {
certificates: parsedCertificates
},
http: { http: {
routers: {}, routers: {},
services: {}, services: {},

View File

@ -42,13 +42,14 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dayjs": "1.11.5",
"@sveltejs/adapter-static": "1.0.0-next.39", "@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7", "@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8", "cuid": "2.1.8",
"daisyui": "2.24.2", "daisyui": "2.24.2",
"dayjs": "1.11.5",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",
"sveltekit-i18n": "2.2.2" "sveltekit-i18n": "2.2.2"
} }

View File

@ -3,33 +3,35 @@ import Cookies from 'js-cookie';
export function getAPIUrl() { export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
return newURL return newURL;
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
return `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
} }
return dev ? 'http://localhost:3001' : 'http://localhost:3000'; return dev
? 'http://localhost:3001'
: 'http://localhost:3000';
} }
export function getWebhookUrl(type: string) { export function getWebhookUrl(type: string) {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
@ -37,7 +39,7 @@ export function getWebhookUrl(type: string) {
async function send({ async function send({
method, method,
path, path,
data = {}, data = null,
headers, headers,
timeout = 120000 timeout = 120000
}: { }: {
@ -51,7 +53,7 @@ async function send({
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(), timeout);
const opts: any = { method, headers: {}, body: null, signal: controller.signal }; const opts: any = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
const parsedData = data; const parsedData = data;
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (value === '') { if (value === '') {
@ -83,7 +85,9 @@ async function send({
if (dev && !path.startsWith('https://')) { if (dev && !path.startsWith('https://')) {
path = `${getAPIUrl()}${path}`; path = `${getAPIUrl()}${path}`;
} }
if (method === 'POST' && data && !opts.body) {
opts.body = data;
}
const response = await fetch(`${path}`, opts); const response = await fetch(`${path}`, opts);
clearTimeout(id); clearTimeout(id);
@ -103,7 +107,11 @@ async function send({
return {}; return {};
} }
if (!response.ok) { if (!response.ok) {
if (response.status === 401 && !path.startsWith('https://api.github') && !path.includes('/v4/user')) { if (
response.status === 401 &&
!path.startsWith('https://api.github') &&
!path.includes('/v4/user')
) {
Cookies.remove('token'); Cookies.remove('token');
} }
@ -126,7 +134,7 @@ export function del(
export function post( export function post(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown> | FormData,
headers?: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
return send({ method: 'POST', path, data, headers }); return send({ method: 'POST', path, data, headers });

View File

@ -13,8 +13,9 @@
export let id: string; export let id: string;
export let name: string; export let name: string;
export let placeholder = ''; export let placeholder = '';
export let inputStyle = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack'; let disabledClass = 'bg-coolback disabled:bg-coolblack w-full';
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
function copyToClipboard() { function copyToClipboard() {
@ -32,6 +33,7 @@
{#if !isPasswordField || showPassword} {#if !isPasswordField || showPassword}
{#if textarea} {#if textarea}
<textarea <textarea
style={inputStyle}
rows="5" rows="5"
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
@ -47,6 +49,7 @@
> >
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
type="text" type="text"
class:pr-10={true} class:pr-10={true}
@ -63,6 +66,7 @@
{/if} {/if}
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}
@ -78,7 +82,7 @@
/> />
{/if} {/if}
<div class="absolute top-0 right-0 m-3 cursor-pointer text-stone-600 hover:text-white"> <div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
<div class="flex space-x-2"> <div class="flex space-x-2">
{#if isPasswordField} {#if isPasswordField}
<div on:click={() => (showPassword = !showPassword)}> <div on:click={() => (showPassword = !showPassword)}>

View File

@ -10,23 +10,10 @@
.slice(-16); .slice(-16);
</script> </script>
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs"> <a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
<svg <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
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="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"
/>
<line x1="13" y1="8" x2="15" y2="8" />
<line x1="13" y1="12" x2="15" y2="12" />
</svg> </svg>
</a> </a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip> <Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>

View File

@ -1,26 +1,38 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; // import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; // import Tooltip from './Tooltip.svelte';
export let explanation = ''; export let explanation = '';
let id: any; export let position = 'dropdown-right'
let self: any; // let id: any;
onMount(() => { // let self: any;
id = `info-${self.offsetLeft}-${self.offsetTop}`; // onMount(() => {
}); // id = `info-${self.offsetLeft}-${self.offsetTop}`;
// });
</script> </script>
<div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}> <div class={`dropdown dropdown-end ${position}`}>
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</label>
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
<div class="card-body">
<!-- <h2 class="card-title">You needed more info?</h2> -->
<p class="text-xs font-normal">{@html explanation}</p>
</div>
</div>
</div>
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
<svg <svg
fill="none" fill="none"
height="18" height="14"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="1.5" stroke-width="1.4"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="18" width="14"
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path ><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
/><circle cx="12" cy="17" r=".5" /> /><circle cx="12" cy="17" r=".5" />
@ -28,4 +40,4 @@
</div> </div>
{#if id} {#if id}
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip> <Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
{/if} {/if} -->

View File

@ -15,9 +15,13 @@
<div class="flex items-center py-4 pr-8"> <div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col"> <div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base"> <!-- svelte-ignore a11y-label-has-associated-control -->
{title}<Explaner explanation={description} /> <label>
</div> {title}
{#if description && description !== ''}
<Explaner explanation={description} />
{/if}
</label>
</div> </div>
</div> </div>
<div class:text-center={isCenter} class="flex justify-center"> <div class:text-center={isCenter} class="flex justify-center">

View File

@ -2,6 +2,11 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let type = 'info'; export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-coollabs';
}
}
</script> </script>
<div <div
@ -10,8 +15,7 @@
on:focus={() => dispatch('pause')} on:focus={() => dispatch('pause')}
on:mouseout={() => dispatch('resume')} on:mouseout={() => dispatch('resume')}
on:blur={() => dispatch('resume')} on:blur={() => dispatch('resume')}
class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer" class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
class:bg-coollabs={type === 'success'}
class:alert-error={type === 'error'} class:alert-error={type === 'error'}
class:alert-info={type === 'info'} class:alert-info={type === 'info'}
> >

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import Toast from './Toast.svelte'; import Toast from './Toast.svelte';
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store'; import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
@ -7,7 +6,7 @@
{#if $toasts} {#if $toasts}
<section> <section>
<article class="toast toast-top toast-end rounded-none" role="alert" transition:fade> <article class="toast toast-top toast-end rounded-none px-10" role="alert" >
{#each $toasts as toast (toast.id)} {#each $toasts as toast (toast.id)}
<Toast <Toast
type={toast.type} type={toast.type}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Tooltip } from 'flowbite-svelte'; import { Tooltip } from 'flowbite-svelte';
export let placement = 'bottom'; export let placement = 'bottom';
export let color = 'bg-coollabs text-left'; export let color = 'bg-coollabs font-thin text-left';
export let triggeredBy = '#tooltip-default'; export let triggeredBy = '#tooltip-default';
</script> </script>

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { dev } from '$app/env'; import { dev } from '$app/env';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { addToast, appSession, features } from '$lib/store'; import { addToast, appSession, features, updateLoading, isUpdateAvailable } from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common'; import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; import Tooltip from './Tooltip.svelte';
let isUpdateAvailable = false;
let updateStatus: any = { let updateStatus: any = {
found: false, found: false,
loading: false, loading: false,
@ -58,37 +57,41 @@
if ($appSession.userId) { if ($appSession.userId) {
const overrideVersion = $features.latestVersion; const overrideVersion = $features.latestVersion;
if ($appSession.teamId === '0') { if ($appSession.teamId === '0') {
if ($updateLoading === true) return;
try { try {
$updateLoading = true;
const data = await get(`/update`); const data = await get(`/update`);
if (overrideVersion || data?.isUpdateAvailable) { if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion; latestVersion = overrideVersion || data.latestVersion;
if (overrideVersion) { if (overrideVersion) {
isUpdateAvailable = true; $isUpdateAvailable = true;
} else { } else {
isUpdateAvailable = data.isUpdateAvailable; $isUpdateAvailable = data.isUpdateAvailable;
} }
} }
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$updateLoading = false;
} }
} }
} }
}); });
</script> </script>
<div class="py-2"> <div class="py-0 lg:py-2">
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
{#if isUpdateAvailable} {#if $isUpdateAvailable}
<button <button
id="update" id="update"
disabled={updateStatus.success === false} disabled={updateStatus.success === false}
on:click={update} on:click={update}
class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
> >
{#if updateStatus.loading} {#if updateStatus.loading}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="lds-heart h-9 w-8" class="lds-heart h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -102,9 +105,10 @@
/> />
</svg> </svg>
{:else if updateStatus.success === null} {:else if updateStatus.success === null}
<div class="flex items-center justify-center space-x-2">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-9 w-8" class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -118,8 +122,10 @@
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="8" /> <line x1="16" y1="12" x2="12" y2="8" />
</svg> </svg>
<span class="flex lg:hidden">Update available</span>
</div>
{:else if updateStatus.success} {:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-8 w-8"
><path ><path
fill="#DD2E44" fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@ -184,7 +190,9 @@
> >
{/if} {/if}
</button> </button>
<Tooltip triggeredBy="#update" placement="right" color="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500">New Version Available!</Tooltip> <Tooltip triggeredBy="#update" placement="right" color="bg-coolgray-200 text-white"
>New Version Available!</Tooltip
>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import { post } from '$lib/api';
let cert: any;
let key: any;
async function submitForm() {
const formData = new FormData();
formData.append('cert', cert[0]);
formData.append('key', key[0]);
await post('/upload', formData);
}
</script>
<form on:submit|preventDefault={submitForm}>
<label for="cert">Certificate</label>
<input id="cert" type="file" required name="cert" bind:files={cert} />
<label for="key">Private Key</label>
<input id="key" type="file" required name="key" bind:files={key} />
<br />
<input type="submit" />
</form>

View File

@ -72,19 +72,16 @@
{:else} {:else}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
{/if} {/if}
{#if server.remoteEngine}
<div <div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
class="absolute top-0 right-0 text-xl font-bold uppercase bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 p-1 rounded m-2"
>
BETA
</div>
{/if}
<div class="w-full flex flex-row space-x-4">
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate"> <h1 class="font-bold text-lg lg:text-xl truncate">
{server.name} {server.name}
{#if server.remoteEngine}
<span class="badge bg-coollabs-gradient rounded text-white"> BETA </span>
{/if}
</h1> </h1>
<div class="text-xs "> <div class="text-xs">
{#if server?.remoteIpAddress} {#if server?.remoteIpAddress}
<h2>{server?.remoteIpAddress}</h2> <h2>{server?.remoteIpAddress}</h2>
{:else} {:else}

View File

@ -9,15 +9,8 @@
viewBox="0 0 309.88 252.72" viewBox="0 0 309.88 252.72"
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'} class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
> >
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path <path
class="cls-1" fill="#fff"
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z" d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
transform="translate(-7.45 -9.1)" transform="translate(-7.45 -9.1)"
/> />

View File

@ -5,7 +5,7 @@
<svg <svg
viewBox="0 0 700 240" viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 h-28 mx-auto'} class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path ><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F" fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z" d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="grafana logo"
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/grafana.png"
/>

View File

@ -42,4 +42,8 @@
<Icons.Searxng {isAbsolute} /> <Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'} {:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} /> <Icons.Weblate {isAbsolute} />
{:else if type === 'grafana'}
<Icons.Grafana {isAbsolute} />
{:else if type === 'trilium'}
<Icons.Trilium {isAbsolute} />
{/if} {/if}

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="trilium logo"
class={isAbsolute ? 'w-9 h-9 absolute top-3 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/trilium.png"
/>

View File

@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'} class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-10 h-10 mx-auto'}
version="1.1" version="1.1"
viewBox="0 0 300 300" viewBox="0 0 300 300"
><linearGradient ><linearGradient

View File

@ -18,3 +18,5 @@ export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte'; export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte'; export { default as Weblate } from './Weblate.svelte';
export { default as Grafana } from './Grafana.svelte';
export { default as Trilium } from './Trilium.svelte'

View File

@ -88,7 +88,7 @@
"removing": "Removing...", "removing": "Removing...",
"remove_domain": "Remove domain", "remove_domain": "Remove domain",
"public_port_range": "Public Port Range", "public_port_range": "Public Port Range",
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings font-bold'>9000-9100</span>", "public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings '>9000-9100</span>",
"no_actions_available": "No actions available", "no_actions_available": "No actions available",
"admin_api_key": "Admin API key" "admin_api_key": "Admin API key"
}, },
@ -144,8 +144,8 @@
}, },
"preview": { "preview": {
"need_during_buildtime": "Need during buildtime?", "need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.", "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-settings '>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.", "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-settings '>staging</span> environments.",
"redeploy": "Redeploy", "redeploy": "Redeploy",
"no_previews_available": "No previews available" "no_previews_available": "No previews available"
}, },
@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>." "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
@ -194,14 +194,14 @@
"application": "Application", "application": "Application",
"url_fqdn": "URL (FQDN)", "url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",
"start_command": "Start Command", "start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.", "directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings '>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.", "publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> or <span class='text-settings '>public</span>.",
"features": "Features", "features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment", "enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
@ -209,7 +209,7 @@
"expose_a_port": "Expose a port", "expose_a_port": "Expose a port",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs", "debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings font-bold'>Sensitive information</span> could be visible and saved in logs.", "enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings '>Sensitive information</span> could be visible and saved in logs.",
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.", "cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
"no_applications_found": "No applications found", "no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file", "secret__batch_dot_env": "Paste .env file",
@ -223,7 +223,7 @@
"set_public": "Set it public", "set_public": "Set it public",
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!", "warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
"change_append_only_mode": "Change append only mode", "change_append_only_mode": "Change append only mode",
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>", "warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class=' text-white'>Database restart is required.</span>",
"select_database_type": "Select a Database type", "select_database_type": "Select a Database type",
"select_database_version": "Select a Database version", "select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?", "confirm_stop": "Are you sure you would like to stop {{name}}?",
@ -275,7 +275,7 @@
"application_id": "Application ID", "application_id": "Application ID",
"group_name": "Group Name", "group_name": "Group Name",
"oauth_id": "OAuth ID", "oauth_id": "OAuth ID",
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-settings' >in the URL</span> of your GitLab OAuth Application.", "oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class=' text-settings' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab", "register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": { "gitlab": {
"self_hosted": "Instance-wide application (self-hosted)", "self_hosted": "Instance-wide application (self-hosted)",
@ -290,7 +290,7 @@
}, },
"services": { "services": {
"all_email_verified": "All emails are verified. You can login now.", "all_email_verified": "All emails are verified. You can login now.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted." "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='text-settings'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
}, },
"service": { "service": {
"stop_service": "Stop", "stop_service": "Stop",
@ -306,15 +306,15 @@
"change_language": "Change Language", "change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.", "permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed", "domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.", "ssl_explainer": "If you specify <span class='text-settings'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings '>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.", "must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?", "registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.", "registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings", "coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.", "credential_stat_explainer": "Credentials for <a class=\"text-white \" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?", "auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.", "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.",
"is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled": "DNS check enabled?",
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel." "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
}, },
@ -324,9 +324,9 @@
"delete": "Delete", "delete": "Delete",
"member": "member(s)", "member": "member(s)",
"root": "(root)", "root": "(root)",
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.", "invited_with_permissions": "Invited to <span class=\" text-settings\">{{teamName}}</span> with <span class=\" text-rose-600\">{{permission}}</span> permission.",
"members": "Members", "members": "Members",
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).", "root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
"permission": "Permission", "permission": "Permission",
"you": "(You)", "you": "(You)",
"promote_to": "Promote to {{grade}}", "promote_to": "Promote to {{grade}}",

View File

@ -26,7 +26,8 @@ interface AddToast {
message: string, message: string,
timeout?: number | undefined timeout?: number | undefined
} }
export const updateLoading: Writable<boolean> = writable(false);
export const isUpdateAvailable: Writable<boolean> = writable(false);
export const search: any = writable('') export const search: any = writable('')
export const loginEmail: Writable<string | undefined> = writable() export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({

View File

@ -195,6 +195,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
export const buildPacks = [ export const buildPacks = [
{ {
name: 'node', name: 'node',
type: 'base',
fancyName: 'Node.js', fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@ -202,6 +203,7 @@ export const buildPacks = [
}, },
{ {
name: 'static', name: 'static',
type: 'base',
fancyName: 'Static', fancyName: 'Static',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700', color: 'bg-orange-700',
@ -210,6 +212,7 @@ export const buildPacks = [
{ {
name: 'php', name: 'php',
type: 'base',
fancyName: 'PHP', fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700', color: 'bg-indigo-700',
@ -217,6 +220,8 @@ export const buildPacks = [
}, },
{ {
name: 'laravel', name: 'laravel',
type: 'specific',
base: 'php',
fancyName: 'Laravel', fancyName: 'Laravel',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700', color: 'bg-indigo-700',
@ -224,6 +229,7 @@ export const buildPacks = [
}, },
{ {
name: 'docker', name: 'docker',
type: 'base',
fancyName: 'Docker', fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700', color: 'bg-sky-700',
@ -231,6 +237,8 @@ export const buildPacks = [
}, },
{ {
name: 'svelte', name: 'svelte',
type: 'specific',
base: 'node',
fancyName: 'Svelte', fancyName: 'Svelte',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700', color: 'bg-orange-700',
@ -238,6 +246,8 @@ export const buildPacks = [
}, },
{ {
name: 'vuejs', name: 'vuejs',
type: 'specific',
base: 'node',
fancyName: 'VueJS', fancyName: 'VueJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@ -245,6 +255,8 @@ export const buildPacks = [
}, },
{ {
name: 'nuxtjs', name: 'nuxtjs',
type: 'specific',
base: 'node',
fancyName: 'NuxtJS', fancyName: 'NuxtJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@ -252,6 +264,8 @@ export const buildPacks = [
}, },
{ {
name: 'gatsby', name: 'gatsby',
type: 'specific',
base: 'node',
fancyName: 'Gatsby', fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@ -259,6 +273,8 @@ export const buildPacks = [
}, },
{ {
name: 'astro', name: 'astro',
type: 'specific',
base: 'node',
fancyName: 'Astro', fancyName: 'Astro',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700', color: 'bg-pink-700',
@ -266,14 +282,17 @@ export const buildPacks = [
}, },
{ {
name: 'eleventy', name: 'eleventy',
type: 'specific',
base: 'node',
fancyName: 'Eleventy', fancyName: 'Eleventy',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700', color: 'bg-red-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'react', name: 'react',
type: 'specific',
base: 'node',
fancyName: 'React', fancyName: 'React',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@ -281,6 +300,8 @@ export const buildPacks = [
}, },
{ {
name: 'preact', name: 'preact',
type: 'specific',
base: 'node',
fancyName: 'Preact', fancyName: 'Preact',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@ -288,6 +309,8 @@ export const buildPacks = [
}, },
{ {
name: 'nextjs', name: 'nextjs',
type: 'specific',
base: 'node',
fancyName: 'NextJS', fancyName: 'NextJS',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@ -295,6 +318,8 @@ export const buildPacks = [
}, },
{ {
name: 'nestjs', name: 'nestjs',
type: 'specific',
base: 'node',
fancyName: 'NestJS', fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700', color: 'bg-red-700',
@ -302,6 +327,7 @@ export const buildPacks = [
}, },
{ {
name: 'rust', name: 'rust',
type: 'base',
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700', color: 'bg-pink-700',
@ -309,6 +335,7 @@ export const buildPacks = [
}, },
{ {
name: 'python', name: 'python',
type: 'base',
fancyName: 'Python', fancyName: 'Python',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@ -316,6 +343,7 @@ export const buildPacks = [
}, },
{ {
name: 'deno', name: 'deno',
type: 'base',
fancyName: 'Deno', fancyName: 'Deno',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@ -323,6 +351,7 @@ export const buildPacks = [
}, },
{ {
name: 'heroku', name: 'heroku',
type: 'base',
fancyName: 'Heroku', fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700', hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700', color: 'bg-purple-700',

View File

@ -16,7 +16,7 @@
} }
</script> </script>
<div class="dropdown dropdown-hover"> <div class="dropdown dropdown-bottom">
<slot> <slot>
<label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs"> <label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs">
<svg <svg

View File

@ -107,10 +107,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Coolify</title>
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<title>Coolify</title>
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
{:else if $appSession.whiteLabeledDetails.icon} {:else if $appSession.whiteLabeledDetails.icon}
<title>Coolify</title>
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} /> <link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
{/if} {/if}
</svelte:head> </svelte:head>
@ -120,8 +121,11 @@
<PageLoader /> <PageLoader />
</div> </div>
{/if} {/if}
{#if $appSession.userId} <div class="drawer">
<nav class="nav-main"> <input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
{#if $appSession.userId}
<nav class="nav-main hidden lg:block z-20">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<div class="mb-2 mt-4 h-10 w-10"> <div class="mb-2 mt-4 h-10 w-10">
@ -137,7 +141,7 @@
id="dashboard" id="dashboard"
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
class="icons hover:text-white" class="icons hover:text-pink-500"
class:text-pink-500={$page.url.pathname === '/'} class:text-pink-500={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'} class:bg-coolgray-500={$page.url.pathname === '/'}
class:bg-coolgray-200={!($page.url.pathname === '/')} class:bg-coolgray-200={!($page.url.pathname === '/')}
@ -164,7 +168,7 @@
id="servers" id="servers"
sveltekit:prefetch sveltekit:prefetch
href="/servers" href="/servers"
class="icons hover:text-white" class="icons hover:text-sky-500"
class:text-sky-500={$page.url.pathname === '/servers'} class:text-sky-500={$page.url.pathname === '/servers'}
class:bg-coolgray-500={$page.url.pathname === '/servers'} class:bg-coolgray-500={$page.url.pathname === '/servers'}
class:bg-coolgray-200={!($page.url.pathname === '/servers')} class:bg-coolgray-200={!($page.url.pathname === '/servers')}
@ -191,17 +195,17 @@
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip> <Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip> <Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" /> <div class="flex-1" />
<div class="lg:block hidden">
<UpdateAvailable /> <UpdateAvailable />
</div>
<div class="flex flex-col space-y-2 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
id="iam" id="iam"
sveltekit:prefetch sveltekit:prefetch
href="/iam" href="/iam"
class="icons bg-coolgray-200" class="icons hover:text-iam"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname === '/iam'} class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-200={!($page.url.pathname === '/iam')}
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -219,16 +223,13 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /> <path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg> </svg>
</a> </a>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<a <a
id="settings" id="settings"
sveltekit:prefetch sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'} href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class="icons bg-coolgray-200" class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname === '/settings'} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-200={!($page.url.pathname === '/settings')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -247,11 +248,12 @@
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
</svg> </svg>
</a> </a>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
>Settings</Tooltip
>
<div id="logout" class="icons bg-coolgray-200 hover:text-error" on:click={logout}> <div
id="logout"
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
on:click={logout}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8" class="ml-1 h-8 w-8"
@ -269,8 +271,6 @@
<path d="M7 12h14l-3 -3m0 6l3 -3" /> <path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg> </svg>
</div> </div>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
<div <div
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white" class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
> >
@ -288,9 +288,170 @@
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span >Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
> >
{/if} {/if}
{/if} {/if}
<main> <div
<div class={$appSession.userId ? 'pl-14 lg:pl-20' : null}> class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
class:hidden={!$appSession.userId}
>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
</div>
</div>
<main>
<div class={$appSession.userId ? 'lg:pl-16' : null}>
<slot /> <slot />
</div> </div>
</main> </main>
</div>
<div class="drawer-side">
<label for="main-drawer" class="drawer-overlay w-full" />
<ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4 ">
<li>
<a
class="no-underline icons hover:text-white hover:bg-pink-500"
sveltekit:prefetch
href="/"
class:bg-pink-500={$page.url.pathname === '/'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
/>
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg>
Dashboard
</a>
</li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch
href="/servers"
class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8"
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" />
<rect x="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
Servers
</a>
</li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-iam"
href="/iam"
class:bg-iam={$page.url.pathname.startsWith('/iam')}
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-8 w-8"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="9" cy="7" r="4" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
IAM
</a>
</li>
<li>
<a
class="no-underline icons hover:text-black hover:bg-settings"
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class:bg-settings={$page.url.pathname.startsWith('/settings')}
class:text-black={$page.url.pathname.startsWith('/settings')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-8 w-8"
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.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"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Settings
</a>
</li>
<li class="flex-1 bg-transparent" />
<div class="block lg:hidden">
<UpdateAvailable />
</div>
<li>
<div class="no-underline icons hover:bg-error" on:click={logout}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8"
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="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
/>
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>
<div class="-ml-1">Logout</div>
</div>
</li>
<li class="w-full">
<a
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
>
</li>
</ul>
</div>
</div>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>

View File

@ -0,0 +1,289 @@
<script lang="ts">
export let application: any;
import { status } from '$lib/store';
import { page } from '$app/stores';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>Configuration</span>
</li>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<li>
<a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="no-underline"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="w-6 h-6">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-6 h-6">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
Open on Git
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
/>
</svg>
</a>
</li>
{/if}
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}`}>
<a href={`/applications/${$page.params.id}`} class="no-underline w-full"
><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="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Build & Deploy</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/secrets`}
>
<a href={`/applications/${$page.params.id}/secrets`} class="no-underline w-full"
><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="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg>Secrets</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/storages`}
>
<a href={`/applications/${$page.params.id}/storages`} class="no-underline w-full"
><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" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>Persistent Volumes</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/features`}
>
<a href={`/applications/${$page.params.id}/features`} class="no-underline w-full"
><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" />
<polyline points="13 3 13 10 19 10 11 21 11 14 5 14 13 3" />
</svg>Features</a
>
</li>
<li class="menu-title">
<span>Logs</span>
</li>
<li
class:text-stone-600={!$status.application.isRunning}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-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="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>Application</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs/build`}
>
<a href={`/applications/${$page.params.id}/logs/build`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-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" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>Build</a
>
</li>
<li class="menu-title">
<span>Advanced</span>
</li>
<li
class="rounded"
class:text-stone-600={!$status.application.isRunning}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><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="M3 12h4l3 8l4 -16l3 8h4" />
</svg>Monitoring</a
>
</li>
{#if !application.settings.isBot}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/previews`}
>
<a href={`/applications/${$page.params.id}/previews`} class="no-underline w-full"
><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" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg>Preview Deployments</a
>
</li>
{/if}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`}
>
<a href={`/applications/${$page.params.id}/danger`} class="no-underline w-full"
><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="M12 9v2m0 4v.01" />
<path
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
/>
</svg>Danger Zone</a
>
</li>
</ul>

View File

@ -0,0 +1,128 @@
<script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = '';
export let value = '';
export let isBuildSecret = false;
import { page } from '$app/stores';
import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store';
import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function updatePreviewSecret() {
try {
await put(`/applications/${id}/secrets/preview`, {
name,
value
});
addToast({
message: 'Secret updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
{/if}
<input
id="secretName"
readonly
disabled
value={name}
required
placeholder="EXAMPLE_VARIABLE"
class=" w-full"
/>
</div>
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
{/if}
<CopyPasswordField
id="secretValue"
name="secretValue"
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
/>
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
aria-pressed="false"
class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
>
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
{/if}
<div class="flex justify-center h-full items-center pt-3">
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,115 +1,109 @@
<script lang="ts"> <script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = ''; export let name = '';
export let value = ''; export let value = '';
export let isBuildSecret = false; export let isBuildSecret = false;
export let isNewSecret = false; export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret: any = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { del } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const { id } = $page.params; const { id } = $page.params;
async function removeSecret() { function cleanupState() {
try {
await del(`/applications/${id}/secrets`, { name });
dispatch('refresh');
if (isNewSecret) { if (isNewSecret) {
name = ''; name = '';
value = ''; value = '';
isBuildSecret = false; isBuildSecret = false;
} }
}
async function removeSecret() {
try {
await del(`/applications/${id}/secrets`, { name });
cleanupState();
addToast({ addToast({
message: 'Secret removed.', message: 'Secret removed.',
type: 'success' type: 'success'
}); });
} catch (error) {
return errorNotification(error);
}
}
async function createSecret(isNew: any) {
try {
if (isNew) {
if (!name || !value) return;
}
if (value === undefined && isPRMRSecret) {
return
}
if (value === '' && !isPRMRSecret) {
throw new Error('Value is required.')
}
await saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId: id
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
addToast({
message: 'Secret added.',
type: 'success'
});
} else {
addToast({
message: 'Secret updated.',
type: 'success'
});
}
dispatch('refresh'); dispatch('refresh');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
} }
async function setSecretValue() { async function addNewSecret() {
if (!isPRMRSecret) { try {
isBuildSecret = !isBuildSecret; if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!isNewSecret) { if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
await saveSecret({ await post(`/applications/${id}/secrets`, {
isNew: isNewSecret,
name, name,
value, value,
isBuildSecret, isBuildSecret
isPRMRSecret, });
isNewSecret, cleanupState();
applicationId: id addToast({
message: 'Secret added.',
type: 'success'
});
dispatch('refresh');
} catch (error) {
return errorNotification(error);
}
}
async function updateSecret({
changeIsBuildSecret = false
}: { changeIsBuildSecret?: boolean } = {}) {
if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
if (isNewSecret) return
try {
await put(`/applications/${id}/secrets`, {
name,
value,
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
}); });
addToast({ addToast({
message: 'Secret updated.', message: 'Secret updated.',
type: 'success' type: 'success'
}); });
} dispatch('refresh');
} catch (error) {
return errorNotification(error);
} }
} }
</script> </script>
<td> <div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
{/if}
<input <input
id={isNewSecret ? 'secretName' : 'secretNameNew'} id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name} bind:value={name}
required required
placeholder="EXAMPLE_VARIABLE" placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret} readonly={!isNewSecret}
class:bg-transparent={!isNewSecret} class=" w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
class:cursor-not-allowed={!isNewSecret} class:cursor-not-allowed={!isNewSecret}
/> />
</td> </div>
<td> <div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
{/if}
<CopyPasswordField <CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'} id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'} name={isNewSecret ? 'secretValue' : 'secretValueNew'}
@ -117,17 +111,20 @@
bind:value bind:value
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
/> />
</td> </div>
<td class="text-center"> <div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button <button
on:click={setSecretValue} on:click={() => updateSecret({ changeIsBuildSecret: true })}
aria-pressed="false" aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret} class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret} class:bg-stone-700={!isBuildSecret}
class:opacity-50={isPRMRSecret}
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
> >
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span> <span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<span <span
@ -165,28 +162,28 @@
</span> </span>
</span> </span>
</button> </button>
</td> </div>
<td> </div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
{/if}
<div class="flex justify-center h-full items-center pt-3">
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn bg-applications btn-sm" on:click={() => createSecret(true)} <button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
>{$t('forms.add')}</button
>
</div> </div>
{:else} {:else}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn bg-application btn-sm" on:click={() => createSecret(false)} <button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
>{$t('forms.set')}</button
>
</div> </div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end"> <div class="flex justify-center items-end">
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeSecret} <button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
>{$t('forms.remove')}</button </div>
>
</div> </div>
{/if} {/if}
</div> </div>
{/if} </div>
</td> </div>

View File

@ -59,32 +59,36 @@
} }
</script> </script>
<td> <div class="w-full font-bold grid gap-2">
<div class="flex flex-col pb-2">
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
<input <input
class="w-full lg:w-64"
bind:value={storage.path} bind:value={storage.path}
required required
placeholder="eg: /sqlite.db" placeholder="eg: /sqlite.db"
/> />
</td>
<td>
{#if isNew} {#if isNew}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center w-full lg:w-64">
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(true)} <button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button >{$t('forms.add')}</button
> >
</div> </div>
{:else} {:else}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(false)} <button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button >{$t('forms.set')}</button
> >
</div> </div>
<div class="flex justify-center items-end"> <div class="flex justify-center">
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeStorage} <button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button >{$t('forms.remove')}</button
> >
</div> </div>
</div> </div>
{/if} {/if}
</td> </div>
</div>
</div>

View File

@ -20,7 +20,7 @@
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
redirect: '/applications' redirect: '/'
}; };
} }
const configurationPhase = checkConfiguration(application); const configurationPhase = checkConfiguration(application);
@ -55,7 +55,6 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -72,6 +71,7 @@
} from '$lib/store'; } from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Menu from './_Menu.svelte';
let statusInterval: any; let statusInterval: any;
let forceDelete = false; let forceDelete = false;
@ -99,23 +99,6 @@
} }
} }
async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
$status.application.initialLoading = true;
try {
await del(`/applications/${id}`, { id, force });
return await window.location.assign(`/`);
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
}
}
}
async function restartApplication() { async function restartApplication() {
try { try {
$status.application.initialLoading = true; $status.application.initialLoading = true;
@ -188,39 +171,39 @@
}); });
</script> </script>
<nav class="nav-side"> <div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
{#if $location} <nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4">
<a <div class="title lg:pb-10">
id="open" {#if $page.url.pathname === `/applications/${id}/configuration/source`}
href={$location} Select a Source
target="_blank" {:else if $page.url.pathname === `/applications/${id}/configuration/destination`}
class="icons flex items-center bg-transparent text-sm" Select a Destination
><svg {:else if $page.url.pathname === `/applications/${id}/configuration/repository`}
xmlns="http://www.w3.org/2000/svg" Select a Repository
class="h-6 w-6" {:else if $page.url.pathname === `/applications/${id}/configuration/buildpack`}
viewBox="0 0 24 24" Select a Build Pack
stroke-width="1.5" {:else}
stroke="currentColor" <div class="flex justify-center items-center space-x-2">
fill="none" <div>Configurations</div>
stroke-linecap="round" <div
stroke-linejoin="round" class="badge rounded uppercase"
class:text-green-500={$status.application.isRunning}
class:text-red-500={!$status.application.isRunning}
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> {$status.application.isRunning ? 'Running' : 'Stopped'}
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> </div>
<line x1="10" y1="14" x2="20" y2="4" /> </div>
<polyline points="15 4 20 4 20 9" />
</svg></a
>
<Tooltip triggeredBy="#open">Open</Tooltip>
<div class="border border-coolgray-500 h-8" />
{/if} {/if}
</div>
</nav>
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.application.isExited || $status.application.isRestarting} {#if $status.application.isExited || $status.application.isRestarting}
<a <a
id="applicationerror" id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-error" class="icons bg-transparent text-sm text-error"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
@ -241,35 +224,10 @@
<line x1="12" y1="16" x2="12.01" y2="16" /> <line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
</a> </a>
<Tooltip triggeredBy="#applicationerror">Application exited or restarting!</Tooltip> <Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
<button
id="stop"
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
>
<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" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{/if} {/if}
{#if $status.application.initialLoading} {#if $status.application.initialLoading}
<button <button class="icons animate-spin bg-transparent duration-500 ease-in-out">
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -295,7 +253,7 @@
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error" class="icons bg-transparent text-error"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -319,7 +277,7 @@
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -338,12 +296,11 @@
</button> </button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> <Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button <button
id="forceredeploy" id="forceredeploy"
type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent "
on:click={() => handleDeploySubmit(true)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -362,19 +319,17 @@
/> />
</svg> </svg>
</button> </button>
<Tooltip triggeredBy="#forceredeploy">Force redeploy (without cache)</Tooltip> <Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
</form>
{:else} {:else}
<form on:submit|preventDefault={() => handleDeploySubmit(false)}> {#if $isDeploymentEnabled}
<button <button
id="deploy" class="icons flex items-center font-bold"
type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success" on:click={() => handleDeploySubmit(false)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2 text-green-500"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -385,25 +340,14 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <path d="M7 4v16l13 -8z" />
</svg> </svg>
Deploy
</button> </button>
<Tooltip triggeredBy="#deploy">Deploy</Tooltip> {/if}
</form>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> {#if $location && $status.application.isRunning}
<a <a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
href={$isDeploymentEnabled ? `/applications/${id}` : null} ><svg
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button
disabled={!$isDeploymentEnabled}
id="configurations"
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -414,199 +358,25 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" /> <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="6" y1="4" x2="6" y2="8" /> <line x1="10" y1="14" x2="20" y2="4" />
<line x1="6" y1="12" x2="6" y2="20" /> <polyline points="15 4 20 4 20 9" />
<rect x="10" y="14" width="4" height="4" /> </svg></a
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
> >
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<a
href={$isDeploymentEnabled ? `/applications/${id}/secrets` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<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="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a
href={$isDeploymentEnabled ? `/applications/${id}/storages` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
>
<button
id="persistentstorages"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<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" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
{#if !application.settings.isBot}
<a
href={$isDeploymentEnabled ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button id="previews" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<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" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
<Tooltip triggeredBy="#previews">Previews</Tooltip>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> </div>
<a </div>
href={$isDeploymentEnabled && $status.application.isRunning ? `/applications/${id}/logs` : null} <div
sveltekit:prefetch class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
class="hover:text-sky-500 rounded" class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`} >
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`} {#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
> <nav class="header flex flex-col lg:pt-0 ">
<button <Menu {application} />
id="applicationlogs" </nav>
disabled={!$isDeploymentEnabled || !$status.application.isRunning}
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-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="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
<a
href={$isDeploymentEnabled ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button id="buildlogs" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-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" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
Force Delete
</button>
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
<DeleteIcon />
</button>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if} {/if}
</nav> <div class="pt-0 col-span-0 lg:col-span-3 pb-24">
<slot /> <slot />
</div>
</div>

View File

@ -40,9 +40,13 @@
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}> <form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
<button <button
type="submit" type="submit"
class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name === class="box-selection relative flex flex-col items-center text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
buildPack.name && buildPack.color}" buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span> >
<div>{buildPack.fancyName}</div>
{#if buildPack.base}
<div class="text-xs font-mono">{buildPack.base}</div>
{/if}
{#if !scanning && foundConfig?.name === buildPack.name} {#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs" <span class="absolute bottom-0 pb-2 text-xs"
>{$t('application.configuration.buildpack.choose_this_one')}</span >{$t('application.configuration.buildpack.choose_this_one')}</span

View File

@ -143,7 +143,6 @@
} }
} }
</script> </script>
{#if repositories.length === 0 && loading.repositories === false} {#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center"> <div class="flex-col text-center">
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div> <div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
@ -152,10 +151,9 @@
> >
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center"> <form on:submit|preventDefault={handleSubmit} class="px-10">
<div class="flex-col space-y-3 md:space-y-0 space-x-1"> <div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center">
<div class="flex-row md:flex gap-4"> <div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Repository</label>
<div class="custom-select-wrapper">
<Select <Select
placeholder={loading.repositories placeholder={loading.repositories
? $t('application.configuration.loading_repositories') ? $t('application.configuration.loading_repositories')
@ -170,7 +168,7 @@
/> />
</div> </div>
<input class="hidden" bind:value={selected.projectId} name="projectId" /> <input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Branch</label>
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? $t('application.configuration.loading_branches') ? $t('application.configuration.loading_branches')
@ -185,9 +183,7 @@
isDisabled={loading.branches || !selected.repository} isDisabled={loading.branches || !selected.repository}
isClearable={false} isClearable={false}
/> />
</div> </div></div>
</div>
</div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4"> <div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button <button
class="btn btn-wide" class="btn btn-wide"

View File

@ -413,7 +413,7 @@
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button >{loading.save ? $t('forms.saving') : $t('forms.save')}</button
> >
{#if tryAgain} {#if tryAgain}
<div> <div class="p-5">
An error occured during authenticating with GitLab. Please check your GitLab Source An error occured during authenticating with GitLab. Please check your GitLab Source
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>

View File

@ -21,6 +21,7 @@
}; };
async function loadBranches() { async function loadBranches() {
try { try {
if (!publicRepositoryLink) return
loading.branches = true; loading.branches = true;
publicRepositoryLink = publicRepositoryLink.trim(); publicRepositoryLink = publicRepositoryLink.trim();
const protocol = publicRepositoryLink.split(':')[0]; const protocol = publicRepositoryLink.split(':')[0];
@ -156,40 +157,36 @@
} }
</script> </script>
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-screen-2xl">
<div class="grid grid-flow-row gap-2 px-10"> <form class="flex flex-col" on:submit|preventDefault={loadBranches}>
<div class="flex"> <div class="flex flex-col space-y-2 w-full">
<form class="flex" on:submit|preventDefault={loadBranches}> <div class="flex flex-row space-x-2"><input
<div class="space-y-4"> class="w-full"
<input
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink} bind:value={publicRepositoryLink}
/> />
{#if branchSelectOptions.length > 0} <button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
Load Repository
</button>
</div>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
class="w-full"
placeholder={loading.branches placeholder={loading.branches
? $t('application.configuration.loading_branches') ? $t('application.configuration.loading_branches')
: !publicRepositoryLink : branchSelectOptions.length ===0
? $t('application.configuration.select_a_repository_first') ? 'Please type a repository link first.'
: $t('application.configuration.select_a_branch')} : $t('application.configuration.select_a_branch')}
isWaiting={loading.branches} isWaiting={loading.branches}
showIndicator={!!publicRepositoryLink && !loading.branches} showIndicator={!!publicRepositoryLink && !loading.branches}
id="branches" id="branches"
on:select={saveRepository} on:select={saveRepository}
items={branchSelectOptions} items={branchSelectOptions}
isDisabled={loading.branches || !!!publicRepositoryLink} isDisabled={loading.branches || !ownerName}
isClearable={false} isClearable={false}
/> />
</div> </div>
{/if}
</div> </div>
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit"
>Load Repository</button
>
</form> </form>
</div>
</div>
</div> </div>

View File

@ -254,12 +254,6 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning} {#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold"> <div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight"> <div class="text-xl tracking-tight">
@ -267,18 +261,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-5xl mx-auto "> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Other</div> <div class="title pb-2">Other</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
@ -288,4 +271,24 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Base</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Specific</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
{/if} {/if}

View File

@ -126,7 +126,7 @@
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-4xl p-6"> <div class="mx-auto max-w-6xl p-6">
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div> <div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
<div class="mt-2 grid grid-cols-2 items-center px-4"> <div class="mt-2 grid grid-cols-2 items-center px-4">

View File

@ -63,19 +63,14 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex flex-col justify-center w-full">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold"> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')} {$t('application.configuration.no_configurable_destination')}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -93,7 +88,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each ownDestinations as destination} {#each ownDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>
@ -106,9 +101,9 @@
{/each} {/each}
</div> </div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'} {#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div> <div class="px-6 pb-5 pt-10 title">Other Destinations</div>
{/if} {/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each otherDestinations as destination} {#each otherDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>

View File

@ -36,16 +36,8 @@
import GitlabRepositories from './_GitlabRepositories.svelte'; import GitlabRepositories from './_GitlabRepositories.svelte';
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> {#if application.gitSource.type === 'github'}
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_repository_project')}
</div>
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}
<GithubRepositories {application} /> <GithubRepositories {application} />
{:else if application.gitSource.type === 'gitlab'} {:else if application.gitSource.type === 'gitlab'}
<GitlabRepositories {application} {appId} {settings} /> <GitlabRepositories {application} {appId} {settings} />
{/if} {/if}
</div>

View File

@ -68,12 +68,7 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="max-w-screen-2xl mx-auto px-9">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-8">Git App</div> <div class="title pb-8">Git App</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#if !filteredSources || ownSources.length === 0} {#if !filteredSources || ownSources.length === 0}
@ -103,7 +98,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row "> <div class="flex flex-col lg:flex-row lg:flex-wrap justify-center">
{#each ownSources as source} {#each ownSources as source}
<div class="p-2 relative"> <div class="p-2 relative">
<div class="absolute -m-4"> <div class="absolute -m-4">
@ -147,7 +142,7 @@
<button <button
disabled={source.gitlabApp && !source.gitlabAppId} disabled={source.gitlabApp && !source.gitlabAppId}
type="submit" type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group" class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group w-full lg:w-96"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId} class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId} class:border-l-4={source.gitlabApp && !source.gitlabAppId}
@ -192,7 +187,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex flex-row items-center">
<div class="title py-4">Public Repository</div> <div class="title py-4">Public Repository</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div> </div>

View File

@ -0,0 +1,79 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
const response = await get(`/applications/${params.id}/secrets`);
return {
props: {
application: stuff.application,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import { page } from '$app/stores';
import { del, get } from '$lib/api';
import { t } from '$lib/translations';
import { appSession, status } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
let forceDelete = false;
async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
$status.application.initialLoading = true;
try {
await del(`/applications/${id}`, { id, force });
return await goto('/')
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
}
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Danger Zone</div>
</div>
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Force Delete Application
</button>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class="btn btn-lg btn-error hover:bg-red-700 text-sm"
>
Delete Application
</button>
{/if}
</div>

View File

@ -0,0 +1,160 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
if (stuff?.application?.id) {
return {
props: {
application: stuff.application,
settings: stuff.settings
}
};
}
const response = await get(`/applications/${params.id}`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
export let settings: any;
import { page } from '$app/stores';
import { get, post } from '$lib/api';
import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
isDeploymentEnabled
} from '$lib/store';
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params;
let debug = application.settings.debug;
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
if ($status.application.isRunning) return;
isBot = !isBot;
application.settings.isBot = isBot;
application.fqdn = null;
setLocation(application, settings);
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
try {
await post(`/applications/${id}/settings`, {
previews,
debug,
dualCerts,
isBot,
autodeploy,
isDBBranching,
branch: application.branch,
projectId: application.projectId
});
return addToast({
message: $t('application.settings_saved'),
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
return errorNotification(error);
} finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
}
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Features</div>
</div>
<div class="px-4 lg:pb-10 pb-6">
{#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="autodeploy"
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
{/if}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center w-full">
<Setting
id="debug"
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>
</div>
</div>

View File

@ -48,34 +48,27 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fade } from 'svelte/transition';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
!$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading; !$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
let domainEl: HTMLInputElement;
let loading = false; let loading = false;
let fqdnEl: any = null;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
let forceSave = false; let forceSave = false;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let isCustomSSL = application.settings.isCustomSSL;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot; let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isHttps = application.fqdn && application.fqdn.startsWith('https://');
let isNonWWWDomainOK = false; let isNonWWWDomainOK = false;
let isWWWDomainOK = false; let isWWWDomainOK = false;
@ -94,31 +87,16 @@
} }
]; ];
function containerClass() { function containerClass() {
return 'text-white bg-transparent font-thin px-0'; return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
} }
async function getUsage() {
if (usageLoading) return;
if (!$status.application.isRunning) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage`);
usage = data.usage;
usageLoading = false;
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => { onMount(async () => {
if (window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
await handleSubmit(); await handleSubmit();
} }
// !isBot && domainEl.focus();
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
await getBaseBuildImages(); await getBaseBuildImages();
if (!application.fqdn && fqdnEl) fqdnEl.focus();
}); });
async function getBaseBuildImages() { async function getBaseBuildImages() {
const data = await post(`/applications/images`, { const data = await post(`/applications/images`, {
@ -166,6 +144,9 @@
if (name === 'autodeploy') { if (name === 'autodeploy') {
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.isRunning) return; if ($status.application.isRunning) return;
isBot = !isBot; isBot = !isBot;
@ -184,6 +165,7 @@
isBot, isBot,
autodeploy, autodeploy,
isDBBranching, isDBBranching,
isCustomSSL,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId
}); });
@ -210,6 +192,9 @@
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
@ -239,6 +224,11 @@
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
}); });
if (application.fqdn && application.fqdn.startsWith('https')) {
isHttps = true;
} else {
isHttps = false;
}
} catch (error) { } catch (error) {
//@ts-ignore //@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) { if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
@ -294,89 +284,15 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="w-full">
<div class="-mb-5 flex-col"> <form on:submit|preventDefault={handleSubmit}>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="mx-auto w-full">
Configuration <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
</div> <div class="title font-bold pb-3 ">General</div>
<span class="text-xs">{application.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
<Tooltip triggeredBy="#git">Open on Git</Tooltip>
{/if}
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Application Usage</div>
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm" class="btn btn-sm btn-primary"
type="submit" type="submit"
class:bg-applications={!loading}
class:loading class:loading
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave} class:hover:bg-orange-400={forceSave}
@ -384,18 +300,17 @@
> >
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" bind:value={application.name} required /> <input name="name" id="name" class="w-full" bind:value={application.name} required />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100" <label for="gitSource">{$t('application.git_source')}</label>
>{$t('application.git_source')}</label
>
{#if isDisabled || application.settings.isPublicRepository} {#if isDisabled || application.settings.isPublicRepository}
<input <input
disabled={isDisabled || application.settings.isPublicRepository} disabled={isDisabled || application.settings.isPublicRepository}
class="w-full"
value={application.gitSource.name} value={application.gitSource.name}
/> />
{:else} {:else}
@ -405,17 +320,16 @@
><input ><input
value={application.gitSource.name} value={application.gitSource.name}
id="gitSource" id="gitSource"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500 w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100" <label for="repository">{$t('application.git_repository')}</label>
>{$t('application.git_repository')}</label
>
{#if isDisabled || application.settings.isPublicRepository} {#if isDisabled || application.settings.isPublicRepository}
<input <input
class="w-full"
disabled={isDisabled || application.settings.isPublicRepository} disabled={isDisabled || application.settings.isPublicRepository}
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
/> />
@ -426,155 +340,39 @@
><input ><input
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
id="repository" id="repository"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500 w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100" <label for="buildPack">{$t('application.build_pack')}</label>
>{$t('application.build_pack')}</label
>
{#if isDisabled} {#if isDisabled}
<input class="capitalize" disabled={isDisabled} value={application.buildPack} /> <input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} />
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`} href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
class="no-underline " class="no-underline"
> >
<input <input
value={application.buildPack} value={application.buildPack}
id="buildPack" id="buildPack"
class="cursor-pointer hover:bg-coolgray-500 capitalize" class="cursor-pointer hover:bg-coolgray-500 capitalize w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100" <label for="destination">{$t('application.destination')}</label>
>{$t('application.destination')}</label
>
<div class="no-underline"> <div class="no-underline">
<input <input
value={application.destinationDocker.name} value={application.destinationDocker.name}
id="destination" id="destination"
disabled disabled
class="bg-transparent" class="bg-transparent w-full"
/> />
</div> </div>
</div> </div>
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center">
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
>{$t('application.base_build_image')}
<Explainer
explanation={application.buildPack === 'laravel'
? 'For building frontend assets with webpack.'
: 'Image that will be used during the build process.'}
/>
</label>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseBuildImages"
showIndicator={!$status.application.isRunning}
items={application.baseBuildImages}
on:select={selectBaseBuildImage}
value={application.baseBuildImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseImages"
showIndicator={!$status.application.isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
<div class="grid grid-cols-2 items-center pb-8">
<label for="deploymentType" class="text-base font-bold text-stone-100"
>Deployment Type
<Explainer
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
/></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="deploymentTypes"
showIndicator={!$status.application.isRunning}
items={['static', 'node']}
on:select={selectDeploymentType}
value={application.deploymentType}
isClearable={false}
/>
</div>
</div>
{/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="isDBBranching"
isCenter={false}
bind:setting={isDBBranching}
on:click={() => changeSettings('isDBBranching')}
title="Enable DB Branching"
description="Enable DB Branching"
/>
</div>
{#if isDBBranching}
<button
on:click|stopPropagation|preventDefault={() =>
goto(`/applications/${id}/configuration/database`)}
class="btn btn-sm">Configure Connected Database</button
>
{#if application.connectedDatabase}
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>Base Database
<Explainer
explanation={'The name of the database that will be used as base when branching.'}
/></label
>
<input
name="baseDatabaseBranch"
required
id="baseDatabaseBranch"
bind:value={baseDatabaseBranch}
/>
</div>
<div class="text-center bg-green-600 rounded">
Connected to {application.connectedDatabase.databaseId}
</div>
{/if}
{/if}
{/if}
{/if}
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isBot" id="isBot"
@ -586,21 +384,9 @@
disabled={$status.application.isRunning} disabled={$status.application.isRunning}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/>
</div>
{#if !isBot} {#if !isBot}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center">
<label for="fqdn" class="text-base font-bold text-stone-100" <label for="fqdn"
>{$t('application.url_fqdn')} >{$t('application.url_fqdn')}
<Explainer <Explainer
explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"} explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
@ -608,10 +394,15 @@
</label> </label>
<div> <div>
<input <input
bind:this={fqdnEl}
class="w-full"
required={!application.settings.isBot}
readonly={isDisabled} readonly={isDisabled}
disabled={isDisabled} disabled={isDisabled}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
class:border={!application.settings.isBot && !application.fqdn}
class:border-red-500={!application.settings.isBot && !application.fqdn}
bind:value={application.fqdn} bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io" placeholder="eg: https://coollabs.io"
@ -652,36 +443,178 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/>
</div>
{#if isHttps}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isCustomSSL"
isCenter={false}
bind:setting={isCustomSSL}
title="Use Custom SSL Certificate"
description="Use Custom SSL Certificated added in the Settings/SSL Certificates section. <br><br>By default, the SSL certificate is generated automatically through Let's Encrypt"
on:click={() => changeSettings('isCustomSSL')}
/>
</div>
{/if} {/if}
{/if}
</div>
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5">
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center">
<label for="baseBuildImage"
>{$t('application.base_build_image')}
<Explainer
explanation={application.buildPack === 'laravel'
? 'For building frontend assets with webpack.'
: 'Image that will be used during the build process.'}
/>
</label>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseBuildImages"
showIndicator={!$status.application.isRunning}
items={application.baseBuildImages}
on:select={selectBaseBuildImage}
value={application.baseBuildImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="baseImage"
>{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseImages"
showIndicator={!$status.application.isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
<div class="grid grid-cols-2 items-center pb-8">
<label for="deploymentType"
>Deployment Type
<Explainer
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
/></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="deploymentTypes"
showIndicator={!$status.application.isRunning}
items={['static', 'node']}
on:select={selectDeploymentType}
value={application.deploymentType}
isClearable={false}
/>
</div>
</div>
{/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="isDBBranching"
isCenter={false}
bind:setting={isDBBranching}
on:click={() => changeSettings('isDBBranching')}
title="Enable DB Branching"
description="Enable DB Branching"
/>
</div>
{#if isDBBranching}
<button
on:click|stopPropagation|preventDefault={() =>
goto(`/applications/${id}/configuration/database`)}
class="btn btn-sm">Configure Connected Database</button
>
{#if application.connectedDatabase}
<div class="grid grid-cols-2 items-center">
<label for="baseImage"
>Base Database
<Explainer
explanation={'The name of the database that will be used as base when branching.'}
/></label
>
<input
name="baseDatabaseBranch"
required
id="baseDatabaseBranch"
bind:value={baseDatabaseBranch}
/>
</div>
<div class="text-center bg-green-600 rounded">
Connected to {application.connectedDatabase.databaseId}
</div>
{/if}
{/if}
{/if}
{/if}
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI / ASGI</label> <label for="pythonModule">WSGI / ASGI</label>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} /> <Select
id="wsgi"
items={wsgis}
on:select={selectWSGI}
value={application.pythonWSGI}
/>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label> <label for="pythonModule">Module</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonModule" name="pythonModule"
id="pythonModule" id="pythonModule"
required required
class="w-full"
bind:value={application.pythonModule} bind:value={application.pythonModule}
placeholder={application.pythonWSGI?.toLowerCase() !== 'none' ? 'main' : 'main.py'} placeholder={application.pythonWSGI?.toLowerCase() !== 'none' ? 'main' : 'main.py'}
/> />
</div> </div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable">Variable</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="pythonVariable"
id="pythonVariable" id="pythonVariable"
required required
class="w-full"
bind:value={application.pythonVariable} bind:value={application.pythonVariable}
placeholder="default: app" placeholder="default: app"
/> />
@ -689,13 +622,14 @@
{/if} {/if}
{#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable">Variable</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="pythonVariable"
id="pythonVariable" id="pythonVariable"
required required
class="w-full"
bind:value={application.pythonVariable} bind:value={application.pythonVariable}
placeholder="default: app" placeholder="default: app"
/> />
@ -703,12 +637,13 @@
{/if} {/if}
{/if} {/if}
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pt-4">
<label for="port" class="text-base font-bold text-stone-100" <label for="port"
>{$t('forms.port')} >{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label <Explainer explanation={'The port your application listens on.'} /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="port" name="port"
@ -718,13 +653,14 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-4">
<label for="exposePort" class="text-base font-bold text-stone-100" <label for="exposePort"
>Exposed Port <Explainer >Exposed Port <Explainer
explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
/></label /></label
> >
<input <input
class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning} readonly={!$appSession.isAdmin && !$status.application.isRunning}
disabled={isDisabled} disabled={isDisabled}
name="exposePort" name="exposePort"
@ -735,10 +671,9 @@
</div> </div>
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100" <label for="installCommand">{$t('application.install_command')}</label>
>{$t('application.install_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="installCommand" name="installCommand"
@ -748,10 +683,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100" <label for="buildCommand">{$t('application.build_command')}</label>
>{$t('application.build_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="buildCommand" name="buildCommand"
@ -761,10 +695,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="startCommand" class="text-base font-bold text-stone-100" <label for="startCommand">{$t('application.start_command')}</label>
>{$t('application.start_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="startCommand" name="startCommand"
@ -776,12 +709,13 @@
{/if} {/if}
{#if application.buildPack === 'docker'} {#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100" <label for="dockerFileLocation"
>Dockerfile Location <Explainer >Dockerfile Location <Explainer
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"} explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
/></label /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="dockerFileLocation" name="dockerFileLocation"
@ -793,8 +727,9 @@
{/if} {/if}
{#if application.buildPack === 'deno'} {#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label> <label for="denoMainFile">Main File</label>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoMainFile" name="denoMainFile"
@ -804,12 +739,13 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoOptions" class="text-base font-bold text-stone-100" <label for="denoOptions"
>Arguments <Explainer >Arguments <Explainer
explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."} explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
/></label /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoOptions" name="denoOptions"
@ -819,10 +755,10 @@
/> />
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'} {#if application.buildPack !== 'laravel'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory"
>{$t('forms.base_directory')} >{$t('forms.base_directory')}
<Explainer <Explainer
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."} explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
@ -830,6 +766,7 @@
> >
</div> </div>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="baseDirectory" name="baseDirectory"
@ -842,7 +779,7 @@
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="publishDirectory"
>{$t('forms.publish_directory')} >{$t('forms.publish_directory')}
<Explainer <Explainer
explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."} explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."}
@ -851,6 +788,7 @@
</div> </div>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="publishDirectory" name="publishDirectory"
@ -862,44 +800,6 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
{#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="autodeploy"
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
{/if}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<Setting
id="debug"
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>
</div> </div>

View File

@ -1,12 +1,8 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
const dispatch = createEventDispatcher();
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs'; import { day } from '$lib/dayjs';
@ -15,19 +11,30 @@
let logs: any = []; let logs: any = [];
let currentStatus: any; let currentStatus: any;
let streamInterval: any; let streamInterval: any;
let followingBuild: any; let followingLogs: any;
let followingInterval: any; let followingInterval: any;
let logsEl: any; let logsEl: any;
let fromDb = false; let fromDb = false;
let cancelInprogress = false; let cancelInprogress = false;
let position = 0;
const { id } = $page.params; const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, ''); const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() { function followBuild() {
followingBuild = !followingBuild; followingLogs = !followingLogs;
if (followingBuild) { if (followingLogs) {
followingInterval = setInterval(() => { followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight; logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.body.scrollHeight);
@ -67,7 +74,6 @@
logs = logs.concat( logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
); );
dispatch('updateBuildStatus', { status, took: data.took });
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
@ -98,23 +104,17 @@
}); });
</script> </script>
<div class="relative "> <div class="flex justify-start top-0 pb-2 space-x-2">
{#if currentStatus === 'running'}
<LoadingLogs />
{/if}
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm btn-link hover:text-green-500 hover:bg-coolgray-500" class="btn btn-sm bg-coollabs"
class:text-green-500={followingBuild} disabled={currentStatus !== 'running'}
class:bg-coolgray-300={followingLogs || currentStatus !== 'running'}
class:text-applications={followingLogs}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -128,21 +128,20 @@
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
{#if currentStatus === 'running'}
<button <button
id="cancel"
on:click={cancelBuild} on:click={cancelBuild}
class:animation-spin={cancelInprogress} class:animation-spin={cancelInprogress}
class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500" class="btn btn-sm"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={cancelInprogress || currentStatus !== 'running'}
> >
{#if cancelInprogress}
Cancelling...
{:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -154,15 +153,24 @@
<circle cx="12" cy="12" r="9" /> <circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" /> <path d="M10 10l4 4m0 -4l-4 4" />
</svg> </svg>
{/if} {cancelInprogress ? 'Cancelling...' : 'Cancel Build'}
</button> </button>
<Tooltip triggeredBy="#cancel">Cancel build</Tooltip> {#if currentStatus === 'running'}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if} {/if}
</div> </div>
{#if logs.length > 0} {#if currentStatus === 'queued'}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{$t('application.build.queued_waiting_exec')}
</div>
{:else if logs.length > 0}
<div <div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl} bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
> >
{#each logs as log} {#each logs as log}
{#if fromDb} {#if fromDb}
@ -174,10 +182,8 @@
</div> </div>
{:else} {:else}
<div <div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
> >
No logs found. No logs found yet.
</div> </div>
{/if} {/if}
{/if}
</div>

View File

@ -45,7 +45,6 @@
loadBuildLogsInterval = setInterval(() => { loadBuildLogsInterval = setInterval(() => {
getBuildLogs(); getBuildLogs();
}, 2000); }, 2000);
}); });
onDestroy(() => { onDestroy(() => {
clearInterval(loadBuildLogsInterval); clearInterval(loadBuildLogsInterval);
@ -61,7 +60,7 @@
noMoreBuilds = buildCount <= skip; noMoreBuilds = buildCount <= skip;
try { try {
const data = await get(`/applications/${id}/logs/build?skip=${skip}`); const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
builds = data.builds builds = data.builds;
return; return;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@ -107,74 +106,50 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row">
{$t('application.build_logs')} <div class="title font-bold pb-3 pr-3">Build Logs</div>
<button class="btn btn-sm bg-error" on:click={resetQueue}>Reset Build Queue</button>
</div> </div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex"> <div class="block flex-col justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{:else}
{#if buildCount === 0}
Not build logs found.
{:else}
Select a build to see the logs.
{/if}
{/if}
</div>
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 "> <div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
>Reset Build Queue</button
>
<div class="top-4 md:sticky"> <div class="top-4 md:sticky">
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{#each builds as build, index (build.id)} {#each builds as build, index (build.id)}
<div <div
id={`building-${build.id}`} id={`building-${build.id}`}
on:click={() => loadBuild(build.id)} on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0} class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1} class:rounded-br={index === builds.length - 1}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl" class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-150 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={$selectedBuildId === build.id} class:bg-coolgray-200={$selectedBuildId === build.id}
> >
<div class="flex-col px-2 text-center min-w-[10rem]"> <div class="flex-col px-2 text-center min-w-[10rem]">
<div class="text-sm font-bold"> <div class="text-sm font-bold truncate">
{build.branch || application.branch} {build.branch || application.branch}
</div> </div>
<div class="text-xs"> <div class="text-xs">
@ -189,12 +164,10 @@
</div> </div>
</div> </div>
<div class="w-48 text-center text-xs"> <div class="w-32 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
<div> <div>
<span class="font-bold text-xl" <span class="font-bold text-xl">{build.elapsed}s</span>
>{build.elapsed}s</span
>
</div> </div>
{:else if build.status !== 'queued'} {:else if build.status !== 'queued'}
<div>{day(build.updatedAt).utc().fromNow()}</div> <div>{day(build.updatedAt).utc().fromNow()}</div>
@ -213,26 +186,5 @@
> >
{/each} {/each}
</div> </div>
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2 pb-10">
<button
disabled={noMoreBuilds}
class=" btn btn-sm w-full text-xs"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{/if}
{/if}
</div>
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{/if}
</div> </div>
</div> </div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">{$t('application.build.no_logs')}</div>
{/if}

View File

@ -91,76 +91,26 @@
} }
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="title font-bold pb-3">Application Logs</div>
Application Logs
</div> </div>
<span class="text-xs">{application.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> <div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="text-right " /> <div class="flex justify-start sticky space-x-2 pb-2">
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm btn-link" class="btn btn-sm bg-coollabs"
class:text-green-500={followingLogs} class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -174,20 +124,22 @@
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> {#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div> </div>
<div <div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl} bind:this={logsEl}
on:scroll={detect} on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
> >
<div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} <p>{log + '\n'}</p>
{/each} {/each}
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>

View File

@ -18,22 +18,17 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
import Secret from '../_Secret.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getDomain, getRndInteger } from '$lib/common'; import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
const { id } = $page.params; const { id } = $page.params;
let loadBuildingStatusInterval: any = null; let loadBuildingStatusInterval: any = null;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = { let loading = {
init: true, init: true,
restart: false, restart: false,
@ -41,10 +36,7 @@
}; };
let numberOfGetStatus = 0; let numberOfGetStatus = 0;
let status: any = {}; let status: any = {};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(preview: any) { async function removeApplication(preview: any) {
try { try {
loading.removing = true; loading.removing = true;
@ -119,7 +111,7 @@
return 'error'; return 'error';
} finally { } finally {
numberOfGetStatus--; numberOfGetStatus--;
status = status status = status;
} }
} }
async function restartPreview(preview: any) { async function restartPreview(preview: any) {
@ -164,9 +156,6 @@
try { try {
loading.init = true; loading.init = true;
loading.restart = true; loading.restart = true;
const response = await get(`/applications/${id}/previews`);
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -176,119 +165,29 @@
}); });
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="w-full">
<div class="-mb-5 flex-col"> <div class="mx-auto w-full">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
Preview Deployments <div class="title font-bold pb-3">Preview Deployments</div>
</div> <div class="text-center">
<span class="text-xs">{application?.name}</span> <button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
</div> >Load Previews</button
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
> >
{#if application.gitSource?.type === 'gitlab'} </div>
<svg viewBox="0 0 128 128" class="icons"> </div>
<path </div>
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
{#if loading.init} {#if loading.init}
<div class="mx-auto max-w-6xl px-6 pt-4"> <div class="px-6 pt-4">
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div> <div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
</div> </div>
{:else} {:else if application.previewApplication.length > 0}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
<div class="text-center">
<SimpleExplainer
customClass="w-full"
text={'If your preview is not shown, try load them directly from Docker Engine.<br>(Changed previews process flow in <span class="font-bold text-white">v3.10.4</span>)'}
/>
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
>Fetch Previews</button
>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5 lg:pt-10">
{#if application.previewApplication.length > 0}
<div <div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4" class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
> >
{#each application.previewApplication as preview} {#each application.previewApplication as preview}
<div class="no-underline mb-5"> <div class="no-underline mb-5 w-full lg:w-96">
<div class="w-full rounded p-5 bg-coolgray-200 indicator"> <div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)} {#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" /> <span class="indicator-item badge bg-yellow-500 badge-sm" />
@ -336,7 +235,6 @@
</a> </a>
{/if} {/if}
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip> <Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if loading.restart} {#if loading.restart}
<button <button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
@ -385,11 +283,7 @@
{/if} {/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> <Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button <button id="forceredeploypreview" class="icons" on:click={() => redeploy(preview)}>
id="forceredeploypreview"
class="icons"
on:click={() => redeploy(preview)}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@ -407,10 +301,8 @@
/> />
</svg></button </svg></button
> >
<Tooltip triggeredBy="#forceredeploypreview" <Tooltip triggeredBy="#forceredeploypreview">Force redeploy (without cache)</Tooltip
>Force redeploy (without cache)</Tooltip
> >
<div class="border border-coolgray-500 h-8" />
<button <button
id="deletepreview" id="deletepreview"
class="icons" class="icons"
@ -427,10 +319,6 @@
</div> </div>
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="flex-col"> No previews found.
<div class="text-center font-bold text-xl pb-10">Previews will shown here.</div>
</div>
{/if}
</div>
{/if} {/if}

View File

@ -20,14 +20,16 @@
<script lang="ts"> <script lang="ts">
export let secrets: any; export let secrets: any;
export let application: any; export let previewSecrets: any;
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t } from '$lib/translations'; import { get, post, put } from '$lib/api';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import Secret from './_Secret.svelte';
import PreviewSecret from './_PreviewSecret.svelte';
import { errorNotification } from '$lib/common';
import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const limit = pLimit(1); const limit = pLimit(1);
const { id } = $page.params; const { id } = $page.params;
@ -35,10 +37,11 @@
let batchSecrets = ''; let batchSecrets = '';
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`); const data = await get(`/applications/${id}/secrets`);
previewSecrets = [...data.previewSecrets];
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e: any) { async function getValues() {
e.preventDefault(); if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n'); const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret) .filter((secret) => !secret.startsWith('#') && secret)
@ -49,13 +52,37 @@
return { return {
name, name,
value: cleanValue, value: cleanValue,
isNew: !secrets.find((secret: any) => name === secret.name) createSecret: !secrets.find((secret: any) => name === secret.name)
}; };
}); });
await Promise.all( await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) => batchSecretsPairs.map(({ name, value, createSecret }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew })) limit(async () => {
try {
if (createSecret) {
await post(`/applications/${id}/secrets`, {
name,
value
});
addToast({
message: 'Secret created.',
type: 'success'
});
} else {
await put(`/applications/${id}/secrets`, {
name,
value
});
addToast({
message: 'Secret updated.',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
})
) )
); );
batchSecrets = ''; batchSecrets = '';
@ -67,90 +94,60 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="title font-bold pb-3">Secrets</div>
{$t('application.secret')}
</div> </div>
<span class="text-xs">{application.name} </span> {#each secrets as secret, index}
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr>
<Secret <Secret
{index}
length={secrets.length}
name={secret.name} name={secret.name}
value={secret.value} value={secret.value}
isBuildSecret={secret.isBuildSecret} isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets} on:refresh={refreshSecrets}
/> />
</tr>
{/key} {/key}
{/each} {/each}
<tr> <div class="lg:pt-0 pt-10">
<Secret isNewSecret on:refresh={refreshSecrets} /> <Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
</tr> </div>
</tbody> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
</table> <div class="title font-bold pb-3 pt-8">
<h2 class="title my-6 font-bold">Paste .env file</h2> Preview Secrets <Explainer
<form on:submit|preventDefault={getValues} class="mb-12 w-full"> explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" /> />
<button class="btn btn-sm bg-applications" type="submit">Batch add secrets</button> </div>
</form> </div>
{#if previewSecrets.length !== 0}
{#each previewSecrets as secret, index}
{#key index}
<PreviewSecret
{index}
length={secrets.length}
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
{/key}
{/each}
{:else}
Add secrets first to see Preview Secrets.
{/if}
</div> </div>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="flex flex-row space-x-2">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div>
<textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full"
/>
</form>

View File

@ -5,7 +5,6 @@
const response = await get(`/applications/${params.id}/storages`); const response = await get(`/applications/${params.id}/storages`);
return { return {
props: { props: {
application: stuff.application,
...response ...response
} }
}; };
@ -19,13 +18,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let application: any;
export let persistentStorages: any; export let persistentStorages: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
@ -34,79 +32,22 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="w-full">
<div class="-mb-5 flex-col"> <div class="mx-auto w-full">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
Persistent Storage <div class="title font-bold pb-3">
</div> Persistent Volumes <Explainer
<span class="text-xs">{application.name} </span> position="dropdown-bottom"
</div> explanation={$t('application.storage.persistent_storage_explainer')}
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/> />
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div> </div>
<table class="mx-auto border-separate text-left"> </div>
<thead> <label for="name" class="pb-2 uppercase font-bold">name</label>
<tr class="h-12">
<th scope="col">{$t('forms.path')}</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage} {#each persistentStorages as storage}
{#key storage.id} {#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} /> <Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key} {/key}
{/each} {/each}
<tr>
<Storage on:refresh={refreshStorage} isNew /> <Storage on:refresh={refreshStorage} isNew />
</tr> </div>
</tbody>
</table>
</div> </div>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import { get } from '$lib/api';
import { status } from '$lib/store';
const { id } = $page.params;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
async function getUsage() {
if (usageLoading) return;
if (!$status.application.isRunning) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage`);
usage = data.usage;
usageLoading = false;
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
});
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Monitoring</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>

View File

@ -1,42 +0,0 @@
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
applicationId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/applications/${applicationId}/secrets`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}

View File

@ -44,8 +44,8 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl">{$t('index.applications')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.applications')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button on:click={newApplication} class="btn btn-square btn-sm bg-applications"> <button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
<svg <svg
@ -63,8 +63,9 @@
> >
</button> </button>
{/if} {/if}
</div> </nav>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <br />
<div class="flex flex-col justify-center mt-10 pb-12 lg:pt-16 sm:pb-16">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@ -5,13 +5,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div> <h1 class="title">CouchDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -23,7 +21,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -34,9 +32,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -48,7 +44,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -59,9 +55,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword">{$t('forms.roots_password')}</label>
>{$t('forms.roots_password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@ -107,10 +107,10 @@
} }
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl p-4">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5"> <div class="flex space-x-1 pb-5 items-center">
<div class="title">{$t('general')}</div> <h1 class="title">{$t('general')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@ -121,22 +121,17 @@
> >
{/if} {/if}
</div> </div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-flow-row gap-2 px-10"> <label for="name">{$t('forms.name')}</label>
<div class="grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input <input
class="w-full"
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="name" name="name"
id="name" id="name"
bind:value={database.name} bind:value={database.name}
required required
/> />
</div> <label for="destination">{$t('application.destination')}</label>
<div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId} {#if database.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
<input <input
@ -144,14 +139,11 @@
id="destination" id="destination"
disabled disabled
readonly readonly
class="bg-transparent " class="bg-transparent w-full"
/> />
</div> </div>
{/if} {/if}
</div> <label for="version">Version / Tag</label>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a <a
href={$appSession.isAdmin && !$status.database.isRunning href={$appSession.isAdmin && !$status.database.isRunning
? `/databases/${id}/configuration/version?from=/databases/${id}` ? `/databases/${id}/configuration/version?from=/databases/${id}`
@ -159,18 +151,14 @@
class="no-underline" class="no-underline"
> >
<input <input
class="w-full"
value={database.version} value={database.version}
readonly readonly
disabled={$status.database.isRunning || $status.database.initialLoading} disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning} class:cursor-pointer={!$status.database.isRunning}
/></a /></a
> >
</div> <label for="host">{$t('forms.host')}</label>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false} isPasswordField={false}
@ -180,21 +168,20 @@
name="host" name="host"
value={database.id} value={database.id}
/> />
</div> <label for="publicPort">{$t('forms.port')}</label>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField <CopyPasswordField
placeholder={$t('database.generated_automatically_after_set_to_public')} placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort" id="publicPort"
readonly readonly
disabled disabled
name="publicPort" name="publicPort"
value={publicLoading ? 'Loading...' : $status.database.isPublic ? database.publicPort : privatePort} value={publicLoading
? 'Loading...'
: $status.database.isPublic
? database.publicPort
: privatePort}
/> />
</div> </div>
</div>
<div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'} {#if database.type === 'mysql'}
<MySql bind:database /> <MySql bind:database />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
@ -210,9 +197,9 @@
{:else if database.type === 'edgedb'} {:else if database.type === 'edgedb'}
<EdgeDB {database} /> <EdgeDB {database} />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="flex flex-col space-y-2 mt-5">
<div> <div>
<label for="url" class="text-base font-bold text-stone-100" <label class="px-2" for="url"
>{$t('database.connection_string')} >{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine} {#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer <Explainer
@ -221,6 +208,7 @@
{/if}</label {/if}</label
> >
</div> </div>
<div class="lg:px-10 px-2">
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
@ -235,10 +223,9 @@
</div> </div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">{$t('application.features')}</div> <h1 class="title">{$t('application.features')}</h1>
</div> </div>
<div class="px-10 pb-10"> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isPublic" id="isPublic"
loading={publicLoading} loading={publicLoading}
@ -248,9 +235,7 @@
description={$t('database.warning_database_public')} description={$t('database.warning_database_public')}
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
/> />
</div>
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center">
<Setting <Setting
id="appendOnly" id="appendOnly"
loading={publicLoading} loading={publicLoading}
@ -259,7 +244,6 @@
title={$t('database.change_append_only_mode')} title={$t('database.change_append_only_mode')}
description={$t('database.warning_append_only')} description={$t('database.warning_append_only')}
/> />
</div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -9,11 +9,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">EdgeDB</div> <div class="title">EdgeDB</div>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +34,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100" <label for="rootUser"
>Root Password <Explainer >Root Password <Explainer
explanation="Could be changed while the database is running." explanation="Could be changed while the database is running."
/></label /></label

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div> <h1 class="title">MariaDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase"
>{$t('database.default_database')}</label >{$t('database.default_database')}</label
> >
<CopyPasswordField <CopyPasswordField
@ -25,7 +25,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser" >{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +36,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >
@ -51,7 +51,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser" >{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -62,8 +62,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword"
>{$t('forms.roots_password')} <Explainer explanation="Could be changed while the database is running." /></label >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div> <h1 class="title">MongoDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser" id="rootUser"
@ -22,7 +22,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword"
>{$t('forms.roots_password')} >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,13 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <h1 class="title">MySQL</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +34,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >
@ -51,7 +49,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -62,7 +60,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword"
>{$t('forms.roots_password')} >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,13 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div> <h1 class="title">PostgreSQL</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100" <label for="rootUser"
>Postgres User Password <Explainer >Postgres User Password <Explainer
explanation="Could be changed while the database is running." explanation="Could be changed while the database is running."
/></label /></label
@ -41,7 +39,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -52,7 +50,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div> <h1 class="title">Redis</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -19,7 +19,7 @@
if (id !== 'new' && (!database || Object.entries(database).length === 0)) { if (id !== 'new' && (!database || Object.entries(database).length === 0)) {
return { return {
status: 302, status: 302,
redirect: '/databases' redirect: '/'
}; };
} }
const configurationPhase = checkConfiguration(database); const configurationPhase = checkConfiguration(database);
@ -62,6 +62,7 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DatabaseLinks from './_DatabaseLinks.svelte';
const { id } = $page.params; const { id } = $page.params;
$status.database.isPublic = database.settings.isPublic || false; $status.database.isPublic = database.settings.isPublic || false;
@ -149,13 +150,33 @@
</script> </script>
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center">
<div class="title">
{#if $page.url.pathname === `/databases/${id}`}
Configurations
{:else if $page.url.pathname === `/databases/${id}/logs`}
Database Logs
{:else if $page.url.pathname === `/databases/${id}/configuration/type`}
Select a Database Type
{:else if $page.url.pathname === `/databases/${id}/configuration/version`}
Select a Database Version
{:else if $page.url.pathname === `/databases/${id}/configuration/destination`}
Select a Destination
{/if}
</div>
</div>
<DatabaseLinks {database} />
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
{#if database.type && database.destinationDockerId && database.version} {#if database.type && database.destinationDockerId && database.version}
{#if $status.database.isExited} {#if $status.database.isExited}
<a <a
id="exited" id="exited"
href={!$status.database.isRunning ? `/databases/${id}/logs` : null} href={!$status.database.isRunning ? `/databases/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error" class="icons bg-transparent text-red-500 tooltip-error"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
@ -179,9 +200,7 @@
<Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip> <Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip>
{/if} {/if}
{#if $status.database.initialLoading} {#if $status.database.initialLoading}
<button <button class="icons flex animate-spin duration-500 ease-in-out">
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -207,7 +226,7 @@
on:click={stopDatabase} on:click={stopDatabase}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500" class="icons bg-transparent text-red-500"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -248,6 +267,7 @@
</button> </button>
<Tooltip triggeredBy="#start">{'Start'}</Tooltip> <Tooltip triggeredBy="#start">{'Start'}</Tooltip>
{/if} {/if}
{/if}
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
id="configuration" id="configuration"
@ -282,34 +302,6 @@
></a ></a
> >
<Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip> <Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip>
<a
href="/databases/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/secrets`}
>
<button id="secrets" disabled={$isDeploymentEnabled} class="icons bg-transparent text-sm ">
<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="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
id="databaselogs" id="databaselogs"
@ -340,8 +332,6 @@
></a ></a
> >
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip> <Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{/if}
{#if forceDelete} {#if forceDelete}
<button <button
on:click={() => deleteDatabase(true)} on:click={() => deleteDatabase(true)}
@ -362,7 +352,8 @@
> >
{/if} {/if}
<Tooltip triggeredBy="#delete">{'Delete'}</Tooltip> <Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
</div>
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@ -53,11 +53,6 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">

View File

@ -47,10 +47,6 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each types as type} {#each types as type}
<div class="p-2"> <div class="p-2">

View File

@ -46,9 +46,6 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
</div>
{#if from} {#if from}
<div class="pb-10 text-center"> <div class="pb-10 text-center">
Warning: you are about to change the version of this database.<br />This could cause problem Warning: you are about to change the version of this database.<br />This could cause problem

View File

@ -48,18 +48,7 @@
}); });
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto max-w-6xl p-5">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{database.name}</span>
</div>
<DatabaseLinks {database} />
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Database Usage</div>
<div class="text-center"> <div class="text-center">
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div> <div class="stat-title">Used Memory / Memory Limit</div>

View File

@ -91,14 +91,6 @@
} }
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Database Logs
</div>
<span class="text-xs">{database.name}</span>
</div>
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> <div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
@ -134,17 +126,11 @@
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> <Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1 lg:text-base text-[10px]">
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} <p>{log + '\n'}</p>
{/each} {/each}
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>

View File

@ -42,8 +42,8 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.databases')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button on:click={newDatabase} class="btn btn-square btn-sm bg-databases"> <button on:click={newDatabase} class="btn btn-square btn-sm bg-databases">
<svg <svg
@ -61,9 +61,9 @@
> >
</button> </button>
{/if} {/if}
</div> </nav>
<br />
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16 lg:pt-16">
{#if !databases || ownDatabases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div> <div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>

View File

@ -1,25 +1,14 @@
<script lang="ts"> <script lang="ts">
export let destination: any; export let destination: any;
export let settings: any; export let settings: any;
export let state: any;
import LocalDocker from './_LocalDocker.svelte'; import LocalDocker from './_LocalDocker.svelte';
import RemoteDocker from './_RemoteDocker.svelte'; import RemoteDocker from './_RemoteDocker.svelte';
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto max-w-6xl px-6">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{destination.name}</span>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
{#if destination.remoteEngine} {#if destination.remoteEngine}
<RemoteDocker bind:destination {settings} {state} /> <RemoteDocker bind:destination {settings} />
{:else} {:else}
<LocalDocker bind:destination {settings} {state} /> <LocalDocker bind:destination {settings} />
{/if} {/if}
</div> </div>

View File

@ -141,10 +141,8 @@
} }
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex md:flex-row space-y-2 md:space-y-0 space-x-0 md:space-x-2 flex-col pb-5"> <div class="flex space-x-2">
<div class="title">{$t('forms.configuration')}</div>
{#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
class="btn btn-sm" class="btn btn-sm"
@ -160,21 +158,18 @@
disabled={loading.restart} disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button
> >
{/if}
</div> </div>
<div class="grid lg:grid-cols-2 items-center px-10 "> <div class="grid gap-2 grid-cols-2 auto-rows-max mt-10 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input <input
class="w-full"
name="name" name="name"
placeholder={$t('forms.name')} placeholder={$t('forms.name')}
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
bind:value={destination.name} bind:value={destination.name}
/> />
</div> <label for="engine">{$t('forms.engine')}</label>
<div class="grid lg:grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField <CopyPasswordField
id="engine" id="engine"
readonly readonly
@ -183,9 +178,7 @@
placeholder="{$t('forms.eg')}: /var/run/docker.sock" placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine} value={destination.engine}
/> />
</div> <label for="network">{$t('forms.network')}</label>
<div class="grid lg:grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField <CopyPasswordField
id="network" id="network"
readonly readonly
@ -194,9 +187,7 @@
placeholder="{$t('forms.default')}: coolify" placeholder="{$t('forms.default')}: coolify"
value={destination.network} value={destination.network}
/> />
</div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid lg:grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting" id="changeProxySetting"
loading={loading.proxy} loading={loading.proxy}
@ -210,6 +201,6 @@
: '' : ''
}`} }`}
/> />
</div>
{/if} {/if}
</div>
</form> </form>

View File

@ -31,9 +31,9 @@
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5"> <div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col space-y-4 lg:space-y-0">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading} <button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@ -41,12 +41,12 @@
: $t('forms.save')}</button : $t('forms.save')}</button
> >
</div> </div>
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} /> <input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label> <label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<input <input
required required
@ -55,7 +55,7 @@
bind:value={payload.engine} bind:value={payload.engine}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input <input
required required
@ -65,7 +65,7 @@
/> />
</div> </div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<Setting <Setting
id="changeProxySetting" id="changeProxySetting"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}

View File

@ -38,9 +38,9 @@
</div> </div>
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5"> <div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading} <button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@ -48,12 +48,12 @@
: $t('forms.save')}</button : $t('forms.save')}</button
> >
</div> </div>
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} /> <input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100" <label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label >{$t('forms.ip_address')}</label
> >
@ -65,7 +65,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input <input
required required
@ -75,7 +75,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
required required
@ -84,7 +84,7 @@
bind:value={payload.remotePort} bind:value={payload.remotePort}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input <input
required required
@ -93,7 +93,7 @@
bind:value={payload.network} bind:value={payload.network}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<Setting <Setting
id="isCoolifyProxyUsed" id="isCoolifyProxyUsed"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}

View File

@ -69,7 +69,7 @@
loading.proxy = false; loading.proxy = false;
}); });
async function changeProxySetting() { async function changeProxySetting() {
if (!destination.remoteVerified) return if (!destination.remoteVerified) return;
loading.proxy = true; loading.proxy = true;
if (!cannotDisable) { if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed; const isProxyActivated = destination.isCoolifyProxyUsed;
@ -166,7 +166,6 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5"> <div class="flex space-x-1 pb-5">
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@ -197,9 +196,10 @@
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center px-10 "> <div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input <input
name="name" name="name"
class="w-full"
placeholder={$t('forms.name')} placeholder={$t('forms.name')}
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@ -207,7 +207,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network">{$t('forms.network')}</label>
<CopyPasswordField <CopyPasswordField
id="network" id="network"
readonly readonly
@ -218,7 +218,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100">IP Address</label> <label for="remoteIpAddress">IP Address</label>
<CopyPasswordField <CopyPasswordField
id="remoteIpAddress" id="remoteIpAddress"
readonly readonly
@ -228,7 +228,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label> <label for="remoteUser">User</label>
<CopyPasswordField <CopyPasswordField
id="remoteUser" id="remoteUser"
readonly readonly
@ -238,7 +238,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label> <label for="remotePort">Port</label>
<CopyPasswordField <CopyPasswordField
id="remotePort" id="remotePort"
readonly readonly
@ -248,7 +248,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="sshKey" class="text-base font-bold text-stone-100">SSH Key</label> <label for="sshKey">SSH Key</label>
<a <a
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''} href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
class="no-underline" class="no-underline"
@ -256,7 +256,7 @@
value={destination.sshKey.name} value={destination.sshKey.name}
readonly readonly
id="sshKey" id="sshKey"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer w-full"
/></a /></a
> >
</div> </div>
@ -268,7 +268,7 @@
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${ description={`Install & configure a proxy (based on Traefik) on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: '' : ''

View File

@ -16,7 +16,7 @@
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) { if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
return { return {
status: 302, status: 302,
redirect: '/destinations' redirect: '/'
}; };
} }
const configurationPhase = checkConfiguration(destination); const configurationPhase = checkConfiguration(destination);
@ -88,7 +88,18 @@
</script> </script>
{#if $page.params.id !== 'new'} {#if $page.params.id !== 'new'}
<nav class="nav-side"> <nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center title">
{#if $page.url.pathname === `/destinations/${$page.params.id}`}
Configurations
{:else if $page.url.pathname.startsWith(`/destinations/${$page.params.id}/configuration/sshkey`)}
Select a SSH Key
{/if}
</div>
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
<button <button
id="delete" id="delete"
on:click={() => deleteDestination(destination)} on:click={() => deleteDestination(destination)}
@ -98,7 +109,8 @@
class="icons bg-transparent text-sm" class="icons bg-transparent text-sm"
class:text-stone-600={!isDestinationDeletable}><DeleteIcon /></button class:text-stone-600={!isDestinationDeletable}><DeleteIcon /></button
> >
</nav>
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip> <Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
</div>
</nav>
{/if} {/if}
<slot /> <slot />

View File

@ -38,9 +38,6 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a SSH Keys</div>
</div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row "> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
{#if sshKeys.length > 0} {#if sshKeys.length > 0}
@ -61,7 +58,7 @@
<div class="pb-2 text-center font-bold">No SSH key found</div> <div class="pb-2 text-center font-bold">No SSH key found</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a <a
href="/settings/ssh-keys" href="/settings/ssh"
sveltekit:prefetch sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500" class="add-icon bg-sky-600 hover:bg-sky-500"
> >

View File

@ -36,8 +36,8 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.destinations')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<a href="/destinations/new" class="btn btn-square btn-sm bg-destinations"> <a href="/destinations/new" class="btn btn-square btn-sm bg-destinations">
<svg <svg
@ -55,8 +55,9 @@
> >
</a> </a>
{/if} {/if}
</div> </nav>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <br />
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16 lg:pt-16">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div> <div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>

View File

@ -106,8 +106,8 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div> <h1 class="mr-4 text-2xl tracking-tight font-bold">Identity and Access Management</h1>
<button on:click={newTeam} class="btn btn-square btn-sm bg-iam"> <button on:click={newTeam} class="btn btn-square btn-sm bg-iam">
<svg <svg
class="h-6 w-6" class="h-6 w-6"
@ -123,10 +123,11 @@
/></svg /></svg
> >
</button> </button>
</div> </nav>
<br />
{#if invitations.length > 0} {#if invitations.length > 0}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-6xl px-6 py-4">
<div class="title font-bold">Pending invitations</div> <div class="title font-bold">Pending invitations</div>
<div class="pt-10 text-center"> <div class="pt-10 text-center">
{#each invitations as invitation} {#each invitations as invitation}
@ -148,7 +149,7 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-6xl px-6 py-4">
{#if $appSession.teamId === '0' && accounts.length > 0} {#if $appSession.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div> <div class="title font-bold">Accounts</div>
{:else} {:else}
@ -188,7 +189,7 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6">
<div class="title font-bold">Teams</div> <div class="title font-bold">Teams</div>
<div class="flex-col items-center justify-center pt-10"> <div class="flex-col items-center justify-center pt-10">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row"> <div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">

View File

@ -87,7 +87,7 @@
<span class="arrow-right-applications px-1 text-fuchsia-500">></span> <span class="arrow-right-applications px-1 text-fuchsia-500">></span>
<span class="pr-2">{team.name}</span> <span class="pr-2">{team.name}</span>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6">
<form on:submit|preventDefault={handleSubmit} class=" py-4"> <form on:submit|preventDefault={handleSubmit} class=" py-4">
<div class="flex space-x-1 pb-5"> <div class="flex space-x-1 pb-5">
<div class="title font-bold">{$t('index.settings')}</div> <div class="title font-bold">{$t('index.settings')}</div>

Some files were not shown because too many files have changed in this diff Show More