Merge branch 'restray_i18n' of github.com:restray/coolify into restray_i18n

This commit is contained in:
Restray 2022-04-04 12:31:48 +02:00
commit b4bbd22781
No known key found for this signature in database
GPG Key ID: 67C6DEF95A4DC812
52 changed files with 1188 additions and 258 deletions

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
yarn lint-staged pnpm lint-staged

View File

@ -35,7 +35,8 @@ This is a little list of what you can do to help the project:
- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. - Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool.
- Install dependencies with `pnpm install`. - Install dependencies with `pnpm install`.
- Need to create a local SQlite database with `pnpm db:push`. - Need to create a local SQlite database with `pnpm db:push`.
- This will apply all migrations and seed the database at `db/dev.db`. - This will apply all migrations at `db/dev.db`.
- Seed the database with base entities with `pnpm db:seed`
- You can start coding after starting `pnpm dev`. - You can start coding after starting `pnpm dev`.
#### How to start after you set up your local fork? #### How to start after you set up your local fork?

View File

@ -6,7 +6,7 @@ An open-source & self-hostable Heroku / Netlify alternative.
https://demo.coolify.io/ https://demo.coolify.io/
(If it is unresponsible, that means someone overloaded the server. 🙃) (If it is unresponsive, that means someone overloaded the server. 🙃)
## How to install ## How to install

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.2.7", "version": "2.3.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
@ -30,14 +30,14 @@
"@sveltejs/kit": "1.0.0-next.303", "@sveltejs/kit": "1.0.0-next.303",
"@types/bcrypt": "5.0.0", "@types/bcrypt": "5.0.0",
"@types/js-cookie": "3.0.1", "@types/js-cookie": "3.0.1",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "4.0.5",
"@types/node": "17.0.23", "@types/node": "17.0.23",
"@types/node-forge": "1.0.1", "@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.1", "@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.4", "autoprefixer": "10.4.4",
"cross-env": "^7.0.3", "cross-env": "7.0.3",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
@ -52,8 +52,8 @@
"svelte": "3.46.4", "svelte": "3.46.4",
"svelte-check": "2.4.6", "svelte-check": "2.4.6",
"svelte-preprocess": "4.10.4", "svelte-preprocess": "4.10.4",
"svelte-select": "^4.4.7", "svelte-select": "4.4.7",
"sveltekit-i18n": "^2.1.2", "sveltekit-i18n": "2.1.2",
"tailwindcss": "3.0.23", "tailwindcss": "3.0.23",
"ts-node": "10.7.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.3.1",
@ -68,7 +68,7 @@
"bullmq": "1.78.1", "bullmq": "1.78.1",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",
"cooltipz-css": "^2.1.0", "cooltipz-css": "2.1.0",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.0", "dayjs": "1.11.0",
"dockerode": "3.3.1", "dockerode": "3.3.1",
@ -79,10 +79,11 @@
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "^4.2.0", "mustache": "4.2.0",
"node-forge": "1.3.0", "node-forge": "1.3.0",
"p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.2", "svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "^0.1.0", "tailwindcss-scrollbar": "0.1.0",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"prisma": { "prisma": {

34
pnpm-lock.yaml generated
View File

