Merge github.com:coollabsio/coolify into exposePort
This commit is contained in:
@@ -25,7 +25,6 @@
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
selectedTeamId: session.teamId,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
@@ -35,9 +34,6 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let teams;
|
||||
export let selectedTeamId;
|
||||
|
||||
import '../tailwind.css';
|
||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast';
|
||||
import { page, session } from '$app/stores';
|
||||
@@ -45,7 +41,8 @@
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { asyncSleep } from '$lib/components/common';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { browser, dev } from '$app/env';
|
||||
import { dev } from '$app/env';
|
||||
import { features } from '$lib/store';
|
||||
let isUpdateAvailable = false;
|
||||
|
||||
let updateStatus = {
|
||||
@@ -56,7 +53,7 @@
|
||||
let latestVersion = 'latest';
|
||||
onMount(async () => {
|
||||
if ($session.userId) {
|
||||
const overrideVersion = browser && window.localStorage.getItem('latestVersion');
|
||||
const overrideVersion = $features.latestVersion;
|
||||
try {
|
||||
await get(`/login.json`);
|
||||
} catch ({ error }) {
|
||||
@@ -89,17 +86,6 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function switchTeam() {
|
||||
try {
|
||||
await post(`/dashboard.json?from=${$page.url.pathname}`, {
|
||||
cookie: 'teamId',
|
||||
value: selectedTeamId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
updateStatus.loading = true;
|
||||
@@ -525,21 +511,10 @@
|
||||
</div>
|
||||
</nav>
|
||||
{#if $session.whiteLabeled}
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
|
||||
>
|
||||
{/if}
|
||||
|
||||
<select
|
||||
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
|
||||
bind:value={selectedTeamId}
|
||||
on:change={switchTeam}
|
||||
>
|
||||
<option value="" disabled selected>Switch to a different team...</option>
|
||||
{#each teams as team}
|
||||
<option value={team.teamId}>{team.team.name} - {team.permission}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<main>
|
||||
<slot />
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
>
|
||||
<div class="border border-coolgray-500 h-8" />
|
||||
<a
|
||||
href={!$disabledButton ? `/applications/${id}/logs` : null}
|
||||
href={!$disabledButton && isRunning ? `/applications/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-sky-500 rounded"
|
||||
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
|
||||
@@ -402,7 +402,7 @@
|
||||
>
|
||||
<button
|
||||
title={$t('application.logs')}
|
||||
disabled={$disabledButton}
|
||||
disabled={$disabledButton || !isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('application.logs')}
|
||||
>
|
||||
|
||||
75
src/routes/applications/[id]/cancel.json.ts
Normal file
75
src/routes/applications/[id]/cancel.json.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { asyncExecShell, getEngine, removeDestinationDocker, saveBuildLog } from '$lib/common';
|
||||
import { buildQueue } from '$lib/queues';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import * as db from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { buildId, applicationId } = await event.request.json();
|
||||
if (!buildId) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: 'Build ID not found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
let count = 0;
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
const job = await buildQueue.getJob(buildId);
|
||||
const {
|
||||
destinationDocker: { engine }
|
||||
} = job.data;
|
||||
const host = getEngine(engine);
|
||||
let interval = setInterval(async () => {
|
||||
const { status } = await db.prisma.build.findUnique({ where: { id: buildId } });
|
||||
if (status === 'failed') {
|
||||
clearInterval(interval);
|
||||
return resolve();
|
||||
}
|
||||
if (count > 1200) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Could not cancel build.'));
|
||||
}
|
||||
try {
|
||||
const { stdout: buildContainers } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
|
||||
);
|
||||
if (buildContainers) {
|
||||
const containersArray = buildContainers.trim().split('\n');
|
||||
for (const container of containersArray) {
|
||||
const containerObj = JSON.parse(container);
|
||||
const id = containerObj.ID;
|
||||
if (!containerObj.Names.startsWith(`${applicationId}`)) {
|
||||
await removeDestinationDocker({ id, engine });
|
||||
clearInterval(interval);
|
||||
await saveBuildLog({
|
||||
line: 'Canceled by user!',
|
||||
buildId: job.data.build_id,
|
||||
applicationId: job.data.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
count++;
|
||||
} catch (error) {}
|
||||
}, 100);
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Build canceled.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -185,7 +185,7 @@
|
||||
? $t('application.configuration.loading_repositories')
|
||||
: $t('application.configuration.select_a_repository')}
|
||||
id="repository"
|
||||
showIndicator={true}
|
||||
showIndicator={!loading.repositories}
|
||||
isWaiting={loading.repositories}
|
||||
on:select={loadBranches}
|
||||
items={reposSelectOptions}
|
||||
@@ -202,7 +202,7 @@
|
||||
? $t('application.configuration.select_a_repository_first')
|
||||
: $t('application.configuration.select_a_branch')}
|
||||
isWaiting={loading.branches}
|
||||
showIndicator={selected.repository}
|
||||
showIndicator={selected.repository && !loading.branches}
|
||||
id="branches"
|
||||
on:select={isBranchAlreadyUsed}
|
||||
items={branchSelectOptions}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { ErrorHandler, generatePassword } from '$lib/database';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@@ -34,6 +34,30 @@ export const post: RequestHandler = async (event) => {
|
||||
|
||||
try {
|
||||
await db.configureBuildPack({ id, buildPack });
|
||||
|
||||
// Generate default secrets
|
||||
if (buildPack === 'laravel') {
|
||||
let found = await db.isSecretExists({ id, name: 'APP_ENV', isPRMRSecret: false });
|
||||
if (!found) {
|
||||
await db.createSecret({
|
||||
id,
|
||||
name: 'APP_ENV',
|
||||
value: 'production',
|
||||
isBuildSecret: false,
|
||||
isPRMRSecret: false
|
||||
});
|
||||
}
|
||||
found = await db.isSecretExists({ id, name: 'APP_KEY', isPRMRSecret: false });
|
||||
if (!found) {
|
||||
await db.createSecret({
|
||||
id,
|
||||
name: 'APP_KEY',
|
||||
value: generatePassword(32),
|
||||
isBuildSecret: false,
|
||||
isPRMRSecret: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
||||
@@ -85,13 +85,14 @@
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'blob'
|
||||
);
|
||||
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'blob');
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson) {
|
||||
} else if (packageJson && !laravel) {
|
||||
const path = packageJson.path;
|
||||
const data = await get(
|
||||
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
|
||||
@@ -107,8 +108,10 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP || composerPHP) {
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
@@ -134,13 +137,14 @@
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'file'
|
||||
);
|
||||
const laravel = files.find((file) => file.name === 'artisan' && file.type === 'file');
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson) {
|
||||
} else if (packageJson && !laravel) {
|
||||
const data = await get(`${packageJson.git_url}`, {
|
||||
Authorization: `Bearer ${$gitTokens.githubToken}`,
|
||||
Accept: 'application/vnd.github.v2.raw'
|
||||
@@ -153,8 +157,10 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP || composerPHP) {
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
@@ -225,7 +231,7 @@
|
||||
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
|
||||
{#each buildPacks as buildPack}
|
||||
<div class="p-2">
|
||||
<BuildPack {buildPack} {scanning} bind:foundConfig />
|
||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -46,15 +46,23 @@ export const post: RequestHandler = async (event) => {
|
||||
}
|
||||
});
|
||||
if (pullmergeRequestId) {
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'manual',
|
||||
...applicationFound,
|
||||
sourceBranch: branch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'manual',
|
||||
...applicationFound,
|
||||
sourceBranch: branch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
} else {
|
||||
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{ build_id: buildId, type: 'manual', ...applicationFound },
|
||||
{ jobId: buildId }
|
||||
);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
|
||||
@@ -61,7 +61,9 @@ export const post: RequestHandler = async (event) => {
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage
|
||||
} = await event.request.json();
|
||||
if (port) port = Number(port);
|
||||
if (exposePort) {
|
||||
@@ -99,6 +101,8 @@ export const post: RequestHandler = async (event) => {
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
...defaultConfiguration
|
||||
});
|
||||
return { status: 201 };
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
gitlabApp: Prisma.GitlabApp;
|
||||
gitSource: Prisma.GitSource;
|
||||
destinationDocker: Prisma.DestinationDocker;
|
||||
baseImages: Array<{ value: string; label: string }>;
|
||||
baseBuildImages: Array<{ value: string; label: string }>;
|
||||
};
|
||||
export let isRunning;
|
||||
import { page, session } from '$app/stores';
|
||||
@@ -72,11 +74,14 @@
|
||||
label: 'Gunicorn'
|
||||
}
|
||||
];
|
||||
|
||||
function containerClass() {
|
||||
if (!$session.isAdmin || isRunning) {
|
||||
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
|
||||
}
|
||||
}
|
||||
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
|
||||
application.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
domainEl.focus();
|
||||
});
|
||||
@@ -143,6 +148,14 @@
|
||||
async function selectWSGI(event) {
|
||||
application.pythonWSGI = event.detail.value;
|
||||
}
|
||||
async function selectBaseImage(event) {
|
||||
application.baseImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
async function selectBaseBuildImage(event) {
|
||||
application.baseBuildImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
@@ -315,6 +328,49 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="baseImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_image')}</label
|
||||
>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
isDisabled={!$session.isAdmin || isRunning}
|
||||
containerClasses={containerClass()}
|
||||
id="baseImages"
|
||||
showIndicator={!isRunning}
|
||||
items={application.baseImages}
|
||||
on:select={selectBaseImage}
|
||||
value={application.baseImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
<Explainer text={$t('application.base_image_explainer')} />
|
||||
</div>
|
||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_build_image')}</label
|
||||
>
|
||||
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
isDisabled={!$session.isAdmin || isRunning}
|
||||
containerClasses={containerClass()}
|
||||
id="baseBuildImages"
|
||||
showIndicator={!isRunning}
|
||||
items={application.baseBuildImages}
|
||||
on:select={selectBaseBuildImage}
|
||||
value={application.baseBuildImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
{#if application.buildPack === 'laravel'}
|
||||
<Explainer text="For building frontend assets with webpack." />
|
||||
{:else}
|
||||
<Explainer text={$t('application.base_build_image_explainer')} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">{$t('application.application')}</div>
|
||||
@@ -506,21 +562,23 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.base_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
||||
{#if application.buildPack !== 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.base_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
||||
</div>
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="{$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="{$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import LoadingLogs from '../_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
let followingInterval;
|
||||
let logsEl;
|
||||
|
||||
let cancelInprogress = false;
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
|
||||
@@ -67,6 +69,19 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function cancelBuild() {
|
||||
if (cancelInprogress) return;
|
||||
try {
|
||||
cancelInprogress = true;
|
||||
await post(`/applications/${id}/cancel.json`, {
|
||||
buildId,
|
||||
applicationId: id
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(streamInterval);
|
||||
clearInterval(followingInterval);
|
||||
@@ -90,7 +105,7 @@
|
||||
<div class="flex justify-end sticky top-0 p-2">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingBuild}
|
||||
>
|
||||
@@ -111,7 +126,30 @@
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if currentStatus === 'running'}
|
||||
<button
|
||||
on:click={cancelBuild}
|
||||
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Cancel build"
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<path d="M10 10l4 4m0 -4l-4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</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}
|
||||
|
||||
@@ -98,12 +98,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<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">
|
||||
Application Logs
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
<span class="text-xs">{application.name}</span>
|
||||
</div>
|
||||
|
||||
{#if application.fqdn}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import Astro from '$lib/components/svg/applications/Astro.svelte';
|
||||
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
|
||||
import Deno from '$lib/components/svg/applications/Deno.svelte';
|
||||
import Laravel from '$lib/components/svg/applications/Laravel.svelte';
|
||||
|
||||
async function newApplication() {
|
||||
const { id } = await post('/applications/new', {});
|
||||
@@ -104,6 +105,8 @@
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -162,6 +165,8 @@
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
@@ -65,6 +65,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
export let database;
|
||||
export let isRunning;
|
||||
let loading = false;
|
||||
@@ -163,6 +165,75 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href="/databases/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.configurations')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.configurations')}
|
||||
>
|
||||
<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" />
|
||||
<rect x="4" y="8" width="4" height="4" />
|
||||
<line x1="6" y1="4" x2="6" y2="8" />
|
||||
<line x1="6" y1="12" x2="6" y2="20" />
|
||||
<rect x="10" y="14" width="4" height="4" />
|
||||
<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
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={isRunning ? `/databases/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('database.logs')}
|
||||
disabled={!isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('database.logs')}
|
||||
>
|
||||
<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
|
||||
>
|
||||
<button
|
||||
on:click={deleteDatabase}
|
||||
title={$t('database.delete_database')}
|
||||
|
||||
41
src/routes/databases/[id]/logs/_Loading.svelte
Normal file
41
src/routes/databases/[id]/logs/_Loading.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="lds-ripple absolute left-0">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -19px;
|
||||
top: -8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/routes/databases/[id]/logs/index.json.ts
Normal file
66
src/routes/databases/[id]/logs/index.json.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let since = event.url.searchParams.get('since') || 0;
|
||||
if (since !== 0) {
|
||||
since = dayjs(since).unix();
|
||||
}
|
||||
try {
|
||||
const { destinationDockerId, destinationDocker } = await db.prisma.database.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
body: {
|
||||
logs
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
body: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
179
src/routes/databases/[id]/logs/index.svelte
Normal file
179
src/routes/databases/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
let endpoint = `/databases/${params.id}/logs.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
database: stuff.database,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let database;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loadLogsInterval = null;
|
||||
let logs = [];
|
||||
let lastLog = null;
|
||||
let followingInterval;
|
||||
let followingLogs;
|
||||
let logsEl;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/databases/${id}/logs.json`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/databases/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
{#if database.fqdn}
|
||||
<a
|
||||
href={database.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}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</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}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { session } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
@@ -81,6 +81,18 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchTeam(selectedTeamId) {
|
||||
try {
|
||||
await post(`/dashboard.json?from=${$page.url.pathname}`, {
|
||||
cookie: 'teamId',
|
||||
value: selectedTeamId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@@ -175,20 +187,39 @@
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
||||
{#each ownTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
||||
class:hover:bg-red-500={team.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
<div class="box-selection relative">
|
||||
<div>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="mt-1 text-center text-xs">
|
||||
{team.permissions?.length} member(s)
|
||||
</div>
|
||||
</div>
|
||||
<div class="truncate text-center font-bold">
|
||||
{team.id === '0' ? 'root team' : ''}
|
||||
</div>
|
||||
|
||||
<div class:mt-6={team.id !== '0'} class="mt-1 text-center">
|
||||
{team.permissions?.length} member(s)
|
||||
<div class="flex items-center justify-center pt-3">
|
||||
<button
|
||||
on:click|preventDefault={() => switchTeam(team.id)}
|
||||
class:bg-fuchsia-600={$session.teamId !== team.id}
|
||||
class:hover:bg-fuchsia-500={$session.teamId !== team.id}
|
||||
class:bg-transparent={$session.teamId === team.id}
|
||||
disabled={$session.teamId === team.id}
|
||||
><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 17h5l1.67 -2.386m3.66 -5.227l1.67 -2.387h6" />
|
||||
<path d="M18 4l3 3l-3 3" />
|
||||
<path d="M3 7h5l7 10h6" />
|
||||
<path d="M18 20l3 -3l-3 -3" />
|
||||
</svg></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -207,9 +238,6 @@
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="truncate text-center font-bold">
|
||||
{team.id === '0' ? 'root team' : ''}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import * as db from '$lib/database';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const data = await event.request.json();
|
||||
for (const d of data) {
|
||||
if (d.container_name) {
|
||||
const { log, container_name: containerId, source } = d;
|
||||
console.log(log);
|
||||
// await db.prisma.applicationLogs.create({ data: { log, containerId: containerId.substr(1), source } });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {}
|
||||
};
|
||||
};
|
||||
184
src/routes/services/[id]/_Services/_Fider.svelte
Normal file
184
src/routes/services/[id]/_Services/_Fider.svelte
Normal file
@@ -0,0 +1,184 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import Select from 'svelte-select';
|
||||
export let service;
|
||||
export let readOnly;
|
||||
|
||||
let mailgunRegions = [
|
||||
{
|
||||
value: 'EU',
|
||||
label: 'EU'
|
||||
},
|
||||
{
|
||||
value: 'US',
|
||||
label: 'US'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Fider</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="jwtSecret">JWT Secret</label>
|
||||
<CopyPasswordField
|
||||
name="jwtSecret"
|
||||
id="jwtSecret"
|
||||
isPasswordField
|
||||
value={service.fider.jwtSecret}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailNoreply">Noreply Email</label>
|
||||
<input
|
||||
name="emailNoreply"
|
||||
id="emailNoreply"
|
||||
type="email"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailNoreply}
|
||||
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Email</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
||||
<CopyPasswordField
|
||||
name="emailMailgunApiKey"
|
||||
id="emailMailgunApiKey"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailMailgunApiKey}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunDomain">Mailgun Domain</label>
|
||||
<input
|
||||
name="emailMailgunDomain"
|
||||
id="emailMailgunDomain"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailMailgunDomain}
|
||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunRegion">Mailgun Region</label>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
id="baseBuildImages"
|
||||
items={mailgunRegions}
|
||||
showIndicator
|
||||
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
|
||||
value={service.fider.emailMailgunRegion || 'EU'}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 px-10 font-bold">
|
||||
<div class="text-lg">Or</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpHost">SMTP Host</label>
|
||||
<input
|
||||
name="emailSmtpHost"
|
||||
id="emailSmtpHost"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpHost}
|
||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPort">SMTP Port</label>
|
||||
<input
|
||||
name="emailSmtpPort"
|
||||
id="emailSmtpPort"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpPort}
|
||||
placeholder="{$t('forms.eg')}: 587"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpUser">SMTP User</label>
|
||||
<input
|
||||
name="emailSmtpUser"
|
||||
id="emailSmtpUser"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpUser}
|
||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPassword">SMTP Password</label>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
id="emailSmtpPassword"
|
||||
isPasswordField
|
||||
value={service.fider.emailSmtpPassword}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
|
||||
<input
|
||||
name="emailSmtpEnableStartTls"
|
||||
id="emailSmtpEnableStartTls"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpEnableStartTls}
|
||||
placeholder="{$t('forms.eg')}: true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.fider.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.fider.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.fider.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
58
src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
58
src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Hasura</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
|
||||
<CopyPasswordField
|
||||
name="graphQLAdminPassword"
|
||||
id="graphQLAdminPassword"
|
||||
isPasswordField
|
||||
value={service.hasura.graphQLAdminPassword}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.hasura.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.hasura.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.hasura.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -12,7 +12,9 @@
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import Fider from './_Fider.svelte';
|
||||
import Ghost from './_Ghost.svelte';
|
||||
import Hasura from './_Hasura.svelte';
|
||||
import MeiliSearch from './_MeiliSearch.svelte';
|
||||
import MinIo from './_MinIO.svelte';
|
||||
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
|
||||
@@ -199,6 +201,10 @@
|
||||
<MeiliSearch bind:service />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami bind:service />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura bind:service />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider bind:service {readOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -270,6 +270,38 @@
|
||||
</button></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('service.logs')}
|
||||
disabled={!isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('service.logs')}
|
||||
>
|
||||
<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
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteService}
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
import { t } from '$lib/translations';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
@@ -93,6 +95,10 @@
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if type.name === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if type.name === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if type.name === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
57
src/routes/services/[id]/fider/index.json.ts
Normal file
57
src/routes/services/[id]/fider/index.json.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { encrypt } from '$lib/crypto';
|
||||
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,
|
||||
fider: {
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}
|
||||
} = await event.request.json();
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (emailNoreply) emailNoreply = emailNoreply.toLowerCase();
|
||||
if (emailSmtpHost) emailSmtpHost = emailSmtpHost.toLowerCase();
|
||||
if (emailSmtpPassword) {
|
||||
emailSmtpPassword = encrypt(emailSmtpPassword);
|
||||
}
|
||||
if (emailSmtpPort) emailSmtpPort = Number(emailSmtpPort);
|
||||
if (emailSmtpEnableStartTls) emailSmtpEnableStartTls = Boolean(emailSmtpEnableStartTls);
|
||||
|
||||
try {
|
||||
await db.updateFiderService({
|
||||
id,
|
||||
fqdn,
|
||||
name,
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
});
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
154
src/routes/services/[id]/fider/start.json.ts
Normal file
154
src/routes/services/[id]/fider/start.json.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
asyncExecShell,
|
||||
createDirectories,
|
||||
getDomain,
|
||||
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';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
|
||||
|
||||
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: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
|
||||
await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
fqdn,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
serviceSecret,
|
||||
fider: {
|
||||
postgresqlUser,
|
||||
postgresqlPassword,
|
||||
postgresqlDatabase,
|
||||
jwtSecret,
|
||||
emailNoreply,
|
||||
emailMailgunApiKey,
|
||||
emailMailgunDomain,
|
||||
emailMailgunRegion,
|
||||
emailSmtpHost,
|
||||
emailSmtpPort,
|
||||
emailSmtpUser,
|
||||
emailSmtpPassword,
|
||||
emailSmtpEnableStartTls
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
const domain = getDomain(fqdn);
|
||||
const config = {
|
||||
fider: {
|
||||
image: `${image}:${version}`,
|
||||
environmentVariables: {
|
||||
HOST_DOMAIN: domain,
|
||||
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`,
|
||||
JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`,
|
||||
EMAIL_NOREPLY: emailNoreply,
|
||||
EMAIL_MAILGUN_API: emailMailgunApiKey,
|
||||
EMAIL_MAILGUN_REGION: emailMailgunRegion,
|
||||
EMAIL_MAILGUN_DOMAIN: emailMailgunDomain,
|
||||
EMAIL_SMTP_HOST: emailSmtpHost,
|
||||
EMAIL_SMTP_PORT: emailSmtpPort,
|
||||
EMAIL_SMTP_USER: emailSmtpUser,
|
||||
EMAIL_SMTP_PASSWORD: emailSmtpPassword,
|
||||
EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
image: 'postgres:12-alpine',
|
||||
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
|
||||
environmentVariables: {
|
||||
POSTGRES_USER: postgresqlUser,
|
||||
POSTGRES_PASSWORD: postgresqlPassword,
|
||||
POSTGRES_DB: postgresqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.fider.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.fider.image,
|
||||
environment: config.fider.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [],
|
||||
restart: 'always',
|
||||
labels: makeLabelForServices('fider'),
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
},
|
||||
depends_on: [`${id}-postgresql`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
image: config.postgresql.image,
|
||||
container_name: `${id}-postgresql`,
|
||||
environment: config.postgresql.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
name: config.postgresql.volume.split(':')[0]
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
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) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/fider/stop.json.ts
Normal file
42
src/routes/services/[id]/fider/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { checkContainer, stopTcpHttpProxy } 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 } = 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);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
21
src/routes/services/[id]/hasura/index.json.ts
Normal file
21
src/routes/services/[id]/hasura/index.json.ts
Normal 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.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
122
src/routes/services/[id]/hasura/start.json.ts
Normal file
122
src/routes/services/[id]/hasura/start.json.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
|
||||
|
||||
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: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
|
||||
await db.getService({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
version,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
serviceSecret,
|
||||
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
||||
} = 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 = {
|
||||
hasura: {
|
||||
image: `${image}:${version}`,
|
||||
environmentVariables: {
|
||||
HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`
|
||||
}
|
||||
},
|
||||
postgresql: {
|
||||
image: 'postgres:12-alpine',
|
||||
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
|
||||
environmentVariables: {
|
||||
POSTGRES_USER: postgresqlUser,
|
||||
POSTGRES_PASSWORD: postgresqlPassword,
|
||||
POSTGRES_DB: postgresqlDatabase
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.hasura.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image: config.hasura.image,
|
||||
environment: config.hasura.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [],
|
||||
restart: 'always',
|
||||
labels: makeLabelForServices('hasura'),
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
},
|
||||
depends_on: [`${id}-postgresql`]
|
||||
},
|
||||
[`${id}-postgresql`]: {
|
||||
image: config.postgresql.image,
|
||||
container_name: `${id}-postgresql`,
|
||||
environment: config.postgresql.environmentVariables,
|
||||
networks: [network],
|
||||
volumes: [config.postgresql.volume],
|
||||
restart: 'always',
|
||||
deploy: {
|
||||
restart_policy: {
|
||||
condition: 'on-failure',
|
||||
delay: '5s',
|
||||
max_attempts: 3,
|
||||
window: '120s'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[config.postgresql.volume.split(':')[0]]: {
|
||||
name: config.postgresql.volume.split(':')[0]
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
try {
|
||||
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) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/services/[id]/hasura/stop.json.ts
Normal file
42
src/routes/services/[id]/hasura/stop.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getUserDetails, removeDestinationDocker } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { checkContainer, stopTcpHttpProxy } 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 } = 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);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
if (found) {
|
||||
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
41
src/routes/services/[id]/logs/_Loading.svelte
Normal file
41
src/routes/services/[id]/logs/_Loading.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="lds-ripple absolute left-0">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -19px;
|
||||
top: -8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/routes/services/[id]/logs/index.json.ts
Normal file
66
src/routes/services/[id]/logs/index.json.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let since = event.url.searchParams.get('since') || 0;
|
||||
if (since !== 0) {
|
||||
since = dayjs(since).unix();
|
||||
}
|
||||
try {
|
||||
const { destinationDockerId, destinationDocker } = await db.prisma.service.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
body: {
|
||||
logs
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
body: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
179
src/routes/services/[id]/logs/index.svelte
Normal file
179
src/routes/services/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
let endpoint = `/services/${params.id}/logs.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loadLogsInterval = null;
|
||||
let logs = [];
|
||||
let lastLog = null;
|
||||
let followingInterval;
|
||||
let followingLogs;
|
||||
let logsEl;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/services/${id}/logs.json`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/services/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</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">
|
||||
Service Logs
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</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}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</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}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -16,6 +16,8 @@
|
||||
import { session } from '$app/stores';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
export let services;
|
||||
async function newService() {
|
||||
@@ -89,6 +91,10 @@
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
@@ -138,6 +144,10 @@
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
|
||||
@@ -64,13 +64,20 @@ export const post: RequestHandler = async (event) => {
|
||||
};
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort, isAutoUpdateEnabled } =
|
||||
await event.request.json();
|
||||
const {
|
||||
fqdn,
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
minPort,
|
||||
maxPort,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
} = await event.request.json();
|
||||
try {
|
||||
const { id } = await db.listSettings();
|
||||
await db.prisma.setting.update({
|
||||
where: { id },
|
||||
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled }
|
||||
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled }
|
||||
});
|
||||
if (fqdn) {
|
||||
await db.prisma.setting.update({ where: { id }, data: { fqdn } });
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
import { session } from '$app/stores';
|
||||
|
||||
export let settings;
|
||||
import Cookies from 'js-cookie';
|
||||
import langs from '$lib/lang.json';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
@@ -39,10 +37,12 @@
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { features } from '$lib/store';
|
||||
|
||||
let isRegistrationEnabled = settings.isRegistrationEnabled;
|
||||
let dualCerts = settings.dualCerts;
|
||||
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
|
||||
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
|
||||
|
||||
let minPort = settings.minPort;
|
||||
let maxPort = settings.maxPort;
|
||||
@@ -78,7 +78,15 @@
|
||||
if (name === 'isAutoUpdateEnabled') {
|
||||
isAutoUpdateEnabled = !isAutoUpdateEnabled;
|
||||
}
|
||||
await post(`/settings.json`, { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled });
|
||||
if (name === 'isDNSCheckEnabled') {
|
||||
isDNSCheckEnabled = !isDNSCheckEnabled;
|
||||
}
|
||||
await post(`/settings.json`, {
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
});
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
@@ -176,13 +184,21 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isDNSCheckEnabled}
|
||||
title={$t('setting.is_dns_check_enabled')}
|
||||
description={$t('setting.is_dns_check_enabled_explainer')}
|
||||
on:click={() => changeSettings('isDNSCheckEnabled')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
dataTooltip={$t('setting.must_remove_domain_before_changing')}
|
||||
disabled={isFqdnSet}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('services.generate_www_non_www_ssl')}
|
||||
description={$t('setting.generate_www_non_www_ssl')}
|
||||
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
@@ -194,7 +210,7 @@
|
||||
on:click={() => changeSettings('isRegistrationEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{#if browser && (window.location.hostname === 'staging.coolify.io' || window.location.hostname === 'localhost')}
|
||||
{#if browser && $features.beta}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isAutoUpdateEnabled}
|
||||
|
||||
@@ -101,11 +101,15 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_commit'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@@ -161,13 +165,17 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_pr'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_pr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_pr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -74,11 +74,15 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_commit'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_commit',
|
||||
...applicationFound
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@@ -157,13 +161,17 @@ export const post: RequestHandler = async (event) => {
|
||||
type: 'webhook_mr'
|
||||
}
|
||||
});
|
||||
await buildQueue.add(buildId, {
|
||||
build_id: buildId,
|
||||
type: 'webhook_mr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
});
|
||||
await buildQueue.add(
|
||||
buildId,
|
||||
{
|
||||
build_id: buildId,
|
||||
type: 'webhook_mr',
|
||||
...applicationFound,
|
||||
sourceBranch,
|
||||
pullmergeRequestId
|
||||
},
|
||||
{ jobId: buildId }
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
Reference in New Issue
Block a user