@ -8,7 +8,7 @@ specifiers:
'@sveltejs/kit': 1.0.0-next.303 '@sveltejs/kit': 1.0.0-next.303
'@types/bcrypt': 5.0.0 '@types/bcrypt': 5.0.0
'@types/js-cookie': 3.0.1 '@types/js-cookie': 3.0.1
'@types/js-yaml': ^4.0.5 '@types/js-yaml': 4.0.5
'@types/node': 17.0.23 '@types/node': 17.0.23
'@types/node-forge': 1.0.1 '@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/eslint-plugin': 4.31.1
@ -19,8 +19,8 @@ specifiers:
bullmq: 1.78.1 bullmq: 1.78.1
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.4.2 cookie: 0.4.2
cooltipz-css: ^2.1.0 cooltipz-css: 2.1.0
cross-env: ^7.0.3 cross-env: 7.0.3
cross-var: 1.1.0 cross-var: 1.1.0
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.11.0 dayjs: 1.11.0
@ -37,8 +37,9 @@ specifiers:
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
lint-staged: 12.3.7 lint-staged: 12.3.7
mustache: ^4.2.0 mustache: 4.2.0
node-forge: 1.3.0 node-forge: 1.3.0
p-limit: 4.0.0
postcss: 8.4.12 postcss: 8.4.12
prettier: 2.6.1 prettier: 2.6.1
prettier-plugin-svelte: 2.6.0 prettier-plugin-svelte: 2.6.0
@ -48,10 +49,10 @@ specifiers:
svelte-check: 2.4.6 svelte-check: 2.4.6
svelte-kit-cookie-session: 2.1.2 svelte-kit-cookie-session: 2.1.2
svelte-preprocess: 4.10.4 svelte-preprocess: 4.10.4
svelte-select: ^4.4.7 svelte-select: 4.4.7
sveltekit-i18n: ^2.1.2 sveltekit-i18n: 2.1.2
tailwindcss: 3.0.23 tailwindcss: 3.0.23
tailwindcss-scrollbar: ^0.1.0 tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0 ts-node: 10.7.0
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.6.3 typescript: 4.6.3
@ -78,6 +79,7 @@ dependencies:
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
mustache: 4.2.0 mustache: 4.2.0
node-forge: 1.3.0 node-forge: 1.3.0
p-limit: 4.0.0
svelte-kit-cookie-session: 2.1.2 svelte-kit-cookie-session: 2.1.2
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23 tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23
unique-names-generator: 4.7.1 unique-names-generator: 4.7.1
@ -4270,6 +4272,16 @@ packages:
engines: { node: '>=12.20' } engines: { node: '>=12.20' }
dev: false dev: false
/p-limit/4.0.0:
resolution:
{
integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
yocto-queue: 1.0.0
dev: false
/p-map/2.1.0: /p-map/2.1.0:
resolution: resolution:
{ {
@ -5714,3 +5726,11 @@ packages:
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dev: true dev: true
/yocto-queue/1.0.0:
resolution:
{
integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
}
engines: { node: '>=12.20' }
dev: false

View File

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "pythonModule" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonVariable" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonWSGI" TEXT;

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "MeiliSearch" (
"id" TEXT NOT NULL PRIMARY KEY,
"masterKey" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "MeiliSearch_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "MeiliSearch_serviceId_key" ON "MeiliSearch"("serviceId");

View File

@ -87,6 +87,9 @@ model Application {
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
phpModules String? phpModules String?
pythonWSGI String?
pythonModule String?
pythonVariable String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
@ -280,6 +283,7 @@ model Service {
wordpress Wordpress? wordpress Wordpress?
ghost Ghost? ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -349,3 +353,12 @@ model Ghost {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model MeiliSearch {
id String @id @default(cuid())
masterKey String
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -100,10 +100,14 @@ export const setDefaultConfiguration = async (data) => {
if (buildPack === 'static') port = 80; if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000; else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80; else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
} }
if (!installCommand) installCommand = template?.installCommand || 'yarn install'; if (template) {
if (!startCommand) startCommand = template?.startCommand || 'yarn start'; if (!installCommand) installCommand = template?.installCommand || 'yarn install';
if (!buildCommand) buildCommand = template?.buildCommand || null; if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null;
}
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}`;
@ -136,7 +140,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
` `
); );
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`); await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (staticDeployments.includes(buildPack)) { } else if (staticDeployments.includes(buildPack)) {
await fs.writeFile( await fs.writeFile(
`${workdir}/nginx.conf`, `${workdir}/nginx.conf`,
@ -190,7 +198,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
} }
` `
); );
saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId }); await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -12,6 +12,7 @@ import php from './php';
import rust from './rust'; import rust from './rust';
import astro from './static'; import astro from './static';
import eleventy from './static'; import eleventy from './static';
import python from './python';
export { export {
node, node,
@ -27,5 +28,6 @@ export {
php, php,
rust, rust,
astro, astro,
eleventy eleventy,
python
}; };

View File

@ -0,0 +1,71 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const {
workdir,
port,
baseDirectory,
secrets,
pullmergeRequestId,
pythonWSGI,
pythonModule,
pythonVariable
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`RUN pip install gunicorn`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`);
// Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`)
}
try {
await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`);
Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`);
Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`);
} catch (e) {
//
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`);
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(
`CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}`
);
} else {
Dockerfile.push(`CMD python ${pythonModule}`);
}
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'python:3-alpine';
await createDockerfile(data, image);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -80,7 +80,7 @@ export const getTeam = (event) => {
export const getUserDetails = async (event, isAdminRequired = true) => { export const getUserDetails = async (event, isAdminRequired = true) => {
const teamId = getTeam(event); const teamId = getTeam(event);
const userId = event.locals.session.data.userId || null; const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({ const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId }, where: { teamId, userId },
select: { permission: true }, select: { permission: true },

View File

@ -0,0 +1,25 @@
<script>
export let database;
import Clickhouse from './svg/databases/Clickhouse.svelte';
import CouchDb from './svg/databases/CouchDB.svelte';
import MongoDb from './svg/databases/MongoDB.svelte';
import MySql from './svg/databases/MySQL.svelte';
import PostgreSql from './svg/databases/PostgreSQL.svelte';
import Redis from './svg/databases/Redis.svelte';
</script>
<span class="relative">
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>

View File

@ -0,0 +1,55 @@
<script>
export let service;
import Ghost from './svg/services/Ghost.svelte';
import LanguageTool from './svg/services/LanguageTool.svelte';
import MinIo from './svg/services/MinIO.svelte';
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
import Wordpress from './svg/services/Wordpress.svelte';
</script>
{#if service.type === 'plausibleanalytics'}
<a href="https://plausible.io" target="_blank">
<PlausibleAnalytics />
</a>
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if}

View File

@ -19,7 +19,7 @@ export const staticDeployments = [
'astro', 'astro',
'eleventy' 'eleventy'
]; ];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
export function getDomain(domain) { export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');
@ -37,3 +37,9 @@ export function dashify(str: string, options?: any): string {
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m)) .replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase(); .toLowerCase();
} }
export function changeQueryParams(buildId) {
const queryParams = new URLSearchParams(window.location.search);
queryParams.set('buildId', buildId);
return history.pushState(null, null, '?' + queryParams.toString());
}

View File

@ -0,0 +1,45 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
viewBox="0 0 127 74"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
><path
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"
fill="url(#meilisearch_logo_svg__paint0_linear_0_6)"
/><path
d="M34.925 73.993l23.243-59.47A21.85 21.85 0 0178.52.626h14.013L69.29 60.096a21.85 21.85 0 01-20.351 13.897H34.925z"
fill="url(#meilisearch_logo_svg__paint1_linear_0_6)"
/><path
d="M69.026 73.993l23.244-59.47A21.85 21.85 0 01112.621.626h14.014l-23.244 59.47a21.851 21.851 0 01-20.352 13.897H69.026z"
fill="url(#meilisearch_logo_svg__paint2_linear_0_6)"
/><defs
><linearGradient
id="meilisearch_logo_svg__paint0_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint1_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint2_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
></defs
></svg
>

View File

@ -146,6 +146,13 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 80 port: 80
}; };
} }
if (pack === 'python') {
return {
...metaData,
startCommand: null,
port: 8000
};
}
return { return {
name: 'node', name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
@ -249,6 +256,12 @@ export const buildPacks = [
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700'
},
{
name: 'python',
fancyName: 'Python',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
} }
]; ];
export const scanningTemplates = { export const scanningTemplates = {

View File

@ -214,11 +214,15 @@ export async function configureApplication({
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
}) { }) {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { data: {
name,
buildPack, buildPack,
fqdn, fqdn,
port, port,
@ -227,7 +231,9 @@ export async function configureApplication({
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory, publishDirectory,
name pythonWSGI,
pythonModule,
pythonVariable
} }
}); });
} }

View File

@ -46,7 +46,9 @@ export function ErrorHandler(e) {
if (e.message?.includes('git clone')) { if (e.message?.includes('git clone')) {
truncatedError.message = 'git clone failed'; truncatedError.message = 'git clone failed';
} }
sentry.captureException(truncatedError); if (!e.message?.includes('Coolify Proxy is not running')) {
sentry.captureException(truncatedError);
}
const payload = { const payload = {
status: truncatedError.status || 500, status: truncatedError.status || 500,
body: { body: {
@ -195,6 +197,16 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 2368 main: 2368
} }
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
ports: {
main: 7700
}
} }
]; ];

View File

@ -22,7 +22,8 @@ export async function getService({ id, teamId }) {
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true, ghost: true,
serviceSecret: true serviceSecret: true,
meiliSearch: true
} }
}); });
@ -50,6 +51,8 @@ export async function getService({ id, teamId }) {
body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword); body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword); if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) { if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => { body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
@ -165,6 +168,15 @@ export async function configureServiceType({ id, type }) {
} }
} }
}); });
} else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword(32));
await prisma.service.update({
where: { id },
data: {
type,
meiliSearch: { create: { masterKey } }
}
});
} }
} }
export async function setServiceVersion({ id, version }) { export async function setServiceVersion({ id, version }) {
@ -191,6 +203,9 @@ export async function updateService({ id, fqdn, name }) {
export async function updateLanguageToolService({ id, fqdn, name }) { export async function updateLanguageToolService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateMeiliSearchService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVaultWardenService({ id, fqdn, name }) { export async function updateVaultWardenService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
@ -214,6 +229,7 @@ export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
} }
export async function removeService({ id }) { export async function removeService({ id }) {
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });

View File

@ -88,12 +88,12 @@ export async function buildImage({
debug = false debug = false
}) { }) {
if (isCache) { if (isCache) {
saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
} else { } else {
saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (!debug && isCache) { if (!debug && isCache) {
saveBuildLog({ await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`, line: `Debug turned off. To see more details, allow it in the configuration.`,
buildId, buildId,
applicationId applicationId
@ -126,13 +126,17 @@ export async function streamEvents({ stream, docker, buildId, applicationId, deb
if (err) reject(err); if (err) reject(err);
resolve(res); resolve(res);
} }
function onProgress(event) { async function onProgress(event) {
if (event.error) { if (event.error) {
reject(event.error); reject(event.error);
} else if (event.stream) { } else if (event.stream) {
if (event.stream !== '\n') { if (event.stream !== '\n') {
if (debug) if (debug)
saveBuildLog({ line: `${event.stream.replace('\n', '')}`, buildId, applicationId }); await saveBuildLog({
line: `${event.stream.replace('\n', '')}`,
buildId,
applicationId
});
} }
} }
} }

View File

@ -175,7 +175,7 @@ export async function configureHAProxy() {
isRunning, isRunning,
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain, redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime() updatedAt: updatedAt.getTime()
}); });
} }
@ -199,7 +199,7 @@ export async function configureHAProxy() {
isRunning, isRunning,
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? previewDomain : 'www.' + previewDomain, redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime() updatedAt: updatedAt.getTime()
}); });
} }
@ -242,7 +242,7 @@ export async function configureHAProxy() {
isRunning, isRunning,
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain, redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime() updatedAt: updatedAt.getTime()
}); });
} }
@ -262,7 +262,7 @@ export async function configureHAProxy() {
domain, domain,
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
}); });
} }
const output = mustache.render(template, data); const output = mustache.render(template, data);

View File

@ -10,11 +10,14 @@ export default async function ({
workdir, workdir,
githubAppId, githubAppId,
repository, repository,
apiUrl,
htmlUrl,
branch, branch,
buildId buildId
}): Promise<any> { }): Promise<any> {
try { try {
saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId }); const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
@ -27,20 +30,20 @@ export default async function ({
algorithm: 'RS256' algorithm: 'RS256'
}); });
const { token } = await got const { token } = await got
.post(`https://api.github.com/app/installations/${installationId}/access_tokens`, { .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, {
headers: { headers: {
Authorization: `Bearer ${jwtToken}`, Authorization: `Bearer ${jwtToken}`,
Accept: 'application/vnd.github.machine-man-preview+json' Accept: 'application/vnd.github.machine-man-preview+json'
} }
}) })
.json(); .json();
saveBuildLog({ await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`, line: `Cloning ${repository}:${branch} branch.`,
buildId, buildId,
applicationId applicationId
}); });
await asyncExecShell( await asyncExecShell(
`git clone -q -b ${branch} https://x-access-token:${token}@github.com/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..` `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..`
); );
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');

View File

@ -1,28 +1,28 @@
import { asyncExecShell, saveBuildLog } from '$lib/common'; import { asyncExecShell, saveBuildLog } from '$lib/common';
import { ErrorHandler } from '$lib/database';
export default async function ({ export default async function ({
applicationId, applicationId,
debug,
workdir, workdir,
repodir, repodir,
htmlUrl,
repository, repository,
branch, branch,
buildId, buildId,
privateSshKey privateSshKey
}): Promise<any> { }): Promise<any> {
saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
saveBuildLog({ await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`, line: `Cloning ${repository}:${branch} branch.`,
buildId, buildId,
applicationId applicationId
}); });
await asyncExecShell( await asyncExecShell(
`git clone -q -b ${branch} git@gitlab.com:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..` `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..`
); );
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');

View File

@ -51,7 +51,10 @@ export default async function (job) {
pullmergeRequestId = null, pullmergeRequestId = null,
sourceBranch = null, sourceBranch = null,
settings, settings,
persistentStorage persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
@ -114,6 +117,7 @@ export default async function (job) {
branch, branch,
buildId, buildId,
apiUrl: gitSource.apiUrl, apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId, projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
@ -127,7 +131,7 @@ export default async function (job) {
} }
try { try {
db.prisma.build.update({ where: { id: buildId }, data: { commit } }); await db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@ -157,7 +161,7 @@ export default async function (job) {
}); });
deployNeeded = true; deployNeeded = true;
if (configHash) { if (configHash) {
saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
} }
} else { } else {
deployNeeded = false; deployNeeded = false;
@ -200,16 +204,19 @@ export default async function (job) {
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets, secrets,
phpModules phpModules,
pythonWSGI,
pythonModule,
pythonVariable
}); });
else { else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`); throw new Error(`Build pack ${buildPack} not found.`);
} }
deployNeeded = true; deployNeeded = true;
} else { } else {
deployNeeded = false; deployNeeded = false;
saveBuildLog({ line: 'Nothing changed.', buildId, applicationId }); await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
} }
// Deploy to Docker Engine // Deploy to Docker Engine
@ -259,15 +266,7 @@ export default async function (job) {
// //
} }
try { try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
// for await (const volume of volumes) {
// const id = volume.split(':')[0];
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`);
// } catch (error) {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`);
// }
// }
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes.map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
@ -300,19 +299,12 @@ export default async function (job) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
// const { stderr } = await asyncExecShell(
// `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
// ' '
// )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : ''
// } -d ${applicationId}:${tag}`
// );
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error); sentry.captureException(error);
throw new Error(error); throw new Error(error);
} }
saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
} }
} }

View File

@ -135,12 +135,12 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
} }
saveBuildLog({ await saveBuildLog({
line: 'Failed to deploy!', line: 'Failed to deploy!',
buildId: job.data.build_id, buildId: job.data.build_id,
applicationId: job.data.id applicationId: job.data.id
}); });
saveBuildLog({ await saveBuildLog({
line: `Reason: ${failedReason.toString()}`, line: `Reason: ${failedReason.toString()}`,
buildId: job.data.build_id, buildId: job.data.build_id,
applicationId: job.data.id applicationId: job.data.id

View File

@ -89,7 +89,6 @@
try { try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push($t('application.deployment_queued')); toast.push($t('application.deployment_queued'));
console.log($page.url);
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else { } else {

View File

@ -5,6 +5,7 @@ import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api'; import { get as getRequest } from '$lib/api';
import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -52,12 +53,23 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
} = await event.request.json(); } = await event.request.json();
if (port) port = Number(port); if (port) port = Number(port);
try { try {
const defaultConfiguration = await setDefaultConfiguration({
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
});
await db.configureApplication({ await db.configureApplication({
id, id,
buildPack, buildPack,
@ -68,7 +80,11 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
...defaultConfiguration
}); });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {

View File

@ -38,6 +38,7 @@
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@ -58,6 +59,23 @@
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let wsgis = [
{
value: 'None',
label: 'None'
},
{
value: 'Gunicorn',
label: 'Gunicorn'
}
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
} }
@ -112,7 +130,7 @@
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith($t('application.dns_not_set_partial_error'))) { if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true; forceSave = true;
} }
return errorNotification(error); return errorNotification(error);
@ -120,12 +138,19 @@
loading = false; loading = false;
} }
} }
async function selectWSGI(event) {
application.pythonWSGI = event.detail.value;
}
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="-mb-5 flex-col">
{application.name} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn} {#if application.fqdn}
<a <a
href={application.fqdn} href={application.fqdn}
@ -300,7 +325,7 @@
> >
{#if browser && window.location.hostname === 'demo.coolify.io'} {#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer <Explainer
text="<span class='text-white font-bold'>You can use the predefined random domain name or enter your own domain name.</span>" text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/> />
{/if} {/if}
<Explainer text={$t('application.https_explainer')} /> <Explainer text={$t('application.https_explainer')} />
@ -328,6 +353,39 @@
on:click={() => !isRunning && changeSettings('dualCerts')} on:click={() => !isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI</label>
<div class="custom-select-wrapper">
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
<input
readonly={!$session.isAdmin}
name="pythonModule"
id="pythonModule"
required
bind:value={application.pythonModule}
placeholder={application.pythonWSGI?.toLowerCase() !== 'gunicorn' ? 'main.py' : 'main'}
/>
</div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
<input
readonly={!$session.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
bind:value={application.pythonVariable}
placeholder="default: app"
/>
</div>
{/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">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
@ -336,7 +394,7 @@
name="port" name="port"
id="port" id="port"
bind:value={application.port} bind:value={application.port}
placeholder="{$t('forms.default')}: 3000" placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/> />
</div> </div>
{/if} {/if}

View File

@ -21,7 +21,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { dateOptions, getDomain } from '$lib/components/common'; import { changeQueryParams, dateOptions, getDomain } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte'; import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
@ -80,17 +80,81 @@
noMoreBuilds = true; noMoreBuilds = true;
} }
} }
async function loadBuild(build) { function loadBuild(build) {
buildId = build; buildId = build;
await goto(`/applications/${id}/logs/build?buildId=${buildId}`); return changeQueryParams(buildId);
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
{$t('application.build.build_logs_of')} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">$t('application.build_logs')}</div>
<a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<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>
</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-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<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 ">
@ -134,11 +198,14 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="flex space-x-2"> {#if !noMoreBuilds}
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds} {#if buildCount > 5}
>{$t('application.build.load_more')}</button <div class="flex space-x-2">
> <button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
</div> >
</div>
{/if}
{/if}
</div> </div>
<div class="flex-1 md:w-96"> <div class="flex-1 md:w-96">
{#if buildId} {#if buildId}

View File

@ -69,16 +69,83 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Application Logs
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<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>
</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>
{:else} {:else}
<div class="relative"> <div class="relative w-full">
<LoadingLogs /> <LoadingLogs />
<div class="flex justify-end sticky top-0 p-2"> <div class="flex justify-end sticky top-0 p-2">
<button <button
@ -106,7 +173,7 @@
</button> </button>
</div> </div>
<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 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}
> >
<div class="px-2"> <div class="px-2">

View File

@ -11,7 +11,6 @@
} }
}; };
} }
return { return {
status: res.status, status: res.status,
error: new Error(`Could not load ${endpoint}`) error: new Error(`Could not load ${endpoint}`)
@ -51,14 +50,88 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
</div>
{#if applicationSecrets.length !== 0} {#if application.fqdn}
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<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>
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
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>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@ -87,16 +160,9 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> {/if}
{/if}
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? $t('application.preview.setup_secret_app_first')
: $t('application.preview.values_overwriting_app_secrets')}
/>
</div> </div>
<div class="mx-auto max-w-4xl py-10"> <div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2"> <div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0} {#if containers.length > 0}

View File

@ -0,0 +1,48 @@
<script>
export let secrets;
export let refreshSecrets;
export let id;
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let batchSecrets = '';
function setBatchValue(event) {
batchSecrets = event.target?.value;
}
const limit = pLimit(1);
async function getValues(e) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, value] = secret.split('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew, dispatch }))
)
);
batchSecrets = '';
refreshSecrets();
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit={getValues} class="mb-12 w-full">
<textarea value={batchSecrets} on:change={setBatchValue} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
>
</form>

View File

@ -9,12 +9,11 @@
if (isPRMRSecret) value = PRMRSecret.value; if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { del, post } from '$lib/api'; import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
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;
@ -31,28 +30,20 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function saveSecret(isNew = false) {
if (!name) return errorNotification(`${$t('forms.name')} ${$t('forms.is_required')}`); async function createSecret(isNew) {
if (!value) return errorNotification(`${$t('forms.value')} ${$t('forms.is_required')}`); saveSecret({
try { isNew,
await post(`/applications/${id}/secrets.json`, { name,
name, value,
value, isBuildSecret,
isBuildSecret, isPRMRSecret,
isPRMRSecret, isNewSecret,
isNew applicationId: id,
}); dispatch
dispatch('refresh'); });
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
toast.push($t('application.secrets.secret_saved'));
} catch ({ error }) {
return errorNotification(error);
}
} }
function setSecretValue() { function setSecretValue() {
if (isNewSecret) { if (isNewSecret) {
isBuildSecret = !isBuildSecret; isBuildSecret = !isBuildSecret;
@ -134,14 +125,14 @@
<td> <td>
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)} <button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(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 justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="" on:click={() => saveSecret(false)}>{$t('forms.set')}</button> <button class="" on:click={() => createSecret(false)}>{$t('forms.set')}</button>
</div> </div>
{#if !isPRMRSecret} {#if !isPRMRSecret}
<div class="flex justify-center items-end"> <div class="flex justify-center items-end">

View File

@ -1,10 +1,10 @@
import { getTeam, getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
@ -24,7 +24,7 @@ export const get: RequestHandler = async (event) => {
}; };
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
@ -53,7 +53,7 @@ export const post: RequestHandler = async (event) => {
} }
}; };
export const del: RequestHandler = async (event) => { export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;

View File

@ -25,8 +25,9 @@
import Secret from './_Secret.svelte'; import Secret from './_Secret.svelte';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import BatchSecrets from './_BatchSecrets.svelte';
const { id } = $page.params; const { id } = $page.params;
@ -36,13 +37,77 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
{$t('application.secrets.secrets_for')} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">{$t('application.secret')}</div>
<a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<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>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl px-6 pt-4">
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@ -72,4 +137,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<BatchSecrets {secrets} {id} {refreshSecrets} />
</div> </div>

View File

@ -0,0 +1,46 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
applicationId: string;
dispatch: (name: string) => void;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId,
dispatch
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/applications/${applicationId}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
toast.push('Secret saved.');
} catch ({ error }) {
return errorNotification(error);
}
}

View File

@ -37,12 +37,77 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Persistent storage for <a href={application.fqdn} target="_blank" <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
>{getDomain(application.fqdn)}</a Persistent Storage
> </div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<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>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">

View File

@ -31,37 +31,20 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import DatabaseLinks from '$lib/components/DatabaseLinks.svelte';
import CouchDb from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDb from '$lib/components/svg/databases/MongoDB.svelte';
import MySql from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSql from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
export let database; export let database;
export let settings; export let settings;
export let privatePort; export let privatePort;
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex items-center space-x-2 p-6 text-2xl font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:block md:text-2xl"> <div class="-mb-5 flex-col">
{database.name} <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> </div>
<span class="relative"> <DatabaseLinks {database} />
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>
</div> </div>
<Databases bind:database {privatePort} {settings} /> <Databases bind:database {privatePort} {settings} />

View File

@ -20,6 +20,9 @@
emailEl.focus(); emailEl.focus();
}); });
async function handleSubmit() { async function handleSubmit() {
// Prevent double submission
if (loading) return;
if (password !== passwordCheck) { if (password !== passwordCheck) {
return errorNotification($t('forms.passwords_not_match')); return errorNotification($t('forms.passwords_not_match'));
} }
@ -89,8 +92,13 @@
/> />
<div class="flex space-x-2 h-8 items-center justify-center pt-8"> <div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button type="submit" class="hover:bg-coollabs-100 text-white bg-coollabs" <button
>{$t('register.register')}</button type="submit"
class="hover:bg-coollabs-100 text-white"
disabled={loading}
class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? $t('register.registering') : $t('register.register')}</button
> >
</div> </div>
</form> </form>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MeiliSearch</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="masterKey">Admin API key</label>
<CopyPasswordField
id="masterKey"
isPasswordField
readonly
disabled
name="masterKey"
value={service.meiliSearch.masterKey}
/>
</div>

View File

@ -12,6 +12,7 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import Ghost from './_Ghost.svelte'; import Ghost from './_Ghost.svelte';
import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte'; import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
@ -113,9 +114,7 @@
</div> </div>
<div class="grid grid-cols-2 px-10"> <div class="grid grid-cols-2 px-10">
<div class="flex-col "> <div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">{$t('application.url_fqdn')}</label>
>{$t('application.domain_fqdn')}</label
>
<Explainer text={$t('application.https_explainer')} /> <Explainer text={$t('application.https_explainer')} />
</div> </div>
@ -150,6 +149,8 @@
<Wordpress bind:service {isRunning} {readOnly} /> <Wordpress bind:service {isRunning} {readOnly} />
{:else if service.type === 'ghost'} {:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} /> <Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'}
<MeiliSearch bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@ -42,6 +42,7 @@
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte'; import Ghost from '$lib/components/svg/services/Ghost.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@ -87,6 +88,8 @@
<UptimeKuma isAbsolute /> <UptimeKuma isAbsolute />
{:else if type.name === 'ghost'} {:else if type.name === 'ghost'}
<Ghost isAbsolute /> <Ghost isAbsolute />
{:else if type.name === 'meilisearch'}
<MeiliSearch isAbsolute />
{/if}{type.fancyName} {/if}{type.fancyName}
</button> </button>
</form> </form>

View File

@ -29,19 +29,10 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
import MinIo from '$lib/components/svg/services/MinIO.svelte';
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
import Services from './_Services/_Services.svelte';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import ServiceLinks from '$lib/components/ServiceLinks.svelte';
import N8n from '$lib/components/svg/services/N8n.svelte'; import Services from './_Services/_Services.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let service; export let service;
export let isRunning; export let isRunning;
@ -52,13 +43,12 @@
} }
</script> </script>
<div <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
class="flex items-center space-x-3 px-6 text-2xl font-bold" <div class="-mb-5 flex-col">
class:p-5={service.fqdn} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
class:p-6={!service.fqdn} Configuration
> </div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <span class="text-xs">{service.name}</span>
{service.name}
</div> </div>
{#if service.fqdn} {#if service.fqdn}
<a <a
@ -83,49 +73,7 @@
> >
{/if} {/if}
<div> <ServiceLinks {service} />
{#if service.type === 'plausibleanalytics'}
<a href="https://plausible.io" target="_blank">
<PlausibleAnalytics />
</a>
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if}
</div>
</div> </div>
<Services bind:service {isRunning} {readOnly} /> <Services bind:service {isRunning} {readOnly} />

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateLanguageToolService({ id, fqdn, name }); await db.updateMeiliSearchService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -0,0 +1,21 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateLanguageToolService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,83 @@
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const {
meiliSearch: { masterKey }
} = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
image: `${image}:${version}`,
volume: `${id}-datams:/data.ms`,
environmentVariables: {
MEILI_MASTER_KEY: masterKey
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.environmentVariables[secret.name] = secret.value;
});
}
const composeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
environment: config.environmentVariables,
restart: 'always',
volumes: [config.volume],
labels: makeLabelForServices('meilisearch')
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
if (version === 'latest') {
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
);
}
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -27,6 +27,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import ServiceLinks from '$lib/components/ServiceLinks.svelte';
const { id } = $page.params; const { id } = $page.params;
@ -36,13 +37,39 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div
<div class="mr-4 text-2xl tracking-tight"> class="flex items-center space-x-2 p-5 px-6 font-bold"
{$t('application.secret')} class:p-5={service.fqdn}
{#if service.fqdn} class:p-6={!service.fqdn}
<a href={service.fqdn} target="_blank">{getDomain(service.fqdn)}</a> >
{/if} <div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">{$t('application.secret')}</div>
<span class="text-xs">{service.name}</span>
</div> </div>
{#if service.fqdn}
<a
href={service.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center 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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<ServiceLinks {service} />
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">

View File

@ -12,6 +12,7 @@
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte'; import Ghost from '$lib/components/svg/services/Ghost.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
export let services; export let services;
async function newService() { async function newService() {
@ -68,6 +69,8 @@
<UptimeKuma isAbsolute /> <UptimeKuma isAbsolute />
{:else if service.type === 'ghost'} {:else if service.type === 'ghost'}
<Ghost isAbsolute /> <Ghost isAbsolute />
{:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute />
{/if} {/if}
<div class="font-bold text-xl text-center truncate"> <div class="font-bold text-xl text-center truncate">
{service.name} {service.name}

View File

@ -123,7 +123,7 @@
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="flex-col"> <div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100"> <div class="pt-2 text-base font-bold text-stone-100">
{$t('application.domain_fqdn')} {$t('application.url_fqdn')}
</div> </div>
<Explainer text={$t('setting.ssl_explainer')} /> <Explainer text={$t('setting.ssl_explainer')} />
</div> </div>

View File

@ -54,10 +54,10 @@ textarea {
} }
#svelte .item.hover { #svelte .item.hover {
@apply bg-coolgray-100 text-white; @apply bg-coollabs text-white !important;
} }
#svelte .item.active { #svelte .item.active {
@apply bg-coollabs text-white; @apply bg-coolgray-100 text-white;
} }
select { select